本文重點介紹下索引的儲存模型

二分查詢

  給定一個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有幾個特點:

  1. 它的關鍵字的數量是跟路數相等的;
  2. B+Tree的根節點和枝節點中都不會儲存資料,只有葉子節點才儲存資料。InnoDB 中 B+ 樹深度一般為 1-3 層,它就能滿足千萬級的資料儲存。搜尋到關鍵字不會直接返回,會到最後一層的葉子節點。比如我們搜尋id=28,雖然在第一層直接命中了,但是全部的資料在葉子節點上面,所以我還要繼續往下搜尋,一直到葉子節點。
  3. B+Tree的每個葉子節點增加了一個指向相鄰葉子節點的指標,它的最後一個數據會指向下一個葉子節點的第一個資料,形成了一個有序連結串列的結構。

總結一下, B+Tree的特點帶來的優勢:

  1. 它是B Tree的變種,B Tree能解決的問題,它都能解決。B Tree解決的兩大問題是什麼?(每個節點儲存更多關鍵字;路數更多)
  2. 掃庫、掃表能力更強(如果我們要對錶進行全表掃描,只需要遍歷葉子節點就可以了,不需要遍歷整棵B+Tree拿到所有的資料)
  3. B+Tree的磁碟讀寫能力相對於B Tree來說更強(根節點和枝節點不儲存資料區,所以一個節點可以儲存更多的關鍵字,一次磁碟載入的關鍵字更多)
  4. 排序能力更強(因為葉子節點上有下一個資料區的指標,資料形成了連結串列)
  5. 效率更加穩定(B+Tree永遠是在葉子節點拿到資料,所以IO次數是穩定的)