先来看一组测试数据
使用sysbench,生成一张大约10,000,000行记录的表,数据全部在buffer pool中
创建索引(k,pad)
5.7.5
alter table sbtest1 add key(k,pad);
query ok, 0 rows affected (44.14 sec)
records: 0 duplicates: 0 warnings: 0
5.7.4
root@sbb 04:25:11>alter table sbtest1 add key(k,pad);
query ok, 0 rows affected (1 min 2.03 sec)
几轮测试的结果都差不多,5.7.5的索引创建速度总是优于5.7.4(同时也优于5.6).
ok,老规矩,我们来看主要对索引创建做了什么样的优化,在5.7.5的changelog entry如下:
innodb: instead of inserting one index record at a time, innodb now performs a bulk load when creating or rebuilding indexes. this method of index creation is also known as a “sorted index build”. this enhancement, which improves the efficiency of index creation, also applies to full-text indexes. a new global configuration option, innodb_fill_factor, defines the percentage of space on each page that is filled with data during a sorted index build, with the remaining space reserved for future index growth. for more information, see bulk load for create index.
在之前的版本中,创建索引时总是一条记录一条记录的插入索引,当记录量比较大时,这种插入方式非常低效,因此引入了bulk load的方式。采用自底向上的构建方式。大体思路为,总是按照从左向右,从底往上的方式来构建btree记录。
因此修改的点应该在获取了已经归并排序完成的索引记录之后,准备向新构建的btree中插入记录。老的实现方式有明显的缺点:1.每次插入btree都需要重定位cursor;2.自顶向下的构建索引可能导致大量的索引分裂。
0.background
新增源文件:btr/btr0bulk.cc
定义了两个类来辅助索引
pagebulk:用于处理页内操作
btrbulk:用于处理btree操作,针对每层btree都有一个pagebulk对象
1.总体入口
我们以上述测试用的建索引语句为例
入口函数:row_merge_build_indexes
在完成对二级索引记录的排序文件后,进入插入阶段:
step 1. 初始化btrbulk
btrbulk btr_bulk(sort_idx, trx->id);
btr_bulk.init(); 初始化m_page_bulks,该vector存储的是pagebulk对象
step 2.随后调用row_merge_insert_index_tuples进入元组插入阶段。
遍历元组,对于每条记录,进行转换后,调用 btr_bulk->insert(dtuple)->insert(tuple, 0)插入
step 3.完成后调用btr_bulk.finish(error)完成插入操作。
显然我们的重点集中在btr_bulk->insert的逻辑上。
2.自底向上的索引记录插入
入口函数: btrbulk::insert
画了个流程图来便于理解这个过程。

step 1. 当前btree level没有对应的pagebulk,则创建,初始化,并加入到m_page_bulks中。
pagebulk::init() 会开启一个mtr (不记录redo log,mtr_log_no_redo),分配新的page(如果需要的话)
另外还需要计算page上保留的空闲空间数,用于索引完成后的dml操作,由新参数innodb_fill_factor控制。
m_reserved_space =
univ_page_size * (100 – innobase_fill_factor) / 100;
step 2. 当前插入的tuple是非叶子节点的最左节点的最小记录,设置tuple标记rec_info_min_rec_flag
step 3.判断是否需要外部存储,如果需要,转换记录格式(dtuple_convert_big_rec)
page_bulk->needext(tuple, rec_size)。
step 4. 检查当前page的空间是否足够插入记录(pagebulk::isspaceavailable)
如果pagebulk::isspaceavailable返回false,表示page空间不足,需要
#创建一个兄弟节点,及其对应的pagebulk,并进行初始化,分配新page
#提交当前pagebulk:err = pagecommit(page_bulk, sibling_page_bulk, true)
…将当前page指向下一个page;
…压缩表需要特殊处理,先进行压缩(pagebulk::compress()),如果压缩失败,则进行page分裂(btrbulk::pagesplit), 在pagesplit里会先分配一个新的page用于容纳分裂后的数据,简单的说..
page_bulk1 —-> 空间不足
page_bulk1 (page_bulk2)
page_bulk1 —->page_bulk2
压缩page_bulk1—>failed—->split(page_bulk1, page_bulk3) —>(page_bulk1—>page_bulk3—>page_bulk2)
也就是说,这里可能产生btrbulk::pagecommit的递归调用。
…构建父亲节点记录,并插入到父节点。
dtuple_t* node_ptr = page_bulk->getnodeptr();
dberr_t err = insert(node_ptr, page_bulk->getlevel()+1);
这里会递归调用btrbulk::insert函数来完成自下而上的btree节点构建
#如果是叶子节点,还需要唤醒page cleaner线程,做必要的log_free_check()
step 5.在完成上述必要的检查后,将tuple转会为rec,并插入到page中(pagebulk::insert)
page_bulk->insert(rec, offsets)
step 6.处理外部存储记录(pagebulk::storeext)
参考:
worklog
<a href="http://dev.mysql.com/worklog/task/?id=7277">http://dev.mysql.com/worklog/task/?id=7277</a>
对应补丁:
<a href="http://bazaar.launchpad.net/~mysql/mysql-server/5.7/revision/8357">http://bazaar.launchpad.net/~mysql/mysql-server/5.7/revision/8357</a>
官方文档:
<a href="http://dev.mysql.com/doc/refman/5.7/en/create-index-bulk-load.html">http://dev.mysql.com/doc/refman/5.7/en/create-index-bulk-load.html</a>