一、前言
這幾天在研究MySQL
相關的內容,而MySQL
中比較重要的一個內容就是索引。對MySQL
索引有了解的應該都知道,B+樹是MySQL
索引實現的一個主要的數據結構。今天這篇博客就來簡單介紹一下B樹、B+樹以及MySQL
索引使用這種數據結構實現的原因。
二、正文
2.1 B樹
關於B樹的操作細節我這里就不詳細介紹了,這里主要介紹一下B樹的結構,讓大家對B樹有一個大致的了解。
這里首先要糾正一個問題,網上大量文章將B樹稱為B-(減)樹,但其實這是一種錯誤的叫法。會這么叫是因為B樹的英文名稱為“B-Tree”,錯誤的翻譯導致這種錯誤的叫法逐漸傳開。但實際上這里的“-”是杠,而不是減,由於存在B+樹,所以大家都覺得存在一個B-(減)樹是很正常的,但實際上,B+樹的英文完整寫法應該是B+-Tree,“+”是加,但“-”是杠。除此之外,甚至有人認為B樹就是二叉搜索樹(Binary Search Tree,簡稱BST),這就錯的更加離譜了。
B樹是一棵多路平衡查找樹,相信很多人都了解過二叉搜索樹,而B樹和二叉搜索樹類似,只是B樹的每一個節點可以有超過兩個子節點。而B樹中,每一個節點具體可以有幾個子節點,這與這棵B樹的階有關,而樹的階一般用字母m表示。拋開B樹的維護操作不談,B樹可以簡單理解為一棵m叉搜索樹。
我們定義,一個樹中,每個節點允許的子節點的最大數量,就稱為這個數的階,一般用字母m表示。例如:假設一棵5階的B數,則B數上的每一個節點,最多只能有5個子節點。除此之外需要注意,B樹的階m > 2。
下面我們直接通過一張圖來了解B樹的結構:
上面這張圖就是一棵標准的B樹,他的每一個節點中可能存有多個值(每個節點記錄了值的個數,也就是上圖中的紅色數字),以及多個指向子節點的指針,而值的個數 = 指針的個數 - 1 = m - 1。比如說上圖中,根節點存有一個值50
(圖中稱為關鍵字),而根節點的左子節點存有兩個值,即10
和30
。而由於根節點只有一個值,所以他有兩個指向子節點的指針,從上圖可以看出,這兩個指針分別位於值的兩邊。我們之前說過,B樹可以近似的認為是一棵m叉搜索樹,所以上圖中,根節點的左子樹中的所有值都小於根節點的值50
,而右邊子樹中所有節點的值大於根節點的值50
。
根節點只有一個值,以及兩個子節點,和二叉樹類似,所以以它舉例不夠典型,我們現在以根節點的左子節點再次舉例。根節點的左子節點中存有兩個值,即10和30,且他有3個指向子節點的指針。在每一個節點中,多個值是排好序的,比如上圖中是從小到大排序,於是10在30的左邊。對於10左邊這個指針指向的子樹,包含的值都小於10;而位於10和30之間的指針指向的子樹,包含的值一定時10到30之間;而30右邊的指針,指向的子樹的子節點一定是大於30。
我們現在以一個例子來說明B樹查找的過程,假設上圖中,我們想要搜索值35
,於是需要經歷以下步驟:
- 用
35
與根節點中的值比較,根節點中只有一個50
,35<50
,於是向根節點的左子節點搜索; - 根節點的左子節點中的第一個值是
10
,35>10
,於是判斷下一個值,下一個值為30
,35>30
,繼續判斷下一個值,但是此節點中沒有下一個值了,於是向35
右邊的這個指針指向的子節點查找,也就是第三個葉子節點; - 到了葉子節點后,發現其中只有一個值,就是
35
,於是查找成功;
以上就是在B樹中查找一個值的過程。
2.2 為什么需要B樹?
在B樹的每一個節點中,都不止存儲一個值,具體存儲的值的個數依賴於B樹的階。而我們在查找一個值的過程中,需要對當前所在的節點包含的所有值進行一個遍歷,以此來確定當前查找的值是否在當前節點中。這也就意味着,相比於二叉搜索樹,平衡二叉樹,紅黑樹等數據結構,B樹查找一個值需要比較更多的次數。假設一棵B樹的階是100
,那也就意味着在最壞情況下,我們在每一個訪問到的節點中,都需要比較100
次,而前面提到的三種數據結構比較的次數不會超過樹的深度,也就是只需要少量的比較次數。既然B樹相比較於它們需要比較更多的次數才能找到相應的值,那為什么還要B樹呢?這取決於實際的應用場景。
說到B樹,大部分首先想到的就是數據庫的索引,MySQL
中使用的索引主要為BTree索引
(實際是B+樹實現的,這個后面再談)。從上面的B樹結構我們可以看到,B樹中使用大量的指針維護節點之間的關系,這也就意味着B樹在物理存儲上並不是連續的。單個節點中的數據是連續存儲,但是多個節點之間一般都是單獨存儲,然后通過指針相互引用。在實際的存儲中,BTree索引
一般被存放在磁盤中,然后只有需要使用時,才將使用到的部分節點加載進內存,進行比較判斷。
為什么不一次性將BTree索引全部加載進內存?因為在實際生產中,索引往往需要維護百萬甚至千萬行數據,這就導致索引本身占據大量的內存,再加上我們使用的常常不止一個索引,再加上內存中需要運行其他程序,所以將索引一次性加載進內存是不現實的事情。只有當前需要使用的部分才被加載進內存,而不使用的部分則留在磁盤或者從內存中移除。
而上面這種使用到才進行加載的方式有一個什么問題?我們每次需要查找樹中的一個節點,都需要進行一次磁盤IO
,將這個節點從磁盤中加載進內存。而對於B樹或者說之前提到的平衡二叉樹等數據結構,最多需要訪問的節點的個數,實際上就是樹的深度(想想搜索一個值的過程就能明白)。對於B樹來說,他每一個節點可以存儲多個值,而平衡二叉樹等二叉結構,每一個節點只存儲一個值,這也就意味着在值的個數相等的情況下,B樹的深度小於二叉樹的深度。這也就意味着以B樹作為索引,可以進行更少次數的磁盤IO
。
對於一棵含有n個元素的樹,二叉搜索樹的深度在n-log2(n)之間,而平衡二叉樹的深度是log2(n),紅黑樹與平衡二叉樹類似,平均深度也是log2(n)。然而,B樹設計了一種高效簡單的維護操作,使B樹的深度維持在約log(ceil(m/2))(n)~logm(n)之間,大大降低樹高(ceil是向上取整函數,例如5/2 = 3)。
這里面臨一個什么問題?磁盤的速度,相對於內存來說非常的緩慢,磁盤查找的速度比內存查找慢100000倍左右。也就是說,從磁盤中找到1
個數據所花費的時間,可以從內存中查找100000
個數據。這也就意味着我們在使用索引查找數據的過程中,時間主要是花費在了磁盤IO
上,而不是數據的比較上。所以,我們希望盡可能少地進行磁盤IO
。而B樹作為索引,由於樹的深度較小,相比於那些二叉樹,可以進行更少的磁盤IO
,這就是B樹最大的優勢。
二叉樹中,一個節點一般只存儲一個元素,而在將磁盤的數據加載進內存中時,實際上是按頁進行加載的,頁是每次從磁盤加載進內存的數據的最小單位,一般為4K
。這也就意味着我們使用這些二叉樹的數據結構時,加載一個節點所在的頁進入內存時,這個頁中有大量內存都是浪費的。而B樹中每一個節點可以存儲多個數據,於是我們可以通過修改B樹的階,讓他的每一個節點大致占用一個頁(4K)的大小,以此最大限度的減少B樹的深度,提高內存利用率。
2.3 B樹存在的問題
(1)難以存儲具體數據
我們上面在介紹B樹的過程中,對於B樹中存儲的元素,都是用“值”這個字來說明,但是在上面那張圖中可以看到,圖中寫的是關鍵字,這是因為我們在實際使用中,需要存儲的是key-value
型數據。比如說作為數據庫索引,我們需要通過索引值進行查找,索引值就是key
,但是我們真正需要的是索引值對應的數據行,也就是value
。
有一個比較簡單的解決方案,我們可以在B樹的節點中直接存儲key
和value
,這樣在通過key
找到元素是,可以直接取出value
。但是,這會導致另外一個問題。我們上面說過,B樹的一個節點,其大小一般被限制在一個磁盤頁的大小(4K),如果我們在一個節點中既存key
,又存value
,就會導致一個節點中能夠存儲的元素數量減少,value
越大,能夠存儲的元素就越少,於是樹的深度就會增加,違背了我們使用B樹減少磁盤IO
的目的,所以這種方法並不可取。當然,其實也可以讓value
保存數據的地址,但是可能是需要綜合考慮下面這個問題,所有沒有這樣做。
(2)B樹不適合用來處理范圍查詢
在數據庫中,進行范圍查找的頻率非常的高,比如查找員工工資在1000-2000
中的全部員工這種類型的查詢。但是,B樹並不適合用來進行這種范圍查找,因為在B樹中,每一個節點都用來存儲數據,它們之間並不是線性結構,不方便進行范圍查詢。想要在B樹中進行范圍查詢,可以先找到范圍的上界和下界,在通過DFS(或者BFS),遍歷包含下界到上界中的所有節點,但這並不方便。
B+樹正是針對以上兩個問題,而對B樹做了一些修改而得來。
2.4 B+樹
B+樹相對於B樹主要做了如下的修改:
- B+樹中的每一個非葉子節點並不存儲值
value
,只存儲鍵key
,而具體的value
全部存放在葉子節點中,這也就意味着每次查找需要訪問的節點數量都是固定的,都需要向下查找到葉子節點; - 每一個葉子節點都有一個指向下一個葉子節點的指針,所有的葉子節點相互串聯,組成一個線性的結構;
- 對於一個
m
階的B+樹,每一個節點最多只有m個子節點,同時存儲m
個key
(對於m
階的B樹,只有m-1
個key); - 每一個子節點中最小(或最大)的
key
,也包含在父節點中(這個通過下面的例子理解);
下面我們通過一張圖來看看B+樹的結構:
上圖中,就是一棵B+樹。根節點存儲了3
個key
,key
值分別是5,28,65
,且是按照從小到達的順序存儲。同時根節點包含3
個指針,指向自己的3
個子節點。第一個子節點中最小的key
是5
,就是根節點中最小的key
,而這個節點中所有的key
,大小都是在根節點的第一個key(5)
到第二個key(28)
之間(不包含28
);第二個子節點中最小的key
是28
,也就是根節點中的第二個key
,在這個子節點中所有的key
,大小都在根節點第二個key(28)
到第三個key(65)
之間;第三個子節點同理,它包含根節點的第三個key(65)
,同時其中所有的key
都>=65
。再往下的子節點也是同理。
而根據上圖我們可以看到,在最下層的葉子節點中,存儲了全部的key
值(盡管有些key
已經在上層節點出現過),同時不僅存儲了key
值,還存儲了這些key
值對應的value
。除此之外,每一個葉子節點都包含一個指針,指向下一個葉子節點。這些葉子節點相互串聯,組成了一個key
值從小到大排好序的線性結構。
這樣處理有什么好處呢?好處就是我們在樹中存儲了value
,但是由於value
存儲在葉子節點中,所以對於作為索引的非葉子節點來說,並沒有增加它們的大小,從而並沒有導致樹的高度增加。除此之外,由於value
都存儲在葉子節點中,並且葉子節點相互串聯,所以非常方便進行范圍查詢。比如說上圖,我們要查找key
為26-60
對應的數據,那我們首先查找26
所在的葉子節點,發現它在第三個葉子節點,於是我們將第三個葉子節點讀入內存,然后發現並不包含全部數據,於是通過指針找到第四個葉子節點,將第四個葉子節點讀入內存,還不包含全部,於是將第五個葉子節點也讀入,這時就包含全部數據了。
2.5 InnoDB和MyISAM中索引的實現
在MySQL5.1
之前,MySQL
的默認存儲引擎是MyISAM
,而在這之后改為了InnoDB
。這兩個存儲引擎中,都使用了B+Tree
實現索引,但是實現的方式有所區別。
(1)InnoDB中的聚簇索引
什么是聚簇索引,就是指索引與數據庫表中的數據存儲在一起。InnoDB
使用B+樹實現聚簇索引,而數據庫表中的數據行,實際上就是存儲在B+樹的葉子節點中,我們上面說的B+樹中的key-value
,其中的value
指的就是具體的一行數據,我們在葉子節點中找到了key
,實際上也就得到了key對應的那一行數據。所以嚴格來講,聚簇索引不單單是索引,更是數據的一種存儲結構。
InnoDB
使用表的主鍵作為key
,建立聚簇索引,如果表沒有主鍵,將選擇一個唯一的非空索引替代,若也沒有,將隱式的定義一個主鍵用來建立。
(2)MyISAM的非聚簇索引
在MyISAM
中,並不使用聚簇索引,也就是說MyISAM
中通過B+樹實現的索引,並不包含表中具體的數據行,子節點中的value
,存儲的是這一行數據的地址,也就是說數據和索引分開存儲。
三、總結
在MySQL
的實際應用中,BTree索引
都是通過B+樹建立的,而不是B樹。而在InnoDB
中,使用的是聚簇索引,在B+樹的葉子節點中直接存儲表中的數據行;而MyISAM
沒有使用聚簇索引,B+樹的葉子節點中,存放的是數據行的地址。