MySQL InnoDB Add Index实现调研(一:Inplace Add Index)

11月 6th, 2012
  1. MySQL Add Index实现

MySQL各版本,对于add Index的处理方式是不同的,主要有三种:

 

  • Copy Table方式

    这是InnoDB最早支持的创建索引的方式。顾名思义,创建索引是通过临时表拷贝的方式实现的。

    新建一个带有新索引的临时表,将原表数据全部拷贝到临时表,然后Rename,完成创建索引的操作。

    这个方式创建索引,创建过程中,原表是可读的。但是会消耗一倍的存储空间。

  • Inplace方式

    这是原生MySQL 5.5,以及innodb_plugin中提供的创建索引的方式。所谓Inplace,也就是索引创建在原表上直接进行,不会拷贝临时表。相对于Copy Table方式,这是一个进步。

    Inplace方式创建索引,创建过程中,原表同样可读的,但是不可写。

  • 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

本章节,主要通过测试/源码跟踪的方式,调研InnoDB Inplace Add Index的实现方式。以及分析Inplace add Index有哪些需要注意的地方。

  1. 测试准备

测试版本

MySQL 5.5.25

测试表

    create table t1 (a int primary key, b int)engine=innodb;

    insert into t1 values (1,1),(2,2),(3,3),(4,4);

  1. Inplace Add Index处理流程

SQL

    alter table t1 add index idx_t1_b(b);

 

处理流程

    sql_table.cc::mysql_alter_table();

        // 判断当前操作是否可以进行Inplace实现,不可进行Inplace Alter的包括:

        // 1. Auto Increment字段修改;

        // 2. 列重命名;

        // 3. 行存储格式修改;等

        mysql_compare_tables() -> ha_innobase::check_if_incompatible_data();

        // Inplace创建索引第一阶段(主要阶段)

        handler0alter.cc::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();

        // 等待打开当前表的所有只读事务提交

        sql_base.cc::wait_while_table_is_used();

        // 创建索引结束,做最后的清理工作

        handler0alter.cc::final_add_index();

        // Inplace add Index完毕

  1. 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创建索引的事务

        // 此处,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

  1. Rhett
    11月 10th, 201422:26

    这种滞后报错应该不妥吧,既然可以查看到该索引对于当前会话sesion 1不可见,为什么不在计划选择时,忽略掉这种不可见或者说不可以用的索引,这样应该是可以避开滞后报错带来一种有点莫名其妙的感觉。