[Mysql]索引選型

Duancf發表於2024-08-25

索引底層資料結構選型

Hash 表

雜湊表是鍵值對的集合,透過鍵(key)即可快速取出對應的值(value),因此雜湊表可以快速檢索資料(接近 O(1))。
為何能夠透過 key 快速取出 value 呢? 原因在於 雜湊演算法(也叫雜湊演算法)。透過雜湊演算法,我們可以快速找到 key 對應的 index,找到了 index 也就找到了對應的 value。

hash = hashfunc(key)
index = hash % array_size

但是雜湊演算法有個 Hash 衝突 問題,也就是說多個不同的 key 最後得到的 index 相同。通常情況下,我們常用的解決辦法是 鏈地址法。鏈地址法就是將雜湊衝突資料存放在連結串列中。就比如 JDK1.8 之前 HashMap 就是透過鏈地址法來解決雜湊衝突的。不過,JDK1.8 以後HashMap為了減少連結串列過長的時候搜尋時間過長引入了紅黑樹。

為了減少 Hash 衝突的發生,一個好的雜湊函式應該“均勻地”將資料分佈在整個可能的雜湊值集合中。

MySQL 的 InnoDB 儲存引擎不直接支援常規的雜湊索引,但是,InnoDB 儲存引擎中存在一種特殊的“自適應雜湊索引”(Adaptive Hash Index),自適應雜湊索引並不是傳統意義上的純雜湊索引,而是結合了 B+Tree 和雜湊索引的特點,以便更好地適應實際應用中的資料訪問模式和效能需求。自適應雜湊索引的每個雜湊桶實際上是一個小型的 B+Tree 結構。這個 B+Tree 結構可以儲存多個鍵值對,而不僅僅是一個鍵。這有助於減少雜湊衝突鏈的長度,提高了索引的效率。關於 Adaptive Hash Index 的詳細介紹,可以檢視 MySQL 各種“Buffer”之 Adaptive Hash Index 這篇文章。

既然雜湊表這麼快,為什麼 MySQL 沒有使用其作為索引的資料結構呢
主要是因為 Hash 索引不支援順序和範圍查詢。假如我們要對錶中的資料進行排序或者進行範圍查詢,那 Hash 索引可就不行了。
並且,每次 IO 只能取一個。

試想一種情況:

SELECT * FROM tb1 WHERE id < 500;

在這種範圍查詢中,B+優勢非常大,直接遍歷比 500 小的葉子節點就夠了。而 Hash 索引是根據 hash 演算法來定位的,難不成還要把 1 - 499 的資料,每個都進行一次 hash 計算來定位嗎?這就是 Hash 最大的缺點了。

二叉查詢樹(BST)

二叉查詢樹(Binary Search Tree)是一種基於二叉樹的資料結構,它具有以下特點:
左子樹所有節點的值均小於根節點的值。
右子樹所有節點的值均大於根節點的值。
左右子樹也分別為二叉查詢樹。
二叉查詢樹是平衡的時候,也就是樹的每個節點的左右子樹深度相差不超過 1 的時候,查詢的時間複雜度為O(log2(N)),具有比較高的效率。
然而,當二叉查詢樹不平衡時,例如在最壞情況下(有序插入節點),樹會退化成線性連結串列(也被稱為斜樹),導致查詢效率急劇下降,時間複雜退化為 O(N)
也就是說,二叉查詢樹的效能非常依賴於它的平衡程度,這就導致其不適合作為 MySQL 底層索引的資料結構。
為了解決這個問題,並提高查詢效率,人們發明了多種在二叉查詢樹基礎上的改進型資料結構,如平衡二叉樹、B-Tree、B+Tree 等。

AVL 樹

AVL 樹是電腦科學中最早被發明的自平衡二叉查詢樹,它的名稱來自於發明者 G.M. Adelson-Velsky 和 E.M. Landis 的名字縮寫。AVL 樹的特點是保證任何節點的左右子樹高度之差不超過 1,因此也被稱為高度平衡二叉樹,它的查詢、插入和刪除在平均和最壞情況下的時間複雜度都是 O(logn)

AVL 樹採用了旋轉操作來保持平衡。主要有四種旋轉操作:
LL 旋轉、
RR 旋轉、
LR 旋轉和
RL 旋轉。其中 LL 旋轉和 RR 旋轉分別用於處理左左和右右失衡,而 LR 旋轉和 RL 旋轉則用於處理左右和右左失衡。

由於 AVL 樹需要頻繁地進行旋轉操作來保持平衡,因此會有較大的計算開銷進而降低了查詢效能。並且, 在使用 AVL 樹時,每個樹節點僅儲存一個資料,而每次進行磁碟 IO 時只能讀取一個節點的資料,如果需要查詢的資料分佈在多個節點上,那麼就需要進行多次磁碟 IO。 磁碟 IO 是一項耗時的操作,在設計資料庫索引時,我們需要優先考慮如何最大限度地減少磁碟 IO 操作的次數。實際應用中,AVL 樹使用的並不多。

紅黑樹

紅黑樹是一種自平衡二叉查詢樹,透過在插入和刪除節點時進行顏色變換和旋轉操作,使得樹始終保持平衡狀態,它具有以下特點:

每個節點非紅即黑;
根節點總是黑色的;
每個葉子節點都是黑色的空節點(NIL 節點);
如果節點是紅色的,則它的子節點必須是黑色的(反之不一定);
從根節點到葉節點或空子節點的每條路徑,必須包含相同數目的黑色節點(即相同的黑色高度)。

和 AVL 樹不同的是,紅黑樹並不追求嚴格的平衡,而是大致的平衡。正因如此,紅黑樹的查詢效率稍有下降,因為紅黑樹的平衡性相對較弱,可能會導致樹的高度較高,這可能會導致一些資料需要進行多次磁碟 IO 操作才能查詢到,這也是 MySQL 沒有選擇紅黑樹的主要原因。也正因如此,紅黑樹的插入和刪除操作效率大大提高了,因為紅黑樹在插入和刪除節點時只需進行 O(1) 次數的旋轉和變色操作,即可保持基本平衡狀態,而不需要像 AVL 樹一樣進行 O(logn) 次數的旋轉操作。

紅黑樹的應用還是比較廣泛的,TreeMap、TreeSet 以及 JDK1.8 的 HashMap 底層都用到了紅黑樹。對於資料在記憶體中的這種情況來說,紅黑樹的表現是非常優異的。

B 樹& B+樹

B 樹也稱 B-樹,全稱為 多路平衡查詢樹 ,B+ 樹是 B 樹的一種變體。B 樹和 B+樹中的 B 是 Balanced (平衡)的意思。
目前大部分資料庫系統及檔案系統都採用 B-Tree 或其變種 B+Tree 作為索引結構。

B 樹& B+樹兩者有何異同呢?

  • B 樹的所有節點既存放鍵(key) 也存放資料(data);而 B+樹只有葉子節點存放 key 和 data,其他內節點只存放 key。
  • B 樹的葉子節點都是獨立的;B+樹的葉子節點有一條引用鏈指向與它相鄰的葉子節點。
  • B 樹的檢索的過程相當於對範圍內的每個節點的關鍵字做二分查詢,可能還沒有到達葉子節點,檢索就結束了。而 B+樹的檢索效率就很穩定了,任何查詢都是從根節點到葉子節點的過程,葉子節點的順序檢索很明顯。
  • 在 B 樹中進行範圍查詢時,首先找到要查詢的下限,然後對 B 樹進行中序遍歷,直到找到查詢的上限;而 B+樹的範圍查詢,只需要對連結串列進行遍歷即可。

綜上,B+樹與 B 樹相比,具備更少的 IO 次數、更穩定的查詢效率和更適於範圍查詢這些優勢。
在 MySQL 中,MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作為索引結構,但是,兩者的實現方式不太一樣。(下面的內容整理自《Java 工程師修煉之道》)

MyISAM 引擎中,B+Tree 葉節點的 data 域存放的是資料記錄的地址。在索引檢索的時候,首先按照 B+Tree 搜尋演算法搜尋索引,如果指定的 Key 存在,則取出其 data 域的值,然後以 data 域的值為地址讀取相應的資料記錄。這被稱為“非聚簇索引(非聚集索引)”。

InnoDB 引擎中,其資料檔案本身就是索引檔案。相比 MyISAM,索引檔案和資料檔案是分離的,其表資料檔案本身就是按 B+Tree 組織的一個索引結構,樹的葉節點 data 域儲存了完整的資料記錄。這個索引的 key 是資料表的主鍵,因此 InnoDB 表資料檔案本身就是主索引。這被稱為“聚簇索引(聚集索引)”,而其餘的索引都作為 輔助索引 ,輔助索引的 data 域儲存相應記錄主鍵的值而不是地址,這也是和 MyISAM 不同的地方。在根據主索引搜尋時,直接找到 key 所在的節點即可取出資料;在根據輔助索引查詢時,則需要先取出主鍵的值,再走一遍主索引。 因此,在設計表的時候,不建議使用過長的欄位作為主鍵,也不建議使用非單調的欄位作為主鍵,這樣會造成主索引頻繁分裂。

B樹和B+Tree每一個節點實際上可以理解為是一個檔案頁 mysql預設給一個檔案頁分配16k儲存
B樹的每個節點都會儲存data資料,而B+Tree的話非葉子節點是儲存的索引(冗餘),不儲存data資料,這樣每一頁檔案頁能儲存的節點就很多,樹的高度就可以得到很好的控制,樹的高度越高,從磁碟load節點到記憶體對比的次數就會越多,磁碟I/O是費時。
所以B+Tree在樹高度相同的情況下能夠儲存更多的索引資料,間接的減少了磁碟的I/O操作,B+Tree的I/O次數會更加穩定一些。
還有就是從範圍查詢的角度上來說B+Tree也具備絕對的優勢,因為B+Tree在每個相鄰的葉子節點之間都有互相指向。
B+Tree在全表掃描的情況下也是比較佔優勢的,因為B+Tree的資料都是儲存在非葉子節點的,所以只需要掃描葉子節點就可以拿到全部資料了,B Tree的話就需要從頭遍歷整顆樹。

Mysql的索引為什麼使用B+樹而不使用跳錶

B+樹更適合磁碟IO

B+Tree一個節點是一個page,是一種多叉樹結構,每個結點都是一個16k的資料頁,能存放較多索引資訊。一次IO一個page,大大節省了磁碟IO的操作。B+Tree一個page 能存放較多索引資訊 ,所以樹的層數比較低, 三層左右就可以儲存2kw左右的資料也就是說查詢一次資料,如果這些資料頁都在磁碟裡,那麼最多需要查詢三次磁碟IO。

原生跳錶不適合磁碟IO

跳錶是連結串列結構,一條資料一個結點,那麼一個node節點一次磁碟io, 一個page 頁規模的IO儲存的效能 估計要下降1000倍以上。
原生跳錶 一個node存放一個 索引資訊 ,所以樹的層數比較高,如果最底層要存放2kw資料,且每次查詢都要能達到二分查詢的效果,2kw大概在2的24次方 左右。所以,2kw資料的跳錶大概高度在24層左右。 如果要進行查詢,大概要進行 24次磁碟IO。這裡講的是原生跳錶, 如果經過各種改進,那個不在此文討論範圍。

所以,雖然在理論上,跳錶的時間複雜度和B+樹相同 ,但是:
B+樹更適合 磁碟IO, 更合適MYSQL。從反面來說,跳錶更適合記憶體IO, 更適合redis。(redis沒有io的煩惱)

Redis的索引為什麼使用跳錶而不使用B+樹

那麼,為啥 redis 用跳錶而不用B+樹?
關於這個問題,Redis作者是這麼說的:

There are a few reasons:
1、They are not very memory intensive. 
It's up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees.
2、A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees.
3、They are simpler to implement, debug, and so forth. 
For instance thanks to the skip list simplicity I received a patch(already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code.

主要是從記憶體佔用、對範圍查詢的支援、實現難易程度這三方面總結的原因,簡單翻譯如下:

  • 它們不是非常記憶體密集型的。基本上由你決定。改變關於節點具有給定級別數的機率的引數將使其比 btree 佔用更少的記憶體。
  • Zset 經常需要執行 ZRANGE 或 ZREVRANGE 的命令,即作為連結串列遍歷跳錶。透過此操作,跳錶的快取區域性性至少與其他型別的平衡樹一樣好。
  • 它們更易於實現、除錯等。例如,由於跳錶的簡單性,我收到了一個補丁(已經在Redis master中),其中擴充套件了跳錶,在 O(log(N) 中實現了 ZRANK。它只需要對程式碼進行少量修改。

關於上述觀點,做幾點補充如下:

  • 從記憶體佔用上來比較,跳錶比平衡樹更靈活一些。平衡樹每個節點包含 2 個指標(分別指向左右子樹),而跳錶每個節點包含的指標數目平均為 1/(1-p),具體取決於引數 p 的大小。如果像 Redis裡的實現一樣,取 p=1/4,那麼平均每個節點包含 1.33 個指標,比平衡樹更有優勢。
  • 在做範圍查詢的時候,跳錶比平衡樹操作要簡單。在平衡樹上,我們找到指定範圍的小值之後,還需要以中序遍歷的順序繼續尋找其它不超過大值的節點。如果不對平衡樹進行一定的改造,這裡的中序遍歷並不容易實現。而在跳錶上進行範圍查詢就非常簡單,只需要在找到小值之後,對第 1 層連結串列進行若干步的遍歷就可以實現。
  • 從演算法實現難度上來比較,跳錶比平衡樹要簡單得多。平衡樹的插入和刪除操作可能引發子樹的調整,邏輯複雜,而跳錶的插入和刪除只需要修改相鄰節點的指標,操作簡單又快速。

Mysql為什麼用B+樹而不用跳錶

相關文章