一步步分析爲什麼B+樹適合作爲索引的結構

前言

本文是在講述什麼樣的數據結構適合作爲索引,以及其適合作爲索引的原因。而閱讀本文需要對B樹和B+樹結構有稍微的理解。以及需要對磁盤操作知識有稍微的瞭解。對於磁盤操作的相關知識,在文章尾部的鏈接文章中,有詳細的介紹。

在MySQL中,主要有四種類型的索引,分別爲:B-Tree索引,Hash索引,Fulltext索引和R-Tree索引,本文講的是B-Tree索引。

什麼是索引

索引(Index)是幫助數據庫高效獲取數據的數據結構。索引是在基於數據庫表創建的,它包含一個表中某些列的值以及記錄對應的地址,並且把這些值存儲在一個數據結構中。最常見的就是使用哈希表、B+樹作爲索引。

爲什麼要使用索引

我們知道,數據庫查詢是數據庫最主要的功能之一。而查詢速度當然是越快越好。而當數據量越來越大的時候,查詢花費的時間會隨之增長。而索引,可以加速數據的查詢。因爲索引是有序排列的。

舉個例子來說,假設我們有一個數據庫表Employee,這個表分別有三個字段:name,age,address。假設表中有1000條記錄。

假如沒有使用索引,當我們查詢名爲“Jesus”的僱員的時候,即調用:

select name,age,address from Employee where name = 'Jesus';
  • 1

此時數據庫不得不在Employee表中對這1000條記錄一條一條的進行判斷name字段是否爲“Jesus”。這也就是所謂的全表掃描。

而當我們在Employee表上的name字段上創建索引時,當我們查詢名爲“Jesus”的僱員時,會通過索引查找去查詢名爲“Jesus”的僱員,因爲該索引已經按照字母順序排列,因此要查找名爲“Jesus”的記錄時會快很多,因爲名字首字母爲“J”的僱員都是排列在一起的。通過該索引,能獲取到表中對應的記錄。

舉例說明使用索引的好處

假設索引(索引是一種數據結構)是鏈表結構。每個節點存儲的是關鍵字字段(這個例子中對應的是name屬性)以及該關鍵字字段在數據庫表的對應的記錄的地址。而這些節點是根據name屬性排序的(即根據字母順序排序)。因此,當我們執行上面說的查找名爲“Jesus”的sql語句時,數據庫會通過該索引來查詢,因爲該鏈表是有序排列的,在我們找到第一個name屬性爲“Jesus”的節點後,繼續往後找,當遇到name屬性不爲“Jesus”的節點時,就無需再往後查找了,因爲節點是根據name屬性有序排列的啊。假設第一個name=“Jesus”的節點是第499個節點,最後一個name=“Jesus”的節點是第500個節點,那麼只需要遍歷501個節點就可以了。當發現第501個節點的name字段不爲“Jesus”,後面的499個節點也就無需遍歷了。通過索引,我們就找到了name爲“Jesus”的節點,而通過該節點的另一個屬性(關鍵字字段在數據庫表的對應的記錄的地址),我們就能獲取到Employee表中滿足條件name=“Jesus”的記錄了。

通過使用索引,查詢判斷的次數就從1000次縮小到了501次了。起到了加速了查詢效率。但實際上數據庫中索引的結構,並不是鏈表結構。

數據庫中使用什麼數據結構作爲索引

數據庫中實際使用的索引並不會是鏈表結構,因爲效率太低了。 
我們知道鏈表的查詢效率是O(n)。就像上面的例子,遍歷了501次才找到第一條符合條件的記錄,這是很低效的。而我們知道,數組+二分查找的效率是O(lgn),但是數組的插入元素以及刪除元素的效率很低,因此使用數組做爲索引結構並不合適。

另外,在選擇數據庫索引的結構的時候,要考慮到另一個問題。索引是存在於磁盤中,當索引非常大的時候,達到幾個G的時候,無法一次加載到內存中。

考慮到上面兩個因素,數據庫中索引使用的是樹形結構。

各種樹的名字

有這麼幾種樹:

B-Tree
B+-Tree
B*-Tree
  • 1
  • 2
  • 3

首先要明白三種樹名中的“-”起到的是分隔的作用,並不是“減”的意思。 
因此正確的翻譯應該是B樹,B+樹,B*樹。而不是B-樹,B+樹,B*樹。因此,當你聽到別人說“B減樹”的時候,要明白它指的是B-Tree。即B樹和B-樹是同一種樹。

爲什麼要強調上面這一點呢,因爲有的博文中寫的是:B樹是二叉樹,B-樹是多路搜索樹。

然而B樹和B-樹都是指B-Tree。引用維基百科上的話:

B-tree 
Not to be confused with Binary tree.

也就是輸,B-Tree並不是Binart tree。B-Tree的中文名是平衡多路搜索樹。 
(B樹的相關介紹在下面)

平衡二叉樹

樹形結構是計算機系統裏最重要的數據結構。

我們知道,二叉樹的查找的時間複雜度是O(log2N),其查找效率與深度有關,而普通的二叉樹可能由於內部節點排列問題退化成鏈表,這樣查找效率就會很低。因此平衡二叉樹是更好的選擇,因爲它保持平衡,即通過旋轉調整結構保持最小的深度。其查找的時間複雜度也是O(log2N)。

但實際上,數據庫中索引的結構也並非AVL樹或更優秀的紅黑樹,儘管它的查詢的時間複雜度很低。

爲什麼平衡二叉樹也不適合作爲索引

之前說了平衡樹的查找時間複雜度是O(log2N),已經很不錯了,但還是不適合作爲索引結構。那麼肯定是有一種更適合作爲索引的數據結構。那麼這個更適合作爲索引的數據結構,難道是查找的時間複雜度更低嗎?並不是。這種作爲索引的數據結構的查找的時間複雜度也近似O(log2N)。

那爲什麼平衡二叉樹不適合作爲索引呢?

索引是存在於索引文件中,是存在於磁盤中的。因爲索引通常是很大的,因此無法一次將全部索引加載到內存當中,因此每次只能從磁盤中讀取一個磁盤頁的數據到內存中。而這個磁盤的讀取的速度較內存中的讀取速度而言是差了好幾個級別。

注意,我們說的平衡二叉樹結構,指的是邏輯結構上的平衡二叉樹,其物理實現是數組。然後由於在邏輯結構上相近的節點在物理結構上可能會差很遠。因此,每次讀取的磁盤頁的數據中有許多是用不上的。因此,查找過程中要進行許多次的磁盤讀取操作。

而適合作爲索引的結構應該是儘可能少的執行磁盤IO操作,因爲執行磁盤IO操作非常的耗時。因此,平衡二叉樹並不適合作爲索引結構。

B-Tree適合作爲索引

平衡二叉樹不適合作爲索引。那麼什麼才適合作爲索引——B樹。

平衡二叉樹沒能充分利用磁盤預讀功能,而B樹是爲了充分利用磁盤預讀功能來而創建的一種數據結構,也就是說B樹就是爲了作爲索引才被髮明出來的的。

來看看關於“局部性原理與磁盤預讀”的知識:

局部性原理與磁盤預讀:

由於存儲介質的特性,磁盤本身存取就比主存慢很多,再加上機械運動耗費,磁盤的存取速度往往是主存的幾百分分之一,因此爲了提高效率,要儘量減少磁盤I/O。爲了達到這個目的,磁盤往往不是嚴格按需讀取,而是每次都會預讀,即使只需要一個字節,磁盤也會從這個位置開始,順序向後讀取一定長度的數據放入內存。這樣做的理論依據是計算機科學中著名的局部性原理: 
當一個數據被用到時,其附近的數據也通常會馬上被使用。 
程序運行期間所需要的數據通常比較集中。 
由於磁盤順序讀取的效率很高(不需要尋道時間,只需很少的旋轉時間),因此對於具有局部性的程序來說,預讀可以提高I/O效率。

搞清楚上面的意思。磁盤預讀是具體實現,其理論依據是局部性原理。

爲什麼說紅黑樹沒能充分利用磁盤預讀功能,引用一篇博文的一段話:

紅黑樹這種結構,h明顯要深的多。由於邏輯上很近的節點(父子)物理上可能很遠,無法利用局部性,所以紅黑樹的I/O漸進複雜度也爲O(h),效率明顯比B-Tree差很多。

也就是說,使用紅黑樹(平衡二叉樹)結構的話,每次磁盤預讀中的很多數據是用不上的數據。因此,它沒能利用好磁盤預讀的提供的數據。然後又由於深度大(較B樹而言),所以進行的磁盤IO操作更多。

B樹的每個節點可以存儲多個關鍵字,它將節點大小設置爲磁盤頁的大小,充分利用了磁盤預讀的功能。每次讀取磁盤頁時就會讀取一整個節點。也正因每個節點存儲着非常多個關鍵字,樹的深度就會非常的小。進而要執行的磁盤讀取操作次數就會非常少,更多的是在內存中對讀取進來的數據進行查找。

B樹的查詢,主要發生在內存中,而平衡二叉樹的查詢,則是發生在磁盤讀取中。因此,雖然B樹查詢查詢的次數不比平衡二叉樹的次數少,但是相比起磁盤IO速度,內存中比較的耗時就可以忽略不計了。因此,B樹更適合作爲索引。

比B樹更適合作爲索引的結構——B+樹

比B樹更適合作爲索引的結構是B+樹。MySQL中也是使用B+樹作爲索引。它是B樹的變種,因此是基於B樹來改進的。爲什麼B+樹會比B樹更加優秀呢?

B樹:有序數組+平衡多叉樹; 
B+樹:有序數組鏈表+平衡多叉樹;

B+樹的關鍵字全部存放在葉子節點中,非葉子節點用來做索引,而葉子節點中有一個指針指向一下個葉子節點。做這個優化的目的是爲了提高區間訪問的性能。而正是這個特性決定了B+樹更適合用來存儲外部數據。

引用一段話:

走進搜索引擎的作者樑斌老師針對B樹、B+樹給出了他的意見(爲了真實性,特引用其原話,未作任何改動): “B+樹還有一個最大的好處,方便掃庫,B樹必須用中序遍歷的方法按序掃庫,而B+樹直接從葉子結點挨個掃一遍就完了,B+樹支持range-query非常方便,而B樹不支持。這是數據庫選用B+樹的最主要原因。 
比如要查 5-10之間的,B+樹一把到5這個標記,再一把到10,然後串起來就行了,B樹就非常麻煩。B樹的好處,就是成功查詢特別有利,因爲樹的高度總體要比B+樹矮。不成功的情況下,B樹也比B+樹稍稍佔一點點便宜。 
B樹比如你的例子中查,17的話,一把就得到結果了, 
有很多基於頻率的搜索是選用B樹,越頻繁query的結點越往根上走,前提是需要對query做統計,而且要對key做一些變化。 
另外B樹也好B+樹也好,根或者上面幾層因爲被反覆query,所以這幾塊基本都在內存中,不會出現讀磁盤IO,一般已啓動的時候,就會主動換入內存。”

舉個例子來對比。 
B樹: 

該圖以及下一張圖均來自博文《淺談算法和數據結構: 十 平衡查找樹之B樹》,鏈接文章最後。

比如說,我們要查找關鍵字範圍在3到7的關鍵字,在找到第一個符合條件的數字3後,訪問完第一個關鍵字所在的塊後,得遍歷這個B樹,獲取下一個塊,直到遇到一個不符合條件的關鍵字。遍歷的過程是比較複雜的。

B+樹: 

相比之下,B+樹的基於範圍的查詢簡潔很多。由於葉子節點有指向下一個葉子節點的指針,因此從塊1到塊2的訪問,通過塊1指向塊2的指針即可。從塊2到塊3也是通過一個指針即可。

引用一篇博文中網友評論的一段話:

數據庫索引採用B+樹的主要原因是B樹在提高了磁盤IO性能的同時並沒有解決元素遍歷的效率低下的問題。正是爲了解決這個問題,B+樹應運而生。B+樹只要遍歷葉子節點就可以實現整棵樹的遍歷。而且在數據庫中基於範圍的查詢是非常頻繁的,而B樹不支持這樣的操作(或者說效率太低)。

正如上面所說,在數據庫中基於範圍的查詢是非常頻繁的,因此MySQL最終選擇的索引結構是B+樹而不是B樹。

最後

之前對索引幾乎沒什麼理解,只知道索引能加速查詢,但至於爲什麼,完全不懂。現在寫出這篇文章,對MySQL的索引的理解自然是深刻了很多。在這篇文章中我沒有具體的講B樹和B+樹的定義的結構,但是對B樹和B+樹的理解卻是前提。我也是一邊看其他關於索引的博客一邊看B樹、B+樹的介紹來學習。 
這篇文章,參考了許多文章,而最主要的,也是講解的最後的,是下面幾篇文章。

參考文章

1.MySQL索引背後的數據結構及算法原理(超讚的文章,2011年寫的文章,厲害……) 
2.從B樹、B+樹、B*樹談到R 樹(這篇文章作者也是好厲害,其博客訪問量達千萬) 
3.淺談算法和數據結構: 十 平衡查找樹之B樹,而這篇博文裏有B樹和B+樹插入元素的過程GIF圖,超讚,有助於對B樹和B+樹的理解!

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