文中附圖參考至《PostgreSQL數據庫內核分析》
(一)概念描述
B+樹是一種索引數據結構,其一個特征在於非葉子節點用於描述索引,而葉子節點指向具體的數據存儲位置。在PostgreSQL中,存在結構相似的BTree索引,該數據結構最先引用於《Effiicient Locking for Concurrent Operations on B-Trees》論文,一個新特征在於,引入了“High Key”(下述HK)用於描述當前節點子節點的最大值。如下圖所示:
其中K1代表一個HK,其值等於P0及P0子節點的最大值,對於上述存在的2n個節點,每個節點都存在一個指針指向右兄弟節點,Pi的子節點取值范圍為(Ki-1,Ki]
(二)PostgreSQL的BTree索引結構
在PostgreSQL中,普通表的表文件組織結構如下圖
其中Linp結構用來指向文件塊中的一個元組。Freespace是未分配的空閑空間,對於新插入頁面的元組及其對應的Linp元素從該空間進行分配,分配方式是Linp元素從Freespace的頭部分配,tuple從尾部分配。而PostgreSQL的索引結構,也是按照上述頁面結構進行存儲的。如下圖:
itup是排好序的索引元組,對於其如何完成排序將在之后的代碼分析中進行介紹。linp用於索引itup,其存儲了每個itup在頁面中的實際位置。根據PostgreSQL中對BTree索引結構的描述,分為當前節點是否是最右節點兩種類型。由於非最右節點需要一個字段來保存HK,故當對一個頁面進行填充時,存在着以下兩種方式:
(1)當前節點為非最右節點
a.首先將itup3(最大的索引元組)復制到當前節點的右兄弟節點,然后將linp0指向itup3(HK)
b.去掉linp3。使用linp0來指向頁面中的HK。
(2)當前節點為最右節點
對於最右節點,其並不需要HK,故將每個linp遞減一個位置,linp3不再使用。
基於上述,PostgreSQL所實現的BTree索引組織結構如下圖:
總結上圖:
(1)對於非葉子節點,itup指向下一個節點,而對於葉子節點,itup指向實際物理存儲的位置。
(2)Special space中,實現了兩個指針,分配用於指向左右兄弟節點。
(3)根據BTree的特性,索引元組為有序,第一個葉子節點中itup3實際為最大索引元組,即HK,第二個葉子節點中itup1實際為最小索引元組,兩者相同,故指向了同一物理存儲位置。
(三)源碼分析
- btbuild
索引創建的入口函數- BTBuildState buildstate
定義並初始化buildstate結構。用於保存索引元組 - IndexBuildHeapScan
掃描表元組,並將其封裝為索引元組。函數返回構建好的索引元組個數,返回函數指針buildstate- while(... != NULL)
依次遍歷基表的所有元組
- while(... != NULL)
- _bt_leafbuild
將buildstate中得到的索引元組構建為索引結構- BTWriteState wstate
定義並初始化該結構。用於保存整個索引創建過程的信息。 - tuplesort_performsort
對索引元組進行排序- qsort_ssup or qsort_tuple
在內存中對索引元組進行排序。
- qsort_ssup or qsort_tuple
- _bt_load
對已排好序的索引元組,順序讀出將其插入到btree索引結構中- BTPageState *state
定義並初始化該結構,在btree中,每一層僅有一個BTPageState結構,記錄每層節點的信息 - if (merge)
如果spool2不為空,即if條件成立,則將spool與spool2進行歸並排序 - else
spool2為空,則索引元組都存放在spool結構中- while(依次取出spool中的每個索引元組)
- _bt_buildadd
將每個索引元組添加到索引結構- if(頁面已滿)
- Page opage = npage, npage = _bt_blnewpage()
設置舊頁面為當前頁面,並重新分配新頁面作為右兄弟節點 - _bt_buildadd
這里比較巧妙的利用遞歸,從當前已分配的頁面開始,完成后續索引數組的插入 - _bt_blwritepage
將舊頁面的信息寫入索引文件。流程到這里,肯定是遞歸函數已返回,由於舊頁面已完成填充,不會再進行修改,則將其寫入到索引文件中
- Page opage = npage, npage = _bt_blnewpage()
- 頁面未滿
- _bt_sortaddup
將當前索引元組插入到頁面中 - state->btps_page = npage
設置當前頁面 - state->btps_blkno = nblko
設置當前磁盤塊 - state->btps_lastoff = last_off
設置當前頁面內偏移位置
- _bt_sortaddup
- if(頁面已滿)
- _bt_uppershutdown
構建每層節點的最右節點與父節點的鏈接關系- _bt_initmetapage
在最右節點與父節點關系構建完成后,定義元頁,每個btree索引結構由一個元頁結構記錄信息
- _bt_initmetapage
- BTPageState *state
- BTWriteState wstate
- BTBuildState buildstate
- btinsert
在已建立的索引基礎上,插入一個新元素- index_form_tuple
將表元組首先封裝為索引元組 - _bt_doinsert
將索引元組插入到索引- _bt_mkscankey
計算元組的掃描鍵值scan_key - _bt_search
查找包含索引元組的頁面- _bt_getroot
獲取索引結構的根節點 - for()
- _bt_moveright
並發性考慮 - if (當前節點為葉子節點)
跳出循環 - _bt_binsrch
不為葉子節點,在當前頁面找到合適的元組itup - new_stack
分配新的BTStack結構,將當前頁面信息入棧
- _bt_moveright
- _bt_getroot
- if (唯一索引)
進行唯一性檢查 - _bt_findinsertloc
在當前頁面查找索引元素合適的插入位置 - _bt_insertonpg
插入索引元組 - if (當前頁面沒有足夠的剩余空間)
- _bt_findsplitloc
遍歷當前頁面節點,查找最佳分裂點 - _bt_split
查找到該分裂點,對其進行分裂 - _bt_insert_parent
把新節點信息插入到父節點中
- _bt_findsplitloc
- else(當前頁面有存夠的剩余空間)
直接插入節點
- _bt_mkscankey
- index_form_tuple
總結:上述給出了關於btree構建與在已構建的btree中插入新元素時的函數實現流程。實現邏輯思想參考(一)(二)。其中對於函數_bt_moveright,其用於解決並發訪問下的問題,如當前所操作頁面正好是另一事務操作被分裂的頁面,則在當前頁面返回所得結果后,需要查找其右兄弟頁面,來返回所得的正確結果。