本文重點介紹下索引的存儲模型
二分查找
給定一個1~100的自然數,給你5次機會,你能猜中這個數字嗎?
你會從多少開始猜?
為什么一定是50呢?這個就是二分查找的一種思想,也叫折半查找,每一次,我們都把候選數據縮小了一半。如果數據已經排過序的話,這種方式效率比較高。
所以第一個,既然索引是有序的,我們可以考慮用有序數組作為索引的數據結構。
有序數組的等值查詢和比較查詢效率非常高,但是更新數據的時候會出現一個問題,可能要挪動大量的數據(改變index),所以只適合存儲靜態的數據。
為了支持頻繁的修改,比如插入數據,我們需要采用鏈表。鏈表的話,如果是單鏈表,它的查找效率還是不夠高。
所以,有沒有可以使用二分查找的鏈表呢?
為了解決這個問題,BST(Binary [ˈbaɪnəri] Search Tree)也就是我們所說的二叉查找樹誕生了。
二叉查找樹
BST Binary Search Tree 二叉查找樹的特點:左子樹所有的節點都小於父節點,右子樹所有的節點都大於父節點。投影到平面以后,就是一個有序的線性表。
二叉查找樹既能夠實現快速查找,又能夠實現快速插入。
但是二叉查找樹有一個問題:查找耗時是和這棵樹的深度相關的,在最壞的情況下時間復雜度會退化成O(n)。
什么情況是最壞的情況呢?
https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
還是剛才的這一批數字,如果我們插入的數據剛好是有序的,2、6、11、13、17、22。
這個時候BST會變成鏈表( “斜樹”),這種情況下不能達到加快檢索速度的目的,和順序查找效率是沒有區別的。
造成它傾斜的原因是什么呢?
因為左右子樹深度差太大,這棵樹的左子樹根本沒有節點——也就是它不夠平衡。
所以,我們有沒有左右子樹深度相差不是那么大,更加平衡的樹呢?
這個就是平衡二叉樹,叫做Balanced binary search trees,或者AVL樹(AVL是發明這個數據結構的人的名字)。
平衡二叉樹
AVL Trees (Balanced binary search trees)
平衡二叉樹的定義:左右子樹深度差絕對值不能超過1。
是什么意思呢?比如左子樹的深度是2,右子樹的深度只能是1或者3。
這個時候我們再按順序插入1、2、3、4、5、6,一定是這樣,不會變成一棵“斜樹”。
那AVL樹的平衡是怎么做到的呢?怎么保證左右子樹的深度差不能超過1呢?
https://www.cs.usfca.edu/~galles/visualization/AVLtree.html
插入1、2、3。
當我們插入了1、2之后,如果按照二叉查找樹的定義,3肯定是要在2的右邊的,這個時候根節點1的右節點深度會變成2,但是左節點的深度是0,因為它沒有子節點,所以就會違反平衡二叉樹的定義。
那應該怎么辦呢?因為它是右節點下面接一個右節點,右-右型,所以這個時候我們要把2提上去,這個操作叫做左旋。
同樣的,如果我們插入7、6、5,這個時候會變成左左型,就會發生右旋操作,把6提上去。
所以為了保持平衡,AVL樹在插入和更新數據的時候執行了一系列的計算和調整的操作。
平衡的問題我們解決了,那么平衡二叉樹作為索引怎么查詢數據?
在平衡二叉樹中,一個節點,它的大小是一個固定的單位,作為索引應該存儲什么內容?
它應該存儲三塊的內容:
第一個是索引的鍵值。比如我們在id上面創建了一個索引,我在用where id =1的條件查詢的時候就會找到索引里面的id的這個鍵值。
第二個是數據的磁盤地址,因為索引的作用就是去查找數據的存放的地址。
第三個,因為是二叉樹,它必須還要有左子節點和右子節點的引用,這樣我們才能找到下一個節點。比如大於26的時候,走右邊,到下一個樹的節點,繼續判斷。
當我們用樹的結構來存儲索引的時候,因為拿到一塊數據就要在Server層比較是不是需要的數據,如果不是的話就要決定走左子樹還是右子樹,再讀一一個節點。訪問一個樹的節點就是一次磁盤的I/O操作。
因為InnoDB操作磁盤的最小的單位是一頁(或者叫一個磁盤塊),page的默認大小是16KB(16384字節)。那么,讀取一個樹的節點就是讀取16KB的大小。
如果我們一個節點只存一個鍵值+數據+引用,例如整形的字段,可能只用了十幾個或者幾十個字節,它遠遠達不到16384個字節的容量。所以訪問一個樹節點,進行一次I/O的時候,浪費了大量的空間。
所以如果每個節點存儲的數據太少,從索引中找到我們需要的數據,就要訪問更多的節點,意味着跟磁盤交互次數就會過多。
如果是機械硬盤時代,每次從磁盤讀取數據需要10ms左右的尋址時間,交互次數越多,消耗的時間就越多。
比如上面這張圖,我們一張表里面有6條數據,當我們查詢id=37的時候,要查詢兩個子節點,就需要跟磁盤交互3次,如果我們有幾百萬的數據呢?這個時間更加難以估計。
所以我們的解決方案是什么呢?
第一個就是讓每個節點存儲更多的數據,充分利用16KB的大小,這樣讀取一個節點就能對比更多數據,較少對比次數。
第二個,節點上的關鍵字的數量越多,我們的指針數也越多,也就是意味着可以有更多的分叉(我們把它叫做“路數”)。
因為分叉數越多,樹的深度
就會減少(根節點是0)。
這樣,我們的樹是不是從原來的高瘦高瘦的樣子,變成了矮胖矮胖的樣子?
這個時候,我們的樹就不再是二叉了,而是多叉,或者叫做多路。
多路平衡查找樹
(Balanced Tree)
這個就是我們的多路平衡查找樹,叫做B Tree(B代表平衡)。
跟AVL樹一樣,B樹在枝節點和葉子節點存儲鍵值、數據地址、節點引用。
它有一個特點:分叉數(路數)永遠比關鍵字數多1。比如我們畫的這棵樹,每個節點存儲兩個關鍵字,那么就會有三個指針指向三個子節點。
B Tree的查找規則是什么樣的呢?
比如我們要在這張表里面查找15。
因為15小於17,走左邊。
因為15大於12,走右邊。
在磁盤塊7里面就找到了15,只用了3次IO。
這個是不是比AVL 樹效率更高呢?
那B Tree又是怎么實現一個節點存儲多個關鍵字,還保持平衡的呢?跟AVL樹有什么區別?
https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
比如Max Degree(路數)是3的時候,我們插入數據1、2、3,在插入3的時候,本來應該在第一個磁盤塊,但是如果一個節點有三個關鍵字的時候,意味着有4個指針,子節點會變成4路,所以這個時候必須進行分裂(其實就是B+Tree)。把中間的數據2提上去,把1和3變成2的子節點。
如果刪除節點,會有相反的合並的操作。
注意這里是分裂和合並,跟AVL樹的左旋和右旋是不一樣的。
我們繼續插入4和5,B Tree又會出現分裂和合並的操作。
從這個里面我們也能看到,在更新索引的時候會有大量的索引的結構的調整,所以解釋了為什么我們不要在頻繁更新的列上建索引,或者為什么不要更新主鍵。
節點的分裂和合並,其實就是InnoDB頁(page)的分裂和合並。
B+樹
加強版多路平衡查找樹
因為B Tree的這種特性非常適合用於做索引的數據結構,所以很多文件系統和數據庫的索引都是基於B Tree的。
但是實際上,MySQL里面使用的是B Tree的改良版本,叫做B+Tree(加強版多路平衡查找樹)。
B+樹的存儲結構:
MySQL中的B+Tree有幾個特點:
- 它的關鍵字的數量是跟路數相等的;
- B+Tree的根節點和枝節點中都不會存儲數據,只有葉子節點才存儲數據。InnoDB 中 B+ 樹深度一般為 1-3 層,它就能滿足千萬級的數據存儲。搜索到關鍵字不會直接返回,會到最后一層的葉子節點。比如我們搜索id=28,雖然在第一層直接命中了,但是全部的數據在葉子節點上面,所以我還要繼續往下搜索,一直到葉子節點。
- B+Tree的每個葉子節點增加了一個指向相鄰葉子節點的指針,它的最后一個數據會指向下一個葉子節點的第一個數據,形成了一個有序鏈表的結構。
總結一下, B+Tree的特點帶來的優勢:
- 它是B Tree的變種,B Tree能解決的問題,它都能解決。B Tree解決的兩大問題是什么?(每個節點存儲更多關鍵字;路數更多)
- 掃庫、掃表能力更強(如果我們要對表進行全表掃描,只需要遍歷葉子節點就可以了,不需要遍歷整棵B+Tree拿到所有的數據)
- B+Tree的磁盤讀寫能力相對於B Tree來說更強(根節點和枝節點不保存數據區,所以一個節點可以保存更多的關鍵字,一次磁盤加載的關鍵字更多)
- 排序能力更強(因為葉子節點上有下一個數據區的指針,數據形成了鏈表)
- 效率更加穩定(B+Tree永遠是在葉子節點拿到數據,所以IO次數是穩定的)