索引的原理:我們爲什麼用B+樹來做索引?

索引的作用,是否需要建立索引,以及建立什麼樣的索引,需要我們根據實際情況進行選擇。我之前說過,索引其實就是一種數據結構,那麼今天我們就來看下,索引的數據結構究竟是怎樣的?對索引底層的數據結構有了更深入的瞭解後,就會更瞭解索引的使用原則。

今天的文章內容主要包括下面幾個部分:

  1. 爲什麼索引要存放到硬盤上?如何評價索引的數據結構設計的好壞?
  2. 使用平衡二叉樹作爲索引的數據結構有哪些不足?
  3. B 樹和 B+ 樹的結構是怎樣的?爲什麼我們常用 B+ 樹作爲索引的數據結構?

如何評價索引的數據結構設計好壞

數據庫服務器有兩種存儲介質,分別爲硬盤和內存。內存屬於臨時存儲,容量有限,而且當發生意外時(比如斷電或者發生故障重啓)會造成數據丟失;硬盤相當於永久存儲介質,這也是爲什麼我們需要把數據保存到硬盤上。

雖然內存的讀取速度很快,但我們還是需要將索引存放到硬盤上,這樣的話,當我們在硬盤上進行查詢時,也就產生了硬盤的 I/O 操作。相比於內存的存取來說,硬盤的 I/O 存取消耗的時間要高很多。我們通過索引來查找某行數據的時候,需要計算產生的磁盤 I/O 次數,當磁盤 I/O 次數越多,所消耗的時間也就越大。如果我們能讓索引的數據結構儘量減少硬盤的 I/O 操作,所消耗的時間也就越小。

二叉樹的侷限性

二分查找法是一種高效的數據檢索方式,時間複雜度爲 O(log2n),是不是採用二叉樹就適合作爲索引的數據結構呢?

我們先來看下最基礎的二叉搜索樹(Binary Search Tree),搜索某個節點和插入節點的規則一樣,我們假設搜索插入的數值爲 key:

  1. 如果 key 大於根節點,則在右子樹中進行查找;
  2. 如果 key 小於根節點,則在左子樹中進行查找;
  3. 如果 key 等於根節點,也就是找到了這個節點,返回根節點即可。

舉個例子,我們對數列(34,22,89,5,23,77,91)創造出來的二分查找樹如下圖所示:


但是存在特殊的情況,就是有時候二叉樹的深度非常大。比如我們給出的數據順序是 (5, 22, 23, 34, 77, 89, 91),創造出來的二分搜索樹如下圖所示:


你能看出來第一個樹的深度是 3,也就是說最多隻需 3 次比較,就可以找到節點,而第二個樹的深度是 7,最多需要 7 次比較才能找到節點。

第二棵樹也屬於二分查找樹,但是性能上已經退化成了一條鏈表,查找數據的時間複雜度變成了 O(n)。爲了解決這個問題,人們提出了平衡二叉搜索樹(AVL 樹),它在二分搜索樹的基礎上增加了約束,每個節點的左子樹和右子樹的高度差不能超過 1,也就是說節點的左子樹和右子樹仍然爲平衡二叉樹。

這裏說一下,常見的平衡二叉樹有很多種,包括了平衡二叉搜索樹、紅黑樹、數堆、伸展樹。平衡二叉搜索樹是最早提出來的自平衡二叉搜索樹,當我們提到平衡二叉樹時一般指的就是平衡二叉搜索樹。事實上,第一棵樹就屬於平衡二叉搜索樹,搜索時間複雜度就是 O(log2n)。

我剛纔提到過,數據查詢的時間主要依賴於磁盤 I/O 的次數,如果我們採用二叉樹的形式,即使通過平衡二叉搜索樹進行了改進,樹的深度也是 O(log2n),當 n 比較大時,深度也是比較高的,比如下圖的情況:


每訪問一次節點就需要進行一次磁盤 I/O 操作,對於上面的樹來說,我們需要進行 5 次 I/O 操作。雖然平衡二叉樹比較的效率高,但是樹的深度也同樣高,這就意味着磁盤 I/O 操作次數多,會影響整體數據查詢的效率。

針對同樣的數據,如果我們把二叉樹改成 M 叉樹(M>2)呢?當 M=3 時,同樣的 31 個節點可以由下面的三叉樹來進行存儲:


你能看到此時樹的高度降低了,當數據量 N 大的時候,以及樹的分叉數 M 大的時候,M 叉樹的高度會遠小於二叉樹的高度。

什麼是 B 樹

如果用二叉樹作爲索引的實現結構,會讓樹變得很高,增加硬盤的 I/O 次數,影響數據查詢的時間。因此一個節點就不能只有 2 個子節點,而應該允許有 M 個子節點 (M>2)。

B 樹的出現就是爲了解決這個問題,B 樹的英文是 Balance Tree,也就是平衡的多路搜索樹,它的高度遠小於平衡二叉樹的高度。在文件系統和數據庫系統中的索引結構經常採用 B 樹來實現。

B 樹的結構如下圖所示:


B 樹作爲平衡的多路搜索樹,它的每一個節點最多可以包括 M 個子節點,M 稱爲 B 樹的階。同時你能看到,每個磁盤塊中包括了關鍵字和子節點的指針。如果一個磁盤塊中包括了 x 個關鍵字,那麼指針數就是 x+1。對於一個 100 階的 B 樹來說,如果有 3 層的話最多可以存儲約 100 萬的索引數據。對於大量的索引數據來說,採用 B 樹的結構是非常適合的,因爲樹的高度要遠小於二叉樹的高度。

一個 M 階的 B 樹(M>2)有以下的特性:

  1. 根節點的兒子數的範圍是 [2,M]。
  2. 每個中間節點包含 k-1 個關鍵字和 k 個孩子,孩子的數量 = 關鍵字的數量 +1,k 的取值範圍爲 [ceil(M/2), M]。
  3. 葉子節點包括 k-1 個關鍵字(葉子節點沒有孩子),k 的取值範圍爲 [ceil(M/2), M]。
  4. 假設中間節點節點的關鍵字爲:Key[1], Key[2], …, Key[k-1],且關鍵字按照升序排序,即 Key[i]<Key[i+1]。此時 k-1 個關鍵字相當於劃分了 k 個範圍,也就是對應着 k 個指針,即爲:P[1], P[2], …, P[k],其中 P[1] 指向關鍵字小於 Key[1] 的子樹,P[i] 指向關鍵字屬於 (Key[i-1], Key[i]) 的子樹,P[k] 指向關鍵字大於 Key[k-1] 的子樹。
  5. 所有葉子節點位於同一層。

上面那張圖所表示的 B 樹就是一棵 3 階的 B 樹。我們可以看下磁盤塊 2,裏面的關鍵字爲(8,12),它有 3 個孩子 (3,5),(9,10) 和 (13,15),你能看到 (3,5) 小於 8,(9,10) 在 8 和 12 之間,而 (13,15) 大於 12,剛好符合剛纔我們給出的特徵。

然後我們來看下如何用 B 樹進行查找。假設我們想要查找的關鍵字是 9,那麼步驟可以分爲以下幾步:

  1. 我們與根節點的關鍵字 (17,35)進行比較,9 小於 17 那麼得到指針 P1;
  2. 按照指針 P1 找到磁盤塊 2,關鍵字爲(8,12),因爲 9 在 8 和 12 之間,所以我們得到指針 P2;
  3. 按照指針 P2 找到磁盤塊 6,關鍵字爲(9,10),然後我們找到了關鍵字 9。

你能看出來在 B 樹的搜索過程中,我們比較的次數並不少,但如果把數據讀取出來然後在內存中進行比較,這個時間就是可以忽略不計的。而讀取磁盤塊本身需要進行 I/O 操作,消耗的時間比在內存中進行比較所需要的時間要多,是數據查找用時的重要因素,B 樹相比於平衡二叉樹來說磁盤 I/O 操作要少,在數據查詢中比平衡二叉樹效率要高。

什麼是 B+ 樹

B+ 樹基於 B 樹做出了改進,主流的 DBMS 都支持 B+ 樹的索引方式,比如 MySQL。B+ 樹和 B 樹的差異在於以下幾點:

  1. 有 k 個孩子的節點就有 k 個關鍵字。也就是孩子數量 = 關鍵字數,而 B 樹中,孩子數量 = 關鍵字數 +1。
  2. 非葉子節點的關鍵字也會同時存在在子節點中,並且是在子節點中所有關鍵字的最大(或最小)。
  3. 非葉子節點僅用於索引,不保存數據記錄,跟記錄有關的信息都放在葉子節點中。而 B 樹中,非葉子節點既保存索引,也保存數據記錄。
  4. 所有關鍵字都在葉子節點出現,葉子節點構成一個有序鏈表,而且葉子節點本身按照關鍵字的大小從小到大順序鏈接。

下圖就是一棵 B+ 樹,階數爲 3,根節點中的關鍵字 1、18、35 分別是子節點(1,8,14),(18,24,31)和(35,41,53)中的最小值。每一層父節點的關鍵字都會出現在下一層的子節點的關鍵字中,因此在葉子節點中包括了所有的關鍵字信息,並且每一個葉子節點都有一個指向下一個節點的指針,這樣就形成了一個鏈表。


比如,我們想要查找關鍵字 16,B+ 樹會自頂向下逐層進行查找:

  1. 與根節點的關鍵字 (1,18,35) 進行比較,16 在 1 和 18 之間,得到指針 P1(指向磁盤塊 2)
  2. 找到磁盤塊 2,關鍵字爲(1,8,14),因爲 16 大於 14,所以得到指針 P3(指向磁盤塊 7)
  3. 找到磁盤塊 7,關鍵字爲(14,16,17),然後我們找到了關鍵字 16,所以可以找到關鍵字 16 所對應的數據。

整個過程一共進行了 3 次 I/O 操作,看起來 B+ 樹和 B 樹的查詢過程差不多,但是 B+ 樹和 B 樹有個根本的差異在於,B+ 樹的中間節點並不直接存儲數據。這樣的好處都有什麼呢?

首先,B+ 樹查詢效率更穩定。因爲 B+ 樹每次只有訪問到葉子節點才能找到對應的數據,而在 B 樹中,非葉子節點也會存儲數據,這樣就會造成查詢效率不穩定的情況,有時候訪問到了非葉子節點就可以找到關鍵字,而有時需要訪問到葉子節點才能找到關鍵字。

其次,B+ 樹的查詢效率更高,這是因爲通常 B+ 樹比 B 樹更矮胖(階數更大,深度更低),查詢所需要的磁盤 I/O 也會更少。同樣的磁盤頁大小,B+ 樹可以存儲更多的節點關鍵字。

不僅是對單個關鍵字的查詢上,在查詢範圍上,B+ 樹的效率也比 B 樹高。這是因爲所有關鍵字都出現在 B+ 樹的葉子節點中,並通過有序鏈表進行了鏈接。而在 B 樹中則需要通過中序遍歷才能完成查詢範圍的查找,效率要低很多。

總結

磁盤的 I/O 操作次數對索引的使用效率至關重要。雖然傳統的二叉樹數據結構查找數據的效率高,但很容易增加磁盤 I/O 操作的次數,影響索引使用的效率。因此在構造索引的時候,我們更傾向於採用“矮胖”的數據結構。

B 樹和 B+ 樹都可以作爲索引的數據結構,在 MySQL 中採用的是 B+ 樹,B+ 樹在查詢性能上更穩定,在磁盤頁大小相同的情況下,樹的構造更加矮胖,所需要進行的磁盤 I/O 次數更少,更適合進行關鍵字的範圍查詢。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章