1. 程式人生 > >BTree和B+Tree和Hash索引詳解

BTree和B+Tree和Hash索引詳解

b-tree 關系 查詢優化 刪除節點 eight node 常用 技術分享 遍歷

二叉查找樹

二叉樹具有以下性質:左子樹的鍵值小於根的鍵值,右子樹的鍵值大於根的鍵值。

如下圖所示就是一棵二叉查找樹,

技術分享圖片

對該二叉樹的節點進行查找發現深度為1的節點的查找次數為1,深度為2的查找次數為2,深度為n的節點的查找次數為n,因此其平均查找次數為 (1+2+2+3+3+3) / 6 = 2.3次

二叉查找樹可以任意地構造,同樣是2,3,5,6,7,8這六個數字,也可以按照下圖的方式來構造:

技術分享圖片

但是這棵二叉樹的查詢效率就低了。因此若想二叉樹的查詢效率盡可能高,需要這棵二叉樹是平衡的,從而引出新的定義——平衡二叉樹,或稱AVL樹。

平衡二叉樹(AVL Tree)

平衡二叉樹(AVL樹)在符合二叉查找樹的條件下,還滿足任何節點的兩個子樹的高度最大差為1。下面的兩張圖片,左邊是AVL樹,它的任何節點的兩個子樹的高度差<=1;右邊的不是AVL樹,其根節點的左子樹高度為3,而右子樹高度為1;

技術分享圖片

如果在AVL樹中進行插入或刪除節點,可能導致AVL樹失去平衡,這種失去平衡的二叉樹可以概括為四種姿態:LL(左左)、LR(左右)、RL(右左)、RR(右右)。它們的示意圖如下:

技術分享圖片

這四種失去平衡的姿態都有各自的定義:

LL:LeftLeft,也稱“左左”。插入或刪除一個節點後,根節點的左孩子(Left Child)的左孩子(Left Child)還有非空節點,導致根節點的左子樹高度比右子樹高度高2,AVL樹失去平衡。

RR:RightRight,也稱“右右”。插入或刪除一個節點後,根節點的右孩子(Right Child)的右孩子(Right Child)還有非空節點,導致根節點的右子樹高度比左子樹高度高2,AVL樹失去平衡。

LR:LeftRight,也稱“左右”。插入或刪除一個節點後,根節點的左孩子(Left Child)的右孩子(Right Child)還有非空節點,導致根節點的左子樹高度比右子樹高度高2,AVL樹失去平衡。

RL:RightLeft,也稱“右左”。插入或刪除一個節點後,根節點的右孩子(Right Child)的左孩子(Left Child)還有非空節點,導致根節點的右子樹高度比左子樹高度高2,AVL樹失去平衡。

AVL樹失去平衡之後,可以通過旋轉使其恢復平衡。下面分別介紹四種失去平衡的情況下對應的旋轉方法。

LL的旋轉。LL失去平衡的情況下,可以通過一次旋轉讓AVL樹恢復平衡。步驟如下:

1. 將根節點的左孩子作為新根節點。

2. 將新根節點的右孩子作為原根節點的左孩子。

3. 將原根節點作為新根節點的右孩子。

LL旋轉示意圖如下:

技術分享圖片

RR的旋轉:RR失去平衡的情況下,旋轉方法與LL旋轉對稱,步驟如下:

1. 將根節點的右孩子作為新根節點。

2. 將新根節點的左孩子作為原根節點的右孩子。

3. 將原根節點作為新根節點的左孩子。

RR旋轉示意圖如下:

技術分享圖片

LR的旋轉:LR失去平衡的情況下,需要進行兩次旋轉,步驟如下:

1. 圍繞根節點的左孩子進行RR旋轉。

2. 圍繞根節點進行LL旋轉。

LR的旋轉示意圖如下:

技術分享圖片

RL的旋轉:RL失去平衡的情況下也需要進行兩次旋轉,旋轉方法與LR旋轉對稱,步驟如下:

1. 圍繞根節點的右孩子進行LL旋轉。

2. 圍繞根節點進行RR旋轉。

RL的旋轉示意圖如下:

技術分享圖片

BTree特性

InnoDB存儲引擎中默認每個頁的大小為16KB,InnoDB在把磁盤數據讀入到磁盤時會以頁為基本單位,B-Tree結構的數據可以讓系統高效的找到數據所在的磁盤塊。為了描述B-Tree,首先定義一條記錄為一個二元組[key, data] ,key為記錄的鍵值,對應表中的主鍵值,data為一行記錄中除主鍵外的數據。對於不同的記錄,key值互不相同。

一個 m 階的B樹滿足以下條件:

1. 每個結點至多擁有m棵子樹;

2. 根結點至少擁有兩顆子樹(存在子樹的情況下);

3. 除了根結點以外,其余每個分支結點至少擁有 m/2 棵子樹;

4. 所有的葉結點都在同一層上,到達任何一個葉結點最短路徑的長度都是相同的;

5. 有 n 棵子樹的分支結點則存在 n-1 個關鍵字,關鍵字按照遞增次序進行排列;

6. 每個非終端節點包含n個關鍵字信息(P0,P1,…Pn, k1,…kn)

7. 關鍵字數量需要滿足ceil(m/2)-1 <= n <= m-1;

8. ki(i=1,…n)為關鍵字,且關鍵字升序排序。

9. Pi(i=1,…n)為指向子樹根節點的指針。P(i-1)指向的子樹的所有節點關鍵字均小於ki,但都大於k(i-1)

技術分享圖片

模擬查找關鍵字29的過程:

1. 根據根節點找到磁盤塊1,讀入內存。【磁盤I/O操作第1次】

2. 比較關鍵字29在區間(17,35),找到磁盤塊1的指針P2。

3. 根據P2指針找到磁盤塊3,讀入內存。【磁盤I/O操作第2次】

4. 比較關鍵字29在區間(26,30),找到磁盤塊3的指針P2。

5. 根據P2指針找到磁盤塊8,讀入內存。【磁盤I/O操作第3次】

6. 在磁盤塊8中的關鍵字列表中找到關鍵字29。

分析上面過程,發現需要3次磁盤I/O操作,和3次內存查找操作。由於內存中的關鍵字是一個有序表結構,可以利用二分法查找提高效率。而3次磁盤I/O操作是影響整個B-Tree查找效率的決定因素。B-Tree相對於AVLTree縮減了節點個數,使每次磁盤I/O取到內存的數據都發揮了作用,從而提高了查詢效率。

B樹上大部分的操作(插入、刪除、查詢)所需要的磁盤存取次數和B樹的高度是成正比的,並且B樹是盡量多的在節點上存儲信息,保證導數盡量少,在B樹中可以檢查多個子結點,由於在一棵樹中檢查任意一個結點都需要一次磁盤訪問,所以B樹避免了大量的磁盤訪問,減少了磁盤I/O

BTree操作模擬網站

BTree操作過程

插入

新結點一般插在第h層,通過搜索找到對應的結點進行插入,那麽根據即將插入的結點的數量又分為下面幾種情況。

1. 如果該結點的關鍵字個數沒有到達m-1個,那麽直接插入即可;

2. 如果該結點的關鍵字個數已經到達了m-1個,那麽根據B樹的性質顯然無法滿足,需要將其進行分裂。分裂的規則是該結點分成兩半,將中間的關鍵字進行提升,加入到父親結點中,但是這又可能存在父親結點也滿員的情況,則不得不向上進行回溯,甚至是要對根結點進行分裂,那麽整棵樹都加了一層。

其過程如下:

技術分享圖片

技術分享圖片

技術分享圖片

技術分享圖片

刪除

同樣的,我們需要先通過搜索找到相應的值,存在則進行刪除,需要考慮刪除以後的情況,

1. 如果該結點擁有關鍵字數量仍然滿足B樹性質,則不做任何處理;

2. 如果該結點在刪除關鍵字以後不滿足B樹的性質(關鍵字沒有到達ceil(m/2)-1的數量),則需要向兄弟結點借關鍵字,這有分為兄弟結點的關鍵字數量是否足夠的情況。

1) 如果兄弟結點的關鍵字足夠借給該結點,則過程為將父親結點的關鍵字下移,兄弟結點的關鍵字上移;

2) 如果兄弟結點的關鍵字在借出去以後也無法滿足情況,即之前兄弟結點的關鍵字的數量為ceil(m/2)-1,借的一方的關鍵字數量為ceil(m/2)-2的情況,那麽我們可以將該結點合並到兄弟結點中,合並之後的子結點數量少了一個,則需要將父親結點的關鍵字下放,如果父親結點不滿足性質,則向上回溯;

其余情況參照BST中的刪除。

其過程如下:

技術分享圖片

技術分享圖片

技術分享圖片

B+Tree特性

B+Tree是在B-Tree基礎上的一種優化,使其更適合實現外存儲索引結構,InnoDB存儲引擎就是用B+Tree實現其索引結構。

從上一節中的B-Tree結構圖中可以看到每個節點中不僅包含數據的key值,還有data值。而每一個頁的存儲空間是有限的,如果data數據較大時將會導致每個節點(即一個頁)能存儲的key的數量很小,當存儲的數據量很大時同樣會導致B-Tree的深度較大,增大查詢時的磁盤I/O次數,進而影響查詢效率。在B+Tree中,所有數據記錄節點都是按照鍵值大小順序存放在同一層的葉子節點上,而非葉子節點上只存儲key值信息,這樣可以大大加大每個節點存儲的key值數量,降低B+Tree的高度

以一個m階樹為例:

1. 根結點只有一個,分支數量範圍為[2,m];

2. 分支結點,每個結點包含分支數範圍為[ceil(m/2), m];

3. 所有非葉子節點的關鍵字數目等於它的分支數量,關鍵字順序遞增【此處有爭議:或者等於分支數量-1】;

4. 所有葉子結點都在同一層,且關鍵字數目範圍是[ceil(m/2),m]

5. 所有非葉子節點的關鍵字可以看成是索引部分,這些索引等於其子樹(根結點)中的最大(或最小)關鍵字【此處有爭議】。

例如一個非葉子節點包含信息: (n,A0,K0, A1,K1,……,Kn,An),其中Ki為關鍵字,Ai為指向子樹根結點的指針,n表示關鍵字個數。即Ai所指子樹中的關鍵字均小於或等於Ki,而Ai+1所指的關鍵字均大於Ki(i=1,2,……,n)。

6. 所有葉子節點之間都有一個鏈指針,指向相鄰的後一個葉子結點的指針信息,主要是為了加快檢索多個相鄰葉子結點的效率考慮。

技術分享圖片

通常在B+Tree上有兩個頭指針,一個指向根節點,另一個指向關鍵字最小的葉子節點,而且所有葉子節點(即數據節點)之間是一種鏈式環結構。因此可以對B+Tree進行兩種查找運算:一種是對於主鍵的範圍查找和分頁查找,另一種是從根節點開始,進行隨機查找。

可能上面例子中只有22條數據記錄,看不出B+Tree的優點,下面做一個推算:

InnoDB存儲引擎中頁的大小為16KB,一般表的主鍵類型為INT(占用4個字節)或BIGINT(占用8個字節),指針類型也一般為4或8個字節,也就是說一個頁(B+Tree中的一個節點)中大概存儲16KB/(8B+8B)=1K個鍵值(因為是估值,為方便計算,這裏的K取值為〖10〗^3)。也就是說一個深度為3的B+Tree索引可以維護10^3 * 10^3 * 10^3 = 10億 條記錄。

實際情況中每個節點可能不能填充滿,因此在數據庫中,B+Tree的高度一般都在2~4層。mysql的InnoDB存儲引擎在設計時是將根節點常駐內存的,也就是說查找某一鍵值的行記錄時最多只需要1~3次磁盤I/O操作。

其操作和B樹的操作是類似的,不過需要註意的是,在增加值的時候,如果存在滿員的情況,將選擇結點中的值作為新的索引,還有在刪除值的時候,索引中的關鍵字並不會刪除,也不會存在父親結點的關鍵字下沈的情況,因為那只是索引。

B+Tree操作過程

插入

l 例1:

往下圖的3階B+樹中插入關鍵字9

技術分享圖片

首先查找9應插入的葉節點(最左下角的那一個),插入發現沒有破壞B+樹的性質,完畢。插完如下圖所示:

技術分享圖片

l 例2:

往下圖的3階B+樹插入20

技術分享圖片

首先查找20應插入的葉節點(第二個葉子節點),插入,如下圖

技術分享圖片

發現第二個葉子節點已經破壞了B+樹的性質,則把之分解成[20 21], [37 44]兩個,並把21往父節點移,如下圖

技術分享圖片

發現父節點也破壞了B+樹的性質,則把之再分解成[15 21], [44 59]兩個,並把21往其父節點移,如下圖

技術分享圖片

l 例3:

往下圖的3階B+樹插入100

技術分享圖片

首先查找100應插入的葉節點(最後一個節點), 插入,如下圖

技術分享圖片

修改其所有父輩節點的鍵值為100(只有插入比當前樹的最大數大的數時要做此步),如下圖

技術分享圖片

然後重復Eg.2的方法拆分節點,最後得

技術分享圖片

刪除

l 例1:

刪除下圖3階B+樹的關鍵字91

技術分享圖片

首先找到91所在葉節點(最後一個節點),刪除之,如下圖

技術分享圖片

沒有破壞B+樹的性質,刪除完畢

l 例2:

刪除下圖3階B+樹的關鍵字97

技術分享圖片

首先找到97所在葉節點(最後一個節點),刪除之,然後修改該節點的父輩的鍵字為91(只有刪除樹中最大數時要做此步),如下圖

技術分享圖片

l 例3:

刪除下圖3階B+樹的關鍵字51

技術分享圖片

首先找到51所在節點(第三個節點),刪除之,如下圖

技術分享圖片

破壞了B+樹的性質,從該節點的兄弟節點(左邊或右邊)借節點44,並修改相應鍵值,判斷沒有破壞B+樹,完畢,如下圖

技術分享圖片

l 例4:

刪除下圖3階B+樹的關鍵字59

技術分享圖片

首先找到59所在葉節點(第三個節點),刪除之,如下圖

技術分享圖片

破壞B+樹性質,嘗試借節點,無效(因為左兄弟節點被借也會破壞B+樹性質),合並第二第三葉節點並調整鍵值,如下圖

技術分享圖片

l 例5:

刪除下圖3階B+樹的關鍵字63

技術分享圖片

首先找到63所在葉節點(第四個節點),刪除之,如下圖

技術分享圖片

合並第四五葉節點並調整鍵值,如下圖

技術分享圖片

發現第二層的第二個節點不滿足B+樹性質,從第二層的第一個節點借59,並調整鍵值,如下圖

技術分享圖片

完畢

B樹和B+樹的區別

這都是由於B+樹和B具有這不同的存儲結構所造成的區別,以一個m階樹為例。

1. 關鍵字的數量不同;B+樹中分支結點有m個關鍵字,其葉子結點也有m個,其關鍵字只是起到了一個索引的作用,但是B樹雖然也有m個子結點,但是其只擁有m-1個關鍵字。【註:此處有爭議,B+樹到底是與B 樹n-1個關鍵字有n棵子樹保持一致,還是B+樹n個關鍵字的結點中含有n棵子樹;兩種定義都可以,只要自己實現的時候統一用一種就行】。

2. 存儲的位置不同;B+樹中的數據都存儲在葉子結點上,也就是其所有葉子結點的數據組合起來就是完整的數據,但是B樹的數據存儲在每一個結點中,並不僅僅存儲在葉子結點上。而且B+樹葉子結點上還存儲了指向與該結點相鄰的後一個葉子結點的指針信息,這主要是為了加快檢索多個相鄰葉子結點的效率考慮

3. 分支結點的構造不同;分支結點並不存儲真正的信息,僅包含著索引信息,其保存著葉子節點的最小值作為索引及其兒子指針(指的是磁盤塊的偏移量)。【註:此處有爭議,是以最大值還是最小值作為索引看個人實現】。

4. 查詢不同;B樹在找到具體的數值以後,則結束,而B+樹則需要通過索引找到葉子結點中的數據才結束,也就是說B+樹的搜索過程中走了一條從根結點到葉子結點的路徑。

5. 用處不用:由於B+樹的數據都存儲在葉子結點中,分支結點均為索引,方便掃庫,只需要掃一遍葉子結點即可,但是B樹因為其分支結點同樣存儲著數據,我們要找到具體的數據,需要進行一次中序遍歷按序來掃,所以B+樹更加適合在區間查詢的情況,所以通常B+樹用於數據庫索引,而B樹則常用於文件索引

B+樹索引

數據庫中的B+Tree索引可以分為聚集索引(clustered index)和輔助索引(secondary index)。上面的B+Tree示例圖在數據庫中的實現即為聚集索引,聚集索引的B+Tree中的葉子節點存放的是整張表的行記錄數據。輔助索引與聚集索引的區別在於輔助索引的葉子節點並不包含行記錄的全部數據,而是存儲相應行數據的聚集索引鍵,即主鍵。當通過輔助索引來查詢數據時,InnoDB存儲引擎會遍歷輔助索引找到主鍵,然後再通過主鍵在聚集索引中找到完整的行記錄數據。

聚集索引 和 輔助索引區別:葉子節點存放的是否是一整行的信息

聚集索引

聚集索引 ( clustered index ) 按照每張表的主鍵構造一棵 B+樹,同時葉子節點存放的為整張表的行記錄數據,也將聚集索引的葉子節點稱為數據頁。由於實際的數據頁只能按照一棵B+樹進行排序,所以每張表只能擁有一個聚集索引。查詢優化器傾向於采用聚集索引。聚集索引能在B+樹索引的葉節點上直接找到數據,是由於定義了數據的邏輯順序。聚集索引適用於針對範圍值的查詢。

優點:對於主鍵排序查找和範圍查找速度非常快。

輔助索引 ( 非聚集索引 )

輔助索 ( secondary index ) ,葉子節點並不包含行記錄的全部數據。葉子節點除了包含鍵值以外,每個葉子節點中的索引行中還包含了一個書簽 ( bookmark ) 。 該書簽用來告訴 InnoDB 存儲引擎哪裏可以找到與索引相對應的行數據。每張表上可以有多個輔助索引,通過輔助索引查找數據時, InnoDB 存儲引擎會遍歷輔助索引並通過葉級別的指針獲得指向主鍵索引的主鍵,再通過主鍵索引找到完事的行記錄。

HASH索引

Hash 索引結構的特殊性,其檢索效率非常高,索引的檢索可以一次定位,不像B-Tree 索引需要從根節點到枝節點,最後才能訪問到頁節點這樣多次的IO訪問,所以 Hash 索引的查詢效率要遠高於 B-Tree 索引。雖然 Hash 索引效率高,但是 Hash 索引本身由於其特殊性也帶來了很多限制和弊端,代表數據庫:redis、memcache等

Hash 索引結構的特殊性,其檢索效率非常高,索引的檢索可以一次定位,不像B-Tree索引需要從根節點到枝節點,最後才能訪問到頁節點這樣多次的IO訪問,所以 Hash 索引的查詢效率要遠高於 B-Tree索引。

可能很多人又有疑問了,既然Hash 索引的效率要比 B-Tree 高很多,為什麽大家不都用 Hash 索引而還要使用 B-Tree索引呢?任何事物都是有兩面性的,Hash 索引也一樣,雖然 Hash 索引效率高,但是 Hash索引本身由於其特殊性也帶來了很多限制和弊端,主要有以下這些。

1. Hash索引僅僅能滿足"=","IN"和"<=>"查詢,不能使用範圍查詢。

由於 Hash 索引比較的是進行 Hash 運算之後的 Hash值,所以它只能用於等值的過濾,不能用於基於範圍的過濾,因為經過相應的 Hash算法處理之後的 Hash 值的大小關系,並不能保證和Hash運算前完全一樣。

2. Hash 索引無法被用來避免數據的排序操作。

由於 Hash 索引中存放的是經過 Hash 計算之後的 Hash值,而且Hash值的大小關系並不一定和 Hash運算前的鍵值完全一樣,所以數據庫無法利用索引的數據來避免任何排序運算;

3. Hash索引不能利用部分索引鍵查詢。

對於組合索引,Hash 索引在計算 Hash 值的時候是組合索引鍵合並後再一起計算 Hash 值,而不是單獨計算 Hash值,所以通過組合索引的前面一個或幾個索引鍵進行查詢的時候,Hash 索引也無法被利用。

4. Hash索引在任何時候都不能避免表掃描。

前面已經知道,Hash 索引是將索引鍵通過 Hash 運算之後,將 Hash運算結果的 Hash值和所對應的行指針信息存放於一個 Hash 表中,由於不同索引鍵存在相同 Hash 值,所以即使取滿足某個 Hash 鍵值的數據的記錄條數,也無法從 Hash索引中直接完成查詢,還是要通過訪問表中的實際數據進行相應的比較,並得到相應的結果。

5. Hash索引遇到大量Hash值相等的情況後性能並不一定就會比B-Tree索引高。

選擇性比較低的索引鍵,如果創建 Hash 索引,那麽將會存在大量記錄指針信息存於同一個Hash值相關聯。這樣要定位某一條記錄時就會非常麻煩,會浪費多次表數據的訪問,而造成整體性能低下

Innodb特性(也就是使用了B+樹索引,B+樹索引分兩種,上面有介紹)

InnoDB存儲引擎中默認每個頁的大小為16KB,InnoDB在把磁盤數據讀入到磁盤時會以頁為基本單位,B-Tree結構的數據可以讓系統高效的找到數據所在的磁盤塊。

Innodb 存儲引擎中兩種不同形式的索引

一種是 Cluster 形式的主鍵索引(Primary Key),另外一種則是和其他存儲引擎(如 MyISAM 存儲引擎)存放形式基本相同的普通 B-Tree 索引,這種索引在 Innodb存儲引擎中被稱為 Secondary Index。下面我們通過圖示來針對這兩種索引的存放形式做一個比較。

技術分享圖片

圖示中左邊為 Clustered 形式存放的 Primary Key ,右側則為普通的 B-Tree 索引。兩種 Root Node和 Branch Nodes 方面都還是完全一樣的。而 Leaf Nodes 就出現差異了。在 Prim中, Leaf Nodes存放的是表的實際數據,不僅僅包括主鍵字段的數據,還包括其他字段的數據據以主鍵值有序的排列。而 Secondary Index則和其他普通的 B-Tree 索引沒有太大的差異,Leaf Nodes 出了存放索引鍵的相關信息外,還存放了Innodb的主鍵值。

所以,在 Innodb 中如果通過主鍵來訪問數據效率是非常高的,而如果是通過 Secondary Index 來訪問數據的話,Innodb 首先通過 Secondary Index 的相關信息,通過相應的索引鍵檢索到 Leaf Node之後,需要再通過Leaf Node 中存放的主鍵值再通過主鍵索引來獲取相應的數據行。MyISAM存儲引擎的主鍵索引和非主鍵索引差別很小,只不過是主鍵索引的索引鍵是一個唯一且非空 的鍵而已。而且 MyISAM 存儲引擎的索引和Innodb 的 Secondary Index 的存儲結構也基本相同,主要的區別只是 MyISAM 存儲引擎在 Leaf Nodes上面出了存放索引鍵信息之外,再存放能直接定位到 MyISAM 數據文件中相應的數據行的信息(如 Row Number),但並不會存放主鍵的鍵值信息

BTree和B+Tree和Hash索引詳解