MySQL(十一):InnoDB 索引與算法(中篇)

1、簡述

上文我們對InnoDB 索引有了一個基本的認識,本篇主要介紹 InnoDB 存儲引擎實現索引時採用的算法及常見數據庫索引算法比較。通過本節你將得到以下幾個問題的答案。

InnoDB 存儲引擎實現索引爲什麼不用二叉樹、紅黑樹或其它算法?

InnoDB 存儲引擎實現索引中 b tree 是什麼?b+ tree 和 b- tree有什麼區別?

B+樹索引就是傳統意義上的索引,這是目前關係型數據庫系統中查找最爲常用有效的索引。B+樹索引的構造類似於二叉樹,根據鍵值(Key Value)快速找到數據。

B+樹中的B不是代表二叉(binary),而是代表平衡(balance),因爲B+樹是從平衡二叉樹演化而來的,但是B+樹不是二叉樹。B+樹索引並不能找到指定鍵值的具體行。B+樹索引能找到的只是數據行所在的數據頁。然後數據庫通過把頁讀入內存,然後在內存中進行查找,最後得到要查找的數據。

2、二分查找法

二分查找法(binary search)也稱爲折半查找法,用來查找一組有序數組中的某一記錄,其基本思想就是:將記錄有序排列,在查找過程中先以有序數列的中點位置開始比較,如果要找的元素值小於該中間元素,則將待查詢序列縮小爲左半部分,反之縮小爲右半部分。每查找一次,就會將查找範圍縮小一半。

在這裏插入圖片描述

如圖,用了3次就找到了17這個數,如果順序查找需要8次。可見二分查找的效率比順序查找法效率要高。

2.1、二分查找法在InnoDB存儲引擎中的應用

在InnoDB存儲引擎中,每頁 Page Directory 中的槽是按照主鍵的順序存放的,對於某一條具體記錄的查詢是通過對 Page Directory 進行二分查找得到的。

3、哈希表(Hash)

哈希函數也稱爲是散列函數,是Hash表的映射函數,它可以把任意長度的輸入變換成固定長度的輸出,該輸出就是哈希值。哈希函數能使對一個數據序列的訪問過程變得更加迅速有效,通過哈希函數,數據元素能夠被很快的進行定位。

若關鍵字爲k,則其值存放在f(k)的存儲位置上。由此,不需比較便可直接取得所查記錄。稱這個對應關係f爲散列函數,按這個思想建立的表爲散列表

對不同的關鍵字可能得到同一散列地址,即k1!=k2,而f(k1)=f(k2),這種現象稱爲哈希衝突。具有相同函數值的關鍵字對該散列函數來說稱做同義詞。綜上所述,根據散列函數f(k)和處理衝突的方法將一組關鍵字映射到一個有限的連續的地址集(區間)上,並以關鍵字所在地址集中地址作爲記錄在表中的存儲位置,這種表便稱爲散列表,這一映射過程稱爲散列,所得的存儲位置稱散列地址

若對於關鍵字集合中的任一個關鍵字,經散列函數映射到地址集合中任何一個地址的概率是相等的,則稱此類散列函數爲均勻散列函數(Uniform Hash function),這時就需要使關鍵字經過散列函數得到一個“隨機的地址”,從而減少衝突。

3.1、哈希函數的構造

哈希表的構造方法是:假設要存儲的數據元素個數爲n,設置一個長度爲m(m≥n)的連續存儲單元,分別以每個數據元素的關鍵字 Ki(0<= i <=n-1) 爲自變量,通過哈希函數 hash(Ki) 把 Ki 映射爲內存單元的某個地址 hash(ki),並將該數據元素存儲在該內存單元中。

哈希函數實際上是關鍵字到內存單元的映射,因此我們希望用哈希函數通過儘量簡單的運算,使得通過哈希函數計算出的哈希地址儘量均勻地被映射到一系列的內存單元中。

哈希函數的構造一般有以下幾種方法:

  • 直接定址法:

取關鍵字或關鍵字的某個線性函數值爲散列地址。即hash(k)=k或 hash(k)=a * k+b,其中a,b爲常數(這種散列函數叫做自身函數)

在這裏插入圖片描述

此方法的優點是不會產生衝突,但缺點是空間複雜度可能會很高,適用於元素較少的情況下。

  • 數字分析法:

假設關鍵字是以r爲基的數,並且哈希表中可能出現的關鍵字都是事先知道的,則可取關鍵字的若干數位組成哈希地址,但是該方法只適合於所有關鍵字已知的情況。對於想要設計出更加通用的哈希表並不適用。

  • 平方取中法:

取關鍵字平方後的中間幾位爲哈希地址。通常在選定哈希函數時不一定能知道關鍵字的全部情況,取其中的哪幾位也不一定合適,而一個數平方後的中間幾位數和數的每一位都相關,由此使隨機分佈的關鍵字得到的哈希地址也是隨機的。取的位數由表長決定。

  • 摺疊法:

將關鍵字分割成位數相同的幾部分(最後一部分的位數可以不同),然後取這幾部分的疊加和(捨去進位)作爲哈希地址。

  • 隨機數法

  • 除留餘數法:

取關鍵字被某個不大於散列表表長m的數p除後所得的餘數爲散列地址。即

在這裏插入圖片描述

不僅可以對關鍵字直接取模,也可在摺疊法、平方取中法等運算之後取模。對p的選擇很重要,一般取素數或m,若p選擇不好,容易產生衝突。

3.2、Hash衝突的解決

在構造哈希表時,存在這樣的問題,對於兩個不同的關鍵字,通過我們的哈希函數計算哈希地址時卻得到了相同的哈希地址,我們將這種現象稱爲哈希衝突(如圖):

在這裏插入圖片描述

哈希衝突主要與兩個因素相關:

1、填裝因子,所謂的填裝因子是指哈希表中已存入的數據元素個數與哈希地址空間大小的比值,即α=n/m,α越小,衝突的可能性就越小,相反則衝突可能性越大;但是α越小,哈希表的存儲空間利用率也就很低,α越大,存儲空間的利用率也就越高,爲了兼顧哈希衝突和存儲空間利用率,通常將α控制在0.6-0.9之間(JDK中取0.75)。

2、哈希函數,如果哈希函數選擇合理,就可以使哈希地址儘可能的均勻分佈在哈希地址空間上,從而減少衝突的產生,但一個合理的哈希函數需要經過大量的實踐才能得到,不過幸運的是前輩們已經通過實踐總結了高效的哈希函數,可以參考大牛Lucifer的文章:數據結構 : Hash Table [I]

哈希衝突通常是很難避免的,解決哈希衝突有很多種方法,通常分爲兩大類:

1、開放定址法

如果兩個數據元素的哈希值相同,則在哈希表中爲後插入的數據元素逐個探測存放地址的表,直到查找到一個空單元,把散列地址存放在該空單元。線性探測帶來的最大問題就是衝突的堆積,改進的辦法有二次方探測法和隨機數探測法。開放定址法包括線性探測、二次探測以及雙重散列等方法。

2、鏈表法
將散列到同一個存儲位置的所有元素保存在一個鏈表中。實現時,一種策略是散列表同一位置的所有衝突結果都是用棧存放的,新元素被插入到表的前端還是後端完全取決於怎樣方便。

在這裏插入圖片描述

3.4、MySQL爲什麼沒有采用hash表作爲索引算法

InnoDB 存儲引擎支持的哈希索引是自適應的,InnoDB 存儲引擎會根據表的使用情況自動爲表生成哈希索引,不能人爲干預生成哈希索引。

從算法時間複雜度分析來看,哈希算法時間複雜度爲 O(1),檢索速度非常快。比如精確查找 ,哈希索引只需要計算一次就可以獲取到對應的數據,檢索速度非常快。

因爲SQL 查詢中除了精確查找還需要範圍查找數據,比如以下這個 SQL 語句:

select * from user where id>0 and id<9

如果使用哈希算法實現的索引,範圍查找怎麼實現呢?一個簡單的方法就是把所有數據加載到內存,然後在內存裏篩選篩選目標範圍內的數據,但是這個範圍查找的方法代價太高。

所以,使用哈希算法實現的索引雖然可以做到快速檢索數據,但是沒辦法做數據高效範圍查找,因此哈希索引是不適合作爲 Mysql 的底層索引的數據結構。

4、二叉查找樹(BST)

二叉查找樹(Binary Search Tree),又稱二叉排序樹(Binary Sort Tree),亦稱二叉搜索樹。

在這裏插入圖片描述

圖中的數字代表每個節點的鍵值,在二叉查找樹中,左子樹的鍵值總是小於根的鍵值,右子樹的鍵值總是大於根的鍵值。因此可以通過中序遍歷得到鍵值的順序輸出,如圖中中序遍歷的結果爲:2、3、5、6、7、8。

二叉查找樹的時間複雜度是 O(logn),比如針對上面這個二叉樹結構,我們只需要比較3次就可以找到 8 ,相對於直接遍歷查詢省了一半的時間,從檢索效率上看來高效的。

由上圖可知道,二叉樹的葉子節點都是按序排列的,從左到右依次升序排列,如果我們需要找 id>5 的數據,那我們取出節點爲 6 的節點以及其右子樹就可以了,範圍查找也算是比較容易實現。

但是普通的二叉查找樹有個致命缺點:極端情況下會退化爲線性鏈表,二分查找也會退化爲遍歷查找,時間複雜退化爲 O(N),檢索性能急劇下降。

4.1、MySQL爲什麼沒有采用二叉查找樹(BST)作爲索引算法

在數據庫中,數據表的主鍵 id,一般默認都是自增的,如果採取二叉樹這種數據結構作爲索引,就會退化成上圖中的線性鏈表,檢索數據時就會出現線性查找的問題。因此,簡單的二叉查找樹存在不平衡導致的檢索性能降低的問題,是不能直接用於實現 Mysql 底層索引的。

5、紅黑樹

紅黑樹 R-B Tree,全稱是Red-Black Tree,它一種特殊的二叉查找樹。

普通的二叉查找樹在極端情況下可退化成鏈表,此時的增刪查效率都會比較低下。爲了避免這種情況,就出現了一些自平衡的查找樹,比如 AVL,紅黑樹等。這些自平衡的查找樹通過定義一些性質,將任意節點的左右子樹高度差控制在規定範圍內,以達到平衡狀態。以紅黑樹爲例,紅黑樹通過如下的性質定義實現自平衡:

  • 節點是紅色或黑色。

  • 根是黑色。

  • 所有葉子都是黑色(葉子是NIL節點)。

  • 每個紅色節點必須有兩個黑色的子節點。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點。)

  • 從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點(簡稱黑高)。

當二叉樹處於一個不平衡狀態時,紅黑樹就會自動左旋右旋節點以及節點變色,調整樹的形態,使其保持基本的平衡狀態(時間複雜度爲 O(logn)),也就保證了查找效率不會明顯減低。

在這裏插入圖片描述

5.1、MySQL爲什麼沒有采用紅黑樹作爲索引算法

觀察上圖,根據順序插入 1~19 個節點 生產的紅黑樹,樹的形態趨於右傾。由此可見紅黑樹並沒有完全解決二叉查找樹不平衡的問題,儘管這個“右傾”趨勢遠沒有二叉查找樹退化爲線性鏈表那麼嚴重,但是數據庫中的主鍵一般都是順序的,並且都是數十萬數百萬計的,如果紅黑樹存在這種問題,對於查找性能而言也是巨大的消耗,數據庫是不可能容忍這種隱患存在的。

6、平衡二叉樹(AVL)

AVL 樹是最先發明的自平衡二叉查找樹。在AVL樹中任何節點的兩個子樹的高度最大差別爲1,所以它也被稱爲高度平衡樹。增加和刪除可能需要通過一次或多次樹旋轉來重新平衡這個樹。

AVL樹本質上本質上是帶了平衡功能的二叉查找樹(二叉排序樹,二叉搜索樹),它的特點是:

1.本身首先是一棵二叉搜索樹。

2.帶有平衡條件:每個結點的左右子樹的高度之差的絕對值(平衡因子)最多爲1。

因爲 AVL 樹是個絕對平衡的二叉樹,因此他在調整二叉樹的形態上消耗的性能會更多。

在這裏插入圖片描述

AVL 樹順序插入 1~19 個節點,查找 19 需要比較的節點數爲 6。從查找效率而言,
AVL 樹查找的速度要高於紅黑樹的查找效率(AVL 樹是 5 次比較,紅黑樹是 6 次比較)。從樹的形態看來,AVL 樹不存在紅黑樹的“右傾”問題。也就是說,大量的順序插入不會導致查詢性能的降低,這從根本上解決了紅黑樹的問題。

6.1、MySQL爲什麼沒有采用平衡二叉樹(AVL)作爲索引算法

數據庫查詢數據的瓶頸在於磁盤 IO,如果使用的是 AVL 樹,每一個樹節點只存儲了一個數據,我們一次磁盤 IO 只能取出來一個節點上的數據加載到內存裏,那比如查詢 19 這個數據我們就要進行磁盤 IO 5次。所以我們設計數據庫索引時需要首先考慮怎麼儘可能減少磁盤 IO 的次數。

磁盤 IO 有個特點,就是從磁盤讀取 1B 數據和 1KB 數據所消耗的時間是基本一樣的,我們就可以根據這個思路,可以在一個樹節點上儘可能多地存儲數據,一次磁盤 IO 就多加載點數據到內存,這就是 B 樹,B+樹的的設計原理了。

7、B-Tree

B-Tree 樹是一種自平衡搜索樹。要了解 B-Tree 的用法,我們必須考慮無法容納在主內存中的大量數據。當鍵的數量很大時,將以塊形式從磁盤讀取數據。與主存儲器訪問時間相比,磁盤訪問時間非常高。使用 B-Tree 的主要思想是減少磁盤訪問次數。大多數樹操作(搜索,插入,刪除等)都需要O(h)磁盤訪問,其中h是樹的高度。

B-Tree 是一棵胖樹。通過將最大可能的鍵放在B樹節點中,來保證 B-Tree 的高度較低。通常,B-Tree 節點大小保持等於磁盤塊大小。由於B-Tree的h值較低,因此與平衡的二進制搜索樹(例如AVL樹,紅黑樹等)相比,大多數操作的總磁盤訪問量顯着減少。
在這裏插入圖片描述

B-tree有以下特性:

1、關鍵字集合分佈在整棵樹中;

2、任何一個關鍵字出現且只出現在一個結點中;

3、搜索有可能在非葉子結點結束;

4、其搜索性能等價於在關鍵字全集內做一次二分查找;

5、自動層次控制;

由於B-Tree的特性,在B-Tree中按key檢索數據的算法,首先從根節點進行二分查找,如果找到則返回對應節點的data,否則對相應區間的指針指向的節點遞歸進行查找,直到找到節點或找到null指針,前者查找成功,後者查找失敗。

由於限制了除根結點以外的非葉子結點,至少含有M/2個兒子,確保了結點的至少利用率,
其中,M爲設定的非葉子結點最多子樹個數,N爲關鍵字總數;

所以B-樹的性能等價於二分查找(與M值無關),也就沒有B樹平衡的問題;
由於M/2的限制,在插入結點時,如果結點已滿,需要將結點分裂爲兩個各佔M/2的結點;刪除結點時,需將兩個不足M/2的兄弟結點合併。

鑑於B-tree具有良好的定位特性,其常被用於對檢索時間要求苛刻的場合,例如:

1、B-tree索引是數據庫中存取和查找文件(稱爲記錄或鍵值)的一種方法。

2、硬盤中的結點也是B-tree結構的。與內存相比,硬盤必須花成倍的時間來存取一個數據元素,這是因爲硬盤的機械部件讀寫數據的速度遠遠趕不上純電子媒體的內存。與一個結點兩個分支的二元樹相比,B-tree利用多個分支(稱爲子樹)的結點,減少獲取記錄時所經歷的結點數,從而達到節省存取時間的目的。

7.1、MySQL爲什麼沒有采用B-Tree作爲索引算法

由於插入刪除新的數據記錄會破壞B-Tree的性質,因此在插入刪除時,需要對B-Tree進行一個分裂、合併、轉移等操作以保持B-Tree性質。對於數據庫而言,插入、刪除操作是很常見的操作,因此如果MySQL採用B-Tree作爲索引算法,爲保證B-Tree的特性需要付出很大的代價。

8、B+Tree

B-Tree有許多變種,其中最常見的是B+Tree,例如MySQL使用B+Tree實現其索引結構。

與B-Tree相比,B+Tree有以下不同點:

(1)B-Tree 的每個NODE都記錄了 data,所以不是每次都要搜葉子節點才能拿到 data。
B+Tree,只有葉子節點有DATA,因此,每次都要搜到葉子節點取 data。

(2)B+樹的葉子節點用了一個鏈表將數據串聯起來,便於範圍查找。

在這裏插入圖片描述

8.1、MySQL爲什麼採用B+Tree作爲索引算法

通過 B 樹和 B+樹的對比我們看出,B+樹節點存儲的是索引,在單個節點存儲容量有限的情況下,單節點也能存儲大量索引,使得整個 B+樹高度降低,減少了磁盤 IO。其次,B+樹的葉子節點是真正數據存儲的地方,葉子節點用了鏈表連接起來,這個鏈表本身就是有序的,在數據範圍查找時,是很有效率的。因此 Mysql 的索引用的就是 B+樹,B+樹在查找效率、範圍查找中都有着非常不錯的性能。

9、參考文獻

  1. 《高性能MySQL(第3版)》
  2. 《MySQL技術內幕:InnoDB存儲引擎(第2版)》
  3. 《MySQL源碼庫》
  4. 《MySQL參考手冊》
  5. 《MySQL實戰45講》
  6. 《數據庫內核月報》
  7. https://www.cs.usfca.edu/~galles/visualization/BTree.html
  8. https://www.cs.usfca.edu/~galles/visualization/BPlusTree.html
  9. https://baike.baidu.com/item/B-tree/6606402
  10. https://en.wikipedia.org/wiki/B-tree?spm=a2c4e.10696291.0.0.7c4b19a4wjK9HV
  11. https://en.wikipedia.org/wiki/B+_tree?spm=a2c4e.10696291.0.0.54bd19a4c9noHZ
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章