1. 程式人生 > >一步步分析為什麼B+樹適合作為索引的結構

一步步分析為什麼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';

此時資料庫不得不在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

首先要明白三種樹名中的“-”起到的是分隔的作用,並不是“減”的意思。
因此正確的翻譯應該是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+樹的理解!