那些年與面試官交手過的數據庫索引

我坐在面試官的對面,聲情並茂的做着自我介紹,面試官小哥哥面無表情的翻看着我的簡歷。不知道是小哥哥過於高冷還是被我的簡歷吸引,2分鐘了,小哥哥還是沒有和我講一句話。嚶嚶嚶~看起來似乎有兩下子。不過無所謂,這些都不重要。

什麼是索引?

面試官:我看你項目中有做過 SQL 優化,那我們今天就來聊聊索引吧。

(索引能問些啥,無非是索引的概念、索引的使用規則、索引的分類、索引的原理。嘻嘻~我早有準備)

 

我:數據庫中的索引,簡單來說吶,就好比一本書的目錄,它可以幫我們快速進行特定值的定位與查找,從而加快數據查詢的效率。

如果我們不使用索引,就必須從第 1 條記錄開始依次往後查找,直到把所有的數據表都查找完,才能找到想要的數據。

面試官:那照你這麼說,索引是不是越多越好?

索引是不是越多越好?

我:當然索引也不是越多越好,索引也不是萬能的,在有些情況下使用索引反而會讓效率變低。

索引的價值是幫我們從海量數據中找到想要的數據,如果數據量少,那麼是否使用索引對結果的影響並不大。

在數據表中的數據行數比較少的情況下,比如不到 1000 行,是不需要創建索引的。另外,當數據重複度大,比如高於 10% 的時候,也不需要對這個字段使用索引。

比如,如果是性別這個字段,就不需要對它創建索引。這是爲什麼呢?如果你想要在 100 萬行數據中查找其中的 50 萬行(比如性別爲男的數據),一旦創建了索引,你需要先訪問 50 萬次索引,然後再訪問 50 萬次數據表,這樣加起來的開銷比不使用索引可能還要大。

索引的種類

面試官點點頭,看起來對我以上的回答還算滿意。接着又問:

面試官:那你說說索引有哪些種類?

(嘻嘻,對於索引的種類我太熟了。但我還是稍稍頓了頓開始了我的回答。)

 

按照功能邏輯分類

我:從功能邏輯上說,索引主要有 4 種,分別是普通索引、唯一索引、主鍵索引和全文索引。

普通索引是基礎的索引,沒有任何約束,主要用於提高查詢效率。

唯一索引就是在普通索引的基礎上增加了數據唯一性的約束,在一張數據表裏可以有多個唯一索引。

主鍵索引在唯一索引的基礎上增加了不爲空的約束,也就是 NOT NULL+UNIQUE,一張表裏最多隻有一個主鍵索引。

全文索引用的不多,MySQL 自帶的全文索引只支持英文。我們通常可以採用專門的全文搜索引擎,比如 ES(ElasticSearch) 和 Solr。

其實前三種索引(普通索引、唯一索引和主鍵索引)都是一類索引,只不過對數據的約束性逐漸提升。

在一張數據表中只能有一個主鍵索引,這是由主鍵索引的物理實現方式決定的,因爲數據存儲在文件中只能按照一種順序進行存儲。但可以有多個普通索引或者多個唯一索引。

 

按照物理實現方式分類

我:按照物理實現方式,索引可以分爲 2 種:聚集索引和非聚集索引。我們也把非聚集索引稱爲二級索引或者輔助索引。

聚集索引可以按照主鍵來排序存儲數據,這樣在查找行的時候非常有效。

舉個例子,如果是一本漢語字典,我們想要查找「數」這個字,直接在書中找漢語拼音的位置即可,也就是拼音「shu」。這樣找到了索引的位置,在它後面就是我們想要找的數據行。

非聚集索引不會把索引指向的內容像聚集索引一樣直接放到索引的後面,而是維護單獨的索引表(只維護索引,不維護索引指向的數據),爲數據檢索提供方便。

我們還以漢語字典爲例,如果想要查找「數」字,那麼按照部首查找的方式,先找到「數」字的偏旁部首,然後這個目錄會告訴我們「數」字存放到第多少頁,我們再去指定的頁碼找這個字。

也就是說系統會進行兩次查找,第一次先找到索引,第二次找到索引對應的位置取出數據行。

聚集索引和非聚集索引二者的區別

其實回答到上面已經可以了,但是爲了展示自己理解的透徹,我還做了以下闡述:

我:聚集索引與非聚集索引的原理不同,在使用上也有一些區別:

  1. 聚集索引的葉子節點存儲的就是我們的數據記錄,非聚集索引的葉子節點存儲的是數據位置。非聚集索引不會影響數據表的物理存儲順序。
  2. 一個表只能有一個聚集索引,因爲只能有一種排序存儲的方式,但可以有多個非聚集索引,也就是多個索引目錄提供數據檢索。
  3. 使用聚集索引的時候,數據的查詢效率高,但如果對數據進行插入,刪除,更新等操作,效率會比非聚集索引低。

索引的數據結構

面試官:你剛纔從功能邏輯和物理實現的方式闡述了索引的分類,看來對索引的數據結構是有了解的,說一說你知道的索引數據結構就有哪些。

(這個簡單啊,我脫口而出)

我:Hash、B 樹和 B+ 樹都可以作爲索引的數據結構,但是在 MySQL 中採用的是 B+ 樹,B+ 樹也是我們常用的索引數據結構。

爲什麼我們常用 B+ 樹作爲索引的數據結構?

面試官:爲什麼我們常用 B+ 樹作爲索引的數據結構?其它樹形結構不香嗎?

(我就知道沒那麼簡單。唉,我剛纔爲什麼要提「常用」倆字呢?我內心哭笑不得,但還是強作鎮定。)

 

我:在回答這個問題之前我先說一下索引的存放位置,以及索引的數據結構設計好壞的評判標準。

索引的存放位置

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

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

我:雖然內存的讀取速度很快,但我們還是需要將索引存放到硬盤上。因此,當我們在硬盤上進行查詢時,也就產生了硬盤的 I/O 操作。

我們都知道,硬盤的 I/O 存取消耗的時間相比於內存的存取來說,要高很多。我們通過索引來查找某行數據的時候,需要計算產生的磁盤 I/O 次數,當磁盤 I/O 次數越多,所消耗的時間也就越大。

如果我們能讓索引的數據結構儘量減少硬盤的 I/O 操作,所消耗的時間也就越小,那麼這個索引的數據結構設計的也就越優。

二叉樹

面試官點點頭示意我繼續說下去,爲了對「爲什麼我們常用 B+ 樹作爲索引的數據結構」這個問題進行一個小白都能看懂的滿意回答,我拿起了筆,圖文並茂的從二叉樹開始和麪試官扯了起來。

 

我:接下來說說二叉樹,我們知道二分查找法是一種高效的數據檢索方式,時間複雜度爲 O(log2n),可以說檢索速度是很快了。

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

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

舉個例子,我們對數列(25,18,36,9,20,32,41)創造出來的二分查找樹如下圖所示:

 

但是存在特殊的情況,二叉樹的深度會非常大。比如我們給出的數據順序是 (9, 18, 20, 25, 32, 36, 41),創造出來的二分搜索樹如下圖所示:

 

現在這棵樹也屬於二分查找樹,但是性能上已經退化成了一條鏈表,查找數據的時間複雜度變成了 O(n)。

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

平衡二叉搜索樹

面試官:既然普通的二叉樹不行,那平衡二叉搜索樹怎麼樣?因爲我們知道它可以通過旋轉的方式避免數據結構在特殊情況下退化成鏈表。

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

 

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

什麼是 B 樹?

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

 

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

如果用二叉樹作爲索引的實現結構,會讓樹變得很高,增加硬盤的 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+ 樹是基於 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 所對應的數據。

面試官:B+ 樹整個過程一共進行了 3 次 I/O 操作,看起來 B+ 樹和 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 次數更少,更適合進行關鍵字的範圍查詢。

面試官拿起旁邊已經涼透的咖啡,喝了一口。

(這小姑娘有點東西呀)

「持續更新……」

 

愛心三連

最後,感謝各位的閱讀。文章的目的是記錄和分享,若文中出現明顯紕漏也歡迎指出,我們一起在探討中學習。不勝感激 !

作者:馬尾 鏈接:https://juejin.im/post/5ee99394f265da76b559c3f5