天天看點

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>