天天看点

MySQL 5.7 : 索引创建优化(Bulk Load)

先来看一组测试数据

使用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

画了个流程图来便于理解这个过程。

MySQL 5.7 : 索引创建优化(Bulk Load)

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>