不是吧!不是吧!竟然有比B+Tree更快的一種查詢結構 AHI Key Value 查詢AHI 構建AHI時機 構建AHI AHI鎖 小結 思考題

我們都知道MySQL中的B+Tree索引結構,對於根據某個條件查找記錄是非常快的。那麼,在不斷追求極致的驅動下,你有沒有想過MySQL會不會有比B+Tree更快的數據結構,來加速查找記錄的性能呢?答案是有的,MySQL爲了讓我們更快地獲取自己想查找的記錄,在InnoDB中,將查詢頻繁的條件和索引樹結果做了一個Hash映射,這樣,一個查詢就不需要每次搜索B+Tree去定位結果了,這個Hash映射就叫做AHI,全稱Adaptive Hash Index,自適應哈希索引

一聽這名字,你或許已經猜出個一二了。沒錯!它其實就是一個HashTable,在大學學習《數據結構》的時候,我們都知道Hash Table在查找其中的一個節點的數據是非常快的,算法時間複雜度O(1),所以,相比B+Tree而言,它的查找性能一定是更快的。

但是,有個問題:爲什麼這個Hash Table叫做自適應哈希索引呢,這個“自適應”是什麼概念?

從下面這個案例開始,詳細講解AHI,逐步帶你明白AHI這個自適應是怎麼一回事?

假設我們交友平臺有個功能:篩選出年齡在15到23之間的用戶。那麼,通常我們會用下面這條SQL實現:

SELECT id, age, sex FROM user WHERE age >= 15 AND age <= 23

同時,我們給user表建了一個索引index_age_sex(age,sex),那麼,現在我們再來看看這條SQL是如何使用AHI的?

AHI

既然AHI也是一個HashTable,那首先,你肯定會關心,它的Key是什麼樣的,Value又是什麼樣的?那麼,我們就先來看看AHI的Key和Value。

我們看到《導讀》中的語句的查詢條件爲age >= 15 AND age <= 23,按照上面我說的AHI的含義:將某一個查詢條件和其結果做了一個Hash映射,那麼,我們想象中的這個HashTable就類似下面這樣:

圖中上面的age >= 15 AND age <= 23代表查詢條件,也就Key,下面爲索引index_age_sex中滿足查詢條件的4條記錄,也就是Value。其中,每條記錄的結構爲[age,sex,id]。

Key

但是,從上面的圖來看,如果查詢條件的字段很長,那麼,Key存儲的空間也就變得很大,對於MySQL這種內存敏感的系統而言肯定是不能接受的,因此,MySQL設計了下面的這種結構來存放Key:

如上圖爲查找索引index_age_sex時,查詢條件爲age >= 15 AND age <= 23的結構:

  • search_info::n_fields:MySQL使用n_fields來表達查詢索引使用到的字段,圖中1表示查詢條件使用到了索引index_age_sex中的第一個字段,即age。(PS:如果n_fields=2表示查詢條件使用到了索引index_age_sex中的age和sex兩個字段)。這樣做的好處是我們在內存中存儲數字就可以表達一個查詢條件使用到的索引字段了,更節省存儲空間。
  • dtuple_t:由於查詢條件是一個範圍查詢,所以,MySQL使用兩個dtuple_t結構來表示條件中的兩個邊界值。如上圖,右邊第一個dtuple_t中的15表示索引第一列age >= 15,第二個dtuple_t中的23表示索引第一列age <= 23。

最終,MySQL通過search_info::n_fields和dtuple_t的組合來表達查詢條件age >= 15 AND age <= 23。如上圖中的兩個箭頭表示的就是這種組合。

講完Key,我們再來看看MySQL是如何設計HashTable的Value的?

Value

當然,如果按照上面的HashTable的結構,我們肯定認爲查詢條件age >= 15 AND age <= 23,其在HashTable中的Value就是上面圖中1-1中的下面的記錄。但是,我們現在來看下面一個場景:

假設現在我將查詢條件變爲age >= 15 AND age < 16,那麼,這個HashTable就變成這樣:

圖中上面的age >= 15 AND age < 16代表查詢條件,下面爲索引index_age_sex中滿足查詢條件的2條記錄,其中,每條記錄的結構爲[age,sex,id]。

通過對比1-1和1-2-1上面兩張圖,我們發現2個查詢條件對應的查詢結果中有重複記錄15,0,2和15,0,5。現在只有2個查詢條件會出現重複記錄,那麼,如果將來有幾十個,甚至上百個查詢條件都包含重複記錄,那麼,是不是在存儲空間上就很浪費了?

因此,爲了節省查詢結果的存儲空間,我們可以將上面2個查詢HashTable合併,變成下面這樣的結構:

圖中MySQL將條件age >= 15 AND age <= 23和條件age >= 15 AND age < 16對應的記錄合併爲4條:

  • 條件age >= 15 AND age <= 23映射前兩條記錄。如上圖綠色箭頭。
  • 條件age >= 15 AND age < 16映射後兩條記錄。如上圖紅色箭頭。

但是,在講述Key的結構時,我說了MySQL真實設計的Key結構如圖1-1-1,對應到圖1-2-2,顯然圖1-2-2中的Key不是MySQL真實存儲的結構。那麼,結合1-2-2的HashTable概念圖,我們來看下MySQL到底是如何設計AHI的Key和Value的映射的?

如上圖,是MySQL完整的AHI的存儲結構。其中,Value上面的部分就是Key,上面我已經講解過Key的結構,這裏我就不再重述了。我們主要看下Value部分:

  • Cell:在AHI中叫做hash_cell_t,hash_cell_tuple的縮寫。也就是圖中的cell這部分。它是一個數組,如上圖是一個包含2個cell的數組。每個查詢條件的邊界值通過hash運算可以定位到某一個cell。比如:圖中條件age >= 15 AND age <= 23中的左邊界值15,通過hash運算定位到了第1個cell。圖中條件age >= 15 AND age < 16中的左邊界值15,通過hash運算也定位到了第1個cell。圖中條件age >= 15 AND age <= 23中的右邊界值23,通過hash運算也定位到了第1個cell。圖中條件age >= 15 AND age < 16中的右邊界值16,通過hash運算定位到了第2個cell。
  • Node:在AHI中叫做ha_node_t,hash_node_tuple的縮寫。一個cell下可以包含多個node,也就是說多個查詢條件邊界值通過hash運算可以定位到一個cell,該cell下就存放了每個邊界值對應的node記錄,該cell下的每個node還組成了一個單向鏈表。比如,圖中查詢條件age >= 15 AND age <= 23中的左邊界值15,通過hash運算,定位到第1個cell,該cell下的第1個node保存了15對應的記錄(15,0,2)相關信息。同理,圖中查詢條件age >= 15 AND age <= 23中的右邊界值23,通過hash運算,也定位到第1個cell,該cell下的第2個node保存了23對應的記錄相關信息。同理,圖中查詢條件age >= 15 AND age < 16中的右邊界值16,通過hash運算,定位到第2個cell,該cell下的第1個node保存了16對應的記錄(16,0,3)相關信息。這兩個node組成了一個單向鏈表。Node核心元素主要是3個:block:存儲hash映射的結果對應的相關信息。其中,核心元素包含left_side和page。比如,圖中左邊block裏的curr_left_side = true,表示該node中的記錄<15,0,2>是查詢條件age >= 15 AND age <= 23和age >= 15 AND age < 16的最左邊界記錄。比如,圖中左邊block中的page(10)表示該node中的記錄<15,0,2>在索引樹index_age_sex的10號葉子節點內。比如,圖中右邊block裏的curr_left_side = false,表示該node中的記錄<16,0,3>是查詢條件age >= 15 AND age < 16的最右邊界記錄。比如,圖中右邊block中的page(20)表示該node中的記錄<16,0,3>在索引樹index_age_sex的20號葉子節點內。data:hash映射的結果。比如,圖中第1個node中的<15,0,2>爲條件age >= 15 AND age <= 23和age >= 15 AND age < 16左邊界值15對應的記錄。比如,圖中第3個node中的<16,0,3>爲條件age >= 15 AND age < 16右邊界值16對應的記錄。

現在我們知道了AHI的完整結構,通過這個結構,我們發現MySQL沒有直接將查詢條件和結果做了映射,而是通過cell將條件和結果關聯起來,這樣做的好處就是相同條件邊界值對應的node在內存中可以共享,節省了存儲空間。

查詢AHI

說了那麼多,是不是發現好像這個AHI結構並沒有完整存儲查詢條件對應的所有結果記錄,(畢竟我要的可是4條滿足條件的記錄哦!),那MySQL又是怎麼通過AHI找到所有滿足條件的記錄呢?下面我們就以age >= 15 AND age < 16這個查詢條件爲例,來看一下這個查找過程:

  1. 根據條件左邊界值15,做hash運算,計算得到一個fold值,通過該值定位到第1個cell。
  2. 遍歷第1個cell下的node,找到第1個node爲邊界值15對應的node。
  3. 根據第1個node找到對應的記錄<15,0,2>、page(10)和curr_left_side=true。
  4. 根據上一步得到的page編號10和記錄,在索引樹index_age_sex中找到10號葉子節點中的記錄<15,0,2>。
  5. 根據條件右邊界值16,做hash運算,計算得到一個fold值,通過該值定位到第2個cell。
  6. 遍歷第2個cell下的node,找到第1個node爲邊界值16對應的node。
  7. 根據第1個node找到對應的記錄<16,0,3>、page(11)和curr_left_side=false。
  8. 根據上一步得到的page編號11和記錄,在索引樹index_age_sex找到11號葉子節點中的記錄<16,0,3>。
  9. 由於第3步中記錄<15,0,2>所在node中curr_left_side=true,說明記錄<15,0,2>爲查詢條件最左記錄,因此,從索引樹index_age_sex的10號葉子節點內<15,0,2>記錄開始,向後遍歷其他記錄。
  10. 由於第7步中記錄<16,0,3>所在node中curr_left_side=false,說明記錄<16,0,3>爲查詢條件最右記錄,故上一步遍歷到記錄<16,0,3>結束。
  11. 最終,在索引樹index_age_sex中找到所有滿足條件age >= 15 AND age < 16的記錄。

其中,第4,8 ~ 11步的細節過程,可以參考文章《InnoDB是順序查找B+Tree葉子節點的嗎?》

構建AHI時機

現在我們知道了MySQL如何通過AHI找到滿足條件的記錄了,那麼,這個AHI又是在什麼時候創建的,如何創建的呢?

在《導讀》中我講過,MySQL對使用頻繁的查詢條件才構建AHI,即條件與結果的映射關係。因此,我們就要看看MySQL是如何判斷這個查詢條件是否頻繁使用的?

爲了統計一個條件使用的頻率,MySQL設計了下面這樣一種結構。

是不是有點眼熟?其實,圖中search_info就是查詢信息的結構,在圖1-1-1中,我講了search_info中的一個屬性n_fields,現在,我再講另一個屬性hash_analysis。

當一次查詢成功後,MySQL通過累加該屬性,記錄該次查詢成功的次數。比如,初始hash_analysis=0,那麼,條件age >= 15 AND age < 16查詢成功一次,hash_analysis + 1 = 1,再成功一次,hash_analysis + 1 = 2,依次類推,成功多少次,hash_analysis就是多少。

當hash_analysis值超過17時,MySQL就會對該查詢構建AHI。

但是,查詢成功,就一定能夠構建AHI嗎?答案是不一定!我們來看下面這個場景:

SELECT age, sex FROM user WHERE age >= 15 AND age <= 18

上面這條語句,MySQL在索引index_age_sex中的葉子節點找到滿足條件的記錄爲下面4條:

<15,0,2>`、`<16,0,3>`、`<18,0,4>`、`<18,0,5>

這時候,我們再看下這個條件查找AHI的過程:

  1. 根據條件左邊界值15,做hash運算,計算得到一個fold值,通過該值定位到第1個cell。
  2. 遍歷第1個cell下的node,找到第1個node爲邊界值15對應的node。
  3. 根據得到的node找到對應的記錄<15,0>、page(10)和curr_left_side=true。
  4. 根據上一步得到的page編號10和記錄,在索引樹index_age_sex中找到10號葉子節點中滿足node記錄<15,0>的第一條記錄<15,0,2>。
  5. 根據條件右邊界值18,做hash運算,計算得到一個fold值,通過該值定位到第2個cell。
  6. 遍歷第2個cell下的node,找到第1個node爲邊界值18對應的node。
  7. 根據得到的node找到對應的記錄<18,0>、page(11)和curr_left_side=false。
  8. 根據上一步得到的page編號11和記錄,在索引樹index_age_sex找到11號葉子節點中滿足node記錄<18,0>的第一條記錄<18,0,4>。
  9. 由於第3步中記錄<15,0>所在node中curr_left_side=true,說明記錄<15,0,2>爲查詢條件最左記錄,因此,從索引樹index_age_sex的10號葉子節點內<15,0,2>記錄開始,向後遍歷其他記錄。
  10. 由於第7步中記錄<18,0>所在node中curr_left_side=false,說明記錄<18,0,4>爲查詢條件最右記錄,故上一步遍歷到記錄<18,0,4>結束。

其中,第4,8 ~ 10步的細節過程,可以參考文章《InnoDB是順序查找B+Tree葉子節點的嗎?》

從上面的過程,我們發現一個問題:明明11號葉子節點中的記錄<18,0,5>也滿足條件age >= 15 AND age <= 18,但是,AHI查詢卻忽略這條記錄。如上圖,虛線標出的記錄。

因此,我們發現並不是所有的查詢都支持AHI,我們不能簡單地認爲只要查詢成功,就等於可以構建AHI。

爲此,MySQL在search_info中引入了一個新的屬性,我們來看下:

如上圖中的n_hash_potential就是這個新屬性,它表示一次查詢潛在可以成功構建AHI的次數。用它來解決上面那個場景的問題:

只有查詢得到的結果中,最大的記錄中的select字段值唯一,n_hash_potential纔會累加。

這樣一來,MySQL就在真正構建AHI之前做了兩次攔截:

  • 通過hash_analysis將該屬性值小於17的查詢攔截,只有該屬性值大於等17,這次查詢才能構建AHI
  • 如果hash_analysis大於等於17,那麼,再檢查n_hash_potential屬性,如果該屬性值小於100,查詢攔截,反之,這次查詢才能構建AHI

那麼,下一個問題來了:既然我都已經知道上面那個場景是不可能構建AHI的,我爲什麼還要讓查詢處理進入上面兩次攔截檢查呢?

因此,爲了避免進入上面的攔截檢查,MySQL又在search_info中引入了一個屬性:

圖中last_hash_succ屬性,它表示上一次是否成功構建AHI。

有了這個屬性,MySQL只要發現上面這個場景壓根er不能構建AHI,因此,直接就設置last_hash_succ=false,那麼,在下次相同查詢進來後,直接發現last_hash_succ=false,就不再進行後面兩次的攔截檢查。

通過上面的分析,我們就得出了一次查詢觸發AHI構建的檢查過程:

如果last_hash_succ=false,該查詢不能構建AHI,反之進入下一步檢查

如果hash_analysis < 17,該查詢不能構建AHI,反之進入下一步檢查

如果n_hash_potential < 100,該查詢不能構建AHI,反之可以構建AHI

構建AHI

講完AHI構建的觸發條件,我們最後來看看MySQL是如何構建AHI的?

通過《查詢AHI》部分的講解,我們知道查詢AHI的過程中,AHI中的Node中包含幾個核心元素block、left_side和page,因此,我們只要知道這幾個核心元素是如何構建的,也就能夠描述清楚AHI的構建過程了。

我以下面這條語句爲例,看下AHI構建的過程:

SELECT id, age, sex FROM user WHERE age >= 15 AND age <= 18

關注圖中紅線部分:

  1. 根據條件左邊界值15,在索引樹index_age_sex中的10號葉子節點中找到滿足邊界值的第一條記錄<15,0,2>。
  2. 由於找到滿足左邊界值15的記錄只有一條,因此,MySQL將up_match + 1 = 1,表示只有一條記錄滿足左邊界值。由於up_match > low_match,因此,search_info中的left_side設置爲true。
  3. 根據條件左邊界值15,對其做hash運算,定位到AHI中的第1個cell。
  4. 發現cell中沒有node節點,創建一個node。即圖中灰色的node節點。
  5. 在node中創建一個block。如上圖淺藍色的block。
  6. 將索引樹index_age_sex中的10號葉子節點信息寫入block中的page屬性。
  7. 將第2步得到的left_side寫入block中的curr_left_side。
  8. 將第1步得到的記錄<15,0,2>寫入node。
  9. 同理,條件右邊界值18構建AHI的過程相同。

AHI鎖

瞭解完AHI的構建過程後,我們進一步會想,如果併發構建AHI,會出現node覆蓋的問題。因此,爲了解決這個問題,MySQL就必須給AHI加一把鎖,避免併發構建時產生node覆蓋的問題。

當然,我們不能給整個AHI加全局鎖吧,因爲這樣會非常影響查詢的性能,因此,MySQL是這樣設計鎖的。

MySQL在啓動時,從innodb_buffer_pool中劃分出若干個Hash Table,作爲AHI,圖中,我畫了2個HashTable:HashTable[0]和HashTable[1]。假設MySQL通過4個查詢條件hash運算得到4個fold,如上圖4個fold值分別爲1、2、9和17。

那麼,MySQL對AHI加鎖的方式爲fold % 8取模:

  • 1和2取模後,得到0,因此,這兩個fold對應的查詢在HashTable[0]中構建AHI,同時,加同一把鎖Lock0。
  • 9和17取模後,得到1,因此,這兩個fold對應的查詢在HashTable[1]中構建AHI,同時,加同一把鎖Lock1。

通過這種方式,MySQL就可以將鎖分散加在不同的HashTable上,儘可能減少併發導致的HashTable構建鎖死造成的性能問題。

小結

本章中,小k詳細講解AHI的結構、查詢、構建、加鎖等原理。

現在回答文章開頭的問題:爲什麼MySQL把這個HashTable叫做自適應哈希索引呢?

通過AHI構建的過程,我們發現,多個查詢構建cell中的node,是可以變大或縮小的,正是這個原因,MySQL才把這樣一個HashTable叫做AHI,即自適應哈希索引。

思考題

最後留一個思考題:在文中《構建AHI》部分中,我有提到up_match和low_match屬性,我們明明可以通過查詢條件是>或<來判斷left_side爲true還是false,爲什麼還要通過up_match和low_match來判斷呢?

提示:結合索引多列查詢場景思考一下。

最後,希望你有所收穫,如果你覺得文章還不錯,記得點贊哦~~

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