MySQL下使⽤Inplace和Online⽅式创建索引的教程MySQL各版本,对于add Index的处理⽅式是不同的,主要有三种:
(1)Copy Table⽅式
这是InnoDB最早⽀持的创建索引的⽅式。顾名思义,创建索引是通过临时表拷贝的⽅式实现的。
新建⼀个带有新索引的临时表,将原表数据全部拷贝到临时表,然后Rename,完成创建索引的操作。
这个⽅式创建索引,创建过程中,原表是可读的。但是会消耗⼀倍的存储空间。
(2)Inplace⽅式
这是原⽣MySQL 5.5,以及innodb_plugin中提供的创建索引的⽅式。所谓Inplace,也就是索引创建在原表上直接进⾏,不会拷贝临时表。相对于Copy Table⽅式,这是⼀个进步。
Inplace⽅式创建索引,创建过程中,原表同样可读的,但是不可写。
(3)Online⽅式
这是MySQL 5.6.7中提供的创建索引的⽅式。⽆论是Copy Table⽅式,还是Inplace⽅式,创建索引的过程
中,原表只能允许读取,不可写。对应⽤有较⼤的限制,因此MySQL最新版本中,InnoDB⽀持了所谓的Online⽅式创建索引。
InnoDB的Online Add Index,⾸先是Inplace⽅式创建索引,⽆需使⽤临时表。在遍历聚簇索引,收集记录并插⼊到新索引的过程中,原表记录可修改。⽽修改的记录保存在Row Log中。当聚簇索引遍历完毕,并全部插⼊到新索引之后,重放Row Log 中的记录修改,使得新索引与聚簇索引记录达到⼀致状态。
与Copy Table⽅式相⽐,Online Add Index采⽤的是Inplace⽅式,⽆需Copy Table,减少了空间开销;与此同时,Online Add Index只有在重放Row Log最后⼀个Block时锁表,减少了锁表的时间。
与Inplace⽅式相⽐,Online Add Index吸收了Inplace⽅式的优势,却减少了锁表的时间。
1.Inplace add Index
测试表
create table t1 (a int primary key, b int)engine=innodb;
insert into t1 values (1,1),(2,2),(3,3),(4,4);
Inplace Add Index处理流程
SQL
alter table t1 add index idx_t1_b(b);
处理流程
::mysql_alter_table();
// 判断当前操作是否可以进⾏Inplace实现,不可进⾏Inplace Alter的包括:
// 1. Auto Increment字段修改;
// 2. 列重命名;
// 3. ⾏存储格式修改;等
mysql_compare_tables() -> ha_innobase::check_if_incompatible_data();
// Inplace创建索引第⼀阶段(主要阶段)
<::add_index();
// 创建索引数据字典
row0merge.c::row_merge_create_index();
index = dict_mem_index_create();
// 每个索引数据字典上,有⼀个trx_id,记录创建此索引的事务
// 此trx_id有何功能,接着往下看
index->trx_id = trx_id;
// 读取聚簇索引,构造新索引的项,排序并插⼊新索引
row0merge.c::row_merge_build_indexes();
// 读取聚簇索引,注意:只读取其中的⾮删除项
/
/ 跳过所有删除项,为什么可以这么做?往下看
row_merge_read_clustered_index();
// ⽂件排序
row_merge_sort();
// 顺序读取排序⽂件中的索引项,逐个插⼊新建索引中
row_merge_insert_index_tuples();
// 等待打开当前表的所有只读事务提交
::wait_while_table_is_used();
// 创建索引结束,做最后的清理⼯作
<::final_add_index();
// Inplace add Index完毕
Inplace Add Index实现分析
在索引创建完成之后,MySQL Server⽴即可以使⽤新建的索引,做查询。但是,根据以上流程,对我个⼈来说,有三个疑问点:
索引数据字典上,为何需要维护⼀个trx_id?
trx_id有何作⽤?
遍历聚簇索引读取所有记录时,为何可跳过删除项?
只读取⾮删除项,那么新建索引上没有版本信息,⽆法处理原有事务的快照读;
MySQL Server层,为何需要等待打开表的只读事务提交?
等待当前表上的只读事务,可以保证这些事务不会使⽤到新建索引
根据分析,等待打开表的只读事务结束较好理解。因为新索引上没有版本信息,若这些事务使⽤新的索引,将会读不到正确的版本记录。
那么InnoDB是如何处理其他那些在创建索引之前已经开始,但却⼀直未提交的⽼事务呢?这些事务,由
于前期为并未读取当前表,因此不会被等待结束。这些事务在RR隔离级别下,会读取不到正确的版本记录,因为使⽤的索引上并没有版本信息。
当然,InnoDB同样考虑到了此问题,并采⽤了⼀种⽐较简介的处理⽅案。在索引上维护⼀个trx_id,标识创建此索引的事务ID。若有⼀个⽐这个事务更⽼的事务,打算使⽤新建的索引进⾏快照读,那么直接报错。
考虑如下的并发处理流程(事务隔离级别为RR):
session 1:                              session 2:
// 此时创建Global ReadView
select * from t2;
delete from t1 where b = 1;
// idx_t1_b索引上,没有b = 1的项
alter table t1 add index idx_t1_b(b);
/
/ 由于ReadView在delete之前获取
// 因此b = 1这⼀项应该被读取到
select * from t1 where b = 1;
当session 1执⾏最后⼀条select时,MySQL Optimizer会选择idx_t1_b索引进⾏查询,但是索引上并没有b = 1的项,使⽤此索引会导致查询出错。那么,InnoDB是如何处理这个情况的呢?
处理流程:
ha_innobase::index_init();
change_active_index();
// 判断session 1事务的ReadView是否可以看到session 2创建索引的事务
查看mysql索引// 此处,session 2事务当然不可见,那么prebuilt->index_usable = false
prebuilt->index_usable = row_merge_is_index_usable(readview, index->trx_id);
ha_innobase::index_read();
// 判断index_usable属性,此时为false,返回上层表定义修改,查询失败
if (!prebuilt->index_usable)
return HA_ERR_TABLE_DEF_CHANGED;
MySQL Server收到InnoDB返回的错误之后,会将错误报给⽤户,⽤户会收到以下错误:
mysql> select * from t1 where b = 1;
ERROR 1412 (HY000): Table definition has changed, please retry transaction
2.Online add Index
测试表
create table t1 (a int primary key, b int)engine=innodb;
insert into t1 values (1,1),(2,2),(3,3),(4,4);
Online Add Index处理流程
SQL
alter table t1 add index idx_t1_b(b);
处理流程
::mysql_alter_table();
// 1. 判断当前DDL操作是否可以Inplace进⾏
check_if_supported_inplace_alter();
// 2. 开始进⾏Online创建的前期准备⼯作
prepare_inplace_alter_table();
// 修改表的数据字典信息
prepare_inplace_alter_table_dict();
// 等待InnoDB所有的后台线程,停⽌操作此表
dict_stats_wait_bg_to_stop_using_tables();
// Online Add Index区别与Inplace Add Index的关键
// 在Online操作时,原表同时可以读写,因此需要
// 将此过程中的修改操作记录到row log之中
<::row_log_allocate();
row_log_t* log = (row_log_t*)&buf[2 * srv_sort_buf_size];
// 标识当前索引状态为Online创建,那么此索引上的
// DML操作会被写⼊Row Log,⽽不在索引上进⾏更新
dict_index_set_online_status(index, ONLINE_INDEX_CREATION);      …
// 3. 开始进⾏真正的Online Add Index的操作(最重要的流程)
inplace_alter_table();
// 此函数的操作,前部分与Inplace Add Index基本⼀致
// 读取聚簇索引、排序、并插⼊到新建索引中
// 最⼤的不同在于,当插⼊完成之后,Online Add Index
// 还需要将row log中的记录变化,更新到新建索引中
<::row_merge_build_index();
/
/ 在聚簇索引读取、排序、插⼊新建索引的操作结束之后
// 进⼊Online与Inplace真正的不同之处,也是Online操作
// 的精髓部分——将这个过程中产⽣的Row Log重⽤
<::row_log_apply();
// 暂时将新建索引整个索引树完全锁住
// 注意:只是暂时性锁住,并不是在整个重⽤Row Log的
// 过程中⼀直加锁(防⽌加锁时间过长的优化,如何优化?)
rw_lock_x_lock(dict_index_get_lock(new_index));
// InnoDB Online操作最重要的处理流程
// 将Online Copy Table中,记录的Row Log重放到新建索引上
/
/ 重放Row Log的算法如下:
// 1. Row Log中记录的是Online创建索引期间,原表上的DML操作
//  这些操作包括:ROW_OP_INSERT;ROW_OP_DELETE_MARK; …
// 2. Row Log以Block的⽅式存储,若DML较多,那么Row Logs可能
//  会占⽤多个Blocks。row_log_t结构中包含两个指针:head与tail
//  head指针⽤于读取Row Log,tail指针⽤于追加写新的Row Log;
// 3.在重⽤Row Log时,算法遵循⼀个原则:尽量减少索引树加锁
//  的时间(索引树加X锁,也意味着表上禁⽌了新的DML操作)
//  索引树需要加锁的场景:
//  (⼀) 在重⽤Row Log跨越新的Block时,需要短暂加锁;
//  (⼆) 若应⽤的Row Log Block是最后⼀个Block,那么⼀直加锁
/
/    应⽤最后⼀个Block,由于禁⽌了新的DML操作,因此此
//    Block应⽤完毕,新索引记录与聚簇索引达到⼀致状态,
//    重⽤阶段结束;
//  (三) 在应⽤中间Row Log Block上的row log时,⽆需加锁,新的
//    DML操作仍旧可以进⾏,产⽣的row log记录到最后⼀个
//    Row Log Block之上;
// 4. 如果是创建Unique索引,那么在应⽤Row Log时,可能会出现
//  违反唯⼀性约束的情况,这些情况会被记录到
//  row_merge_dup_t结构之中
row_log_apply_ops(trx, index, &dup);
row_log_apply_op();
row_log_apply_op_low();
// 将New Index的Online row log设置为NULL,
// 标识New Index的数据已经与聚簇索引完全⼀致