先來看一組測試資料
使用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>