一文徹底搞懂MySQL基礎:B樹和B+樹的區別

寫在前面

大家在面試的時候,肯定都會被問到MySql的知識,以下是面試場景:
面試官:對於MySQL,你對他索引原理了解嗎?
我:瞭解
面試官:MySQL的索引是用什麼數據機構的?
我:B+樹
面試官:爲什麼要用B+樹,而不是B樹?
我:…
面試官:用B+樹作爲MySql的索引結構,用什麼好處?
我:…

B樹和B+樹是MySQL索引使用的數據結構,對於索引優化和原理理解都非常重要,下面我的寫文章就是要把B樹,B+樹的神祕面紗揭開,讓大家在面試的時候碰到這個知識點一往無前,不再成爲你的知識盲點!

歡迎關注公衆號:「碼農富哥」,致力於分享後端技術 (高併發架構,分佈式集羣系統,消息隊列中間件,網絡,微服務,Linux, TCP/IP, HTTP, MySQL, Redis), Python 等 原創乾貨面試指南! 讓大家在編程路上少走彎路!

B-樹

B-樹概述

B-樹,這裏的 B 表示 balance( 平衡的意思),B-樹是一種多路自平衡的搜索樹(B樹是一顆多路平衡查找樹
它類似普通的平衡二叉樹,不同的一點是B-樹允許每個節點有更多的子節點。下圖是 B-樹的簡化圖.

B 樹

B-樹有如下特點:

  1. 所有鍵值分佈在整顆樹中(索引值和具體data都在每個節點裏);
  2. 任何一個關鍵字出現且只出現在一個結點中;
  3. 搜索有可能在非葉子結點結束(最好情況O(1)就能找到數據);
  4. 在關鍵字全集內做一次查找,性能逼近二分查找;

B樹深入

B樹由來

定義:B-樹是一類樹,包括B-樹、B+樹、B*樹等,是一棵自平衡的搜索樹,它類似普通的平衡二叉樹,不同的一點是B-樹允許每個節點有更多的子節點。
B-樹是專門爲外部存儲器設計的,如磁盤,它對於讀取和寫入大塊數據有良好的性能,所以一般被用在文件系統及數據庫中。

定義只需要知道B-樹允許每個節點有更多的子節點即可(多叉樹)。子節點數量一般在上千,具體數量依賴外部存儲器的特性。

先來看看爲什麼會出現B-樹這類數據結構。

傳統用來搜索的平衡二叉樹有很多,如 AVL 樹,紅黑樹等。這些樹在一般情況下查詢性能非常好,但當數據非常大的時候它們就無能爲力了。原因當數據量非常大時,內存不夠用,大部分數據只能存放在磁盤上,只有需要的數據才加載到內存中。一般而言內存訪問的時間約爲 50 ns,而磁盤在 10 ms 左右。速度相差了近 5 個數量級,磁盤讀取時間遠遠超過了數據在內存中比較的時間。這說明程序大部分時間會阻塞在磁盤 IO 上。那麼我們如何提高程序性能?減少磁盤 IO 次數,像 AVL 樹,紅黑樹這類平衡二叉樹從設計上無法“迎合”磁盤。

平衡二叉樹

上圖是一顆簡單的平衡二叉樹,平衡二叉樹是通過旋轉來保持平衡的,而旋轉是對整棵樹的操作,若部分加載到內存中則無法完成旋轉操作。其次平衡二叉樹的高度相對較大爲 log n(底數爲2),這樣邏輯上很近的節點實際可能非常遠,無法很好的利用磁盤預讀(局部性原理),所以這類平衡二叉樹在數據庫和文件系統上的選擇就被 pass 了。

空間局部性原理:如果一個存儲器的某個位置被訪問,那麼將它附近的位置也會被訪問。

我們從“迎合”磁盤的角度來看看B-樹的設計。

索引的效率依賴與磁盤 IO 的次數,快速索引需要有效的減少磁盤 IO 次數,如何快速索引呢?索引的原理其實是不斷的縮小查找範圍,就如我們平時用字典查單詞一樣,先找首字母縮小範圍,再第二個字母等等。平衡二叉樹是每次將範圍分割爲兩個區間。爲了更快,B-樹每次將範圍分割爲多個區間,區間越多,定位數據越快越精確。那麼如果節點爲區間範圍,每個節點就較大了。所以新建節點時,直接申請頁大小的空間(磁盤存儲單位是按 block 分的,一般爲 512 Byte。磁盤 IO 一次讀取若干個 block,我們稱爲一頁,具體大小和操作系統有關,一般爲 4 k,8 k或 16 k),計算機內存分配是按頁對齊的,這樣就實現了一個節點只需要一次 IO。

B-樹

上圖是一棵簡化的B-樹,多叉的好處非常明顯,有效的降低了B-樹的高度,爲底數很大的 log n,底數大小與節點的子節點數目有關,一般一棵B-樹的高度在 3 層左右。層數低,每個節點區確定的範圍更精確,範圍縮小的速度越快(比二叉樹深層次的搜索肯定快很多)。上面說了一個節點需要進行一次 IO,那麼總 IO 的次數就縮減爲了 log n 次。B-樹的每個節點是 n 個有序的序列(a1,a2,a3…an),並將該節點的子節點分割成 n+1 個區間來進行索引(X1< a1, a2 < X2 < a3, … , an+1 < Xn < anXn+1 > an)。

點評:B樹的每個節點,都是存多個值的,不像二叉樹那樣,一個節點就一個值,B樹把每個節點都給了一點的範圍區間,區間更多的情況下,搜索也就更快了,比如:有1-100個數,二叉樹一次只能分兩個範圍,0-50和51-100,而B樹,分成4個範圍 1-25, 25-50,51-75,76-100一次就能篩選走四分之三的數據。所以作爲多叉樹的B樹是更快的

B-樹的查找

我們來看看B-樹的查找,假設每個節點有 n 個 key值,被分割爲 n+1 個區間,注意,每個 key 值緊跟着 data 域,這說明B-樹的 key 和 data 是聚合在一起的。一般而言,根節點都在內存中,B-樹以每個節點爲一次磁盤 IO,比如上圖中,若搜索 key 爲 25 節點的 data,首先在根節點進行二分查找(因爲 keys 有序,二分最快),判斷 key 25 小於 key 50,所以定位到最左側的節點,此時進行一次磁盤 IO,將該節點從磁盤讀入內存,接着繼續進行上述過程,直到找到該 key 爲止。

查找僞代碼:

Data* BTreeSearch(Root *node, Key key)
{
    Data* data;

    if(root == NULL)
        return NULL;
    data = BinarySearch(node);
    if(data->key == key)
    {
        return data;
    }else{
        node = ReadDisk(data->next);
        BTreeSearch(node, key);
    }
}

B+ 樹

B+樹概述

B+樹是B-樹的變體,也是一種多路搜索樹, 它與 B- 樹的不同之處在於:

  1. 所有關鍵字存儲在葉子節點出現,內部節點(非葉子節點並不存儲真正的 data)
  2. 爲所有葉子結點增加了一個鏈指針

簡化 B+樹 如下圖

B+樹 1

B+樹 2

因爲內節點並不存儲 data,所以一般B+樹的葉節點和內節點大小不同,而B-樹的每個節點大小一般是相同的,爲一頁。

爲了增加 區間訪問性,一般會對B+樹做一些優化。
如下圖帶順序訪問的B+樹。

帶順序訪問的B+樹

B-樹和B+樹的區別

1.B+樹內節點不存儲數據,所有 data 存儲在葉節點導致查詢時間複雜度固定爲 log n。而B-樹查詢時間複雜度不固定,與 key 在樹中的位置有關,最好爲O(1)。

如下所示B-樹/B+樹查詢節點 key 爲 50 的 data。

B-樹:
B-樹

從上圖可以看出,key 爲 50 的節點就在第一層,B-樹只需要一次磁盤 IO 即可完成查找。所以說B-樹的查詢最好時間複雜度是 O(1)。

B+樹:
B+樹

由於B+樹所有的 data 域都在根節點,所以查詢 key 爲 50的節點必須從根節點索引到葉節點,時間複雜度固定爲 O(log n)。

點評:B樹的由於每個節點都有key和data,所以查詢的時候可能不需要O(logn)的複雜度,甚至最好的情況是O(1)就可以找到數據,而B+樹由於只有葉子節點保存了data,所以必須經歷O(logn)複雜度才能找到數據

2. B+樹葉節點兩兩相連可大大增加區間訪問性,可使用在範圍查詢等,而B-樹每個節點 key 和 data 在一起,則無法區間查找。

B+樹
根據空間局部性原理:如果一個存儲器的某個位置被訪問,那麼將它附近的位置也會被訪問。

B+樹可以很好的利用局部性原理,若我們訪問節點 key爲 50,則 key 爲 55、60、62 的節點將來也可能被訪問,我們可以利用磁盤預讀原理提前將這些數據讀入內存,減少了磁盤 IO 的次數。
當然B+樹也能夠很好的完成範圍查詢。比如查詢 key 值在 50-70 之間的節點。

點評:由於B+樹的葉子節點的數據都是使用鏈表連接起來的,而且他們在磁盤裏是順序存儲的,所以當讀到某個值的時候,磁盤預讀原理就會提前把這些數據都讀進內存,使得範圍查詢和排序都很快

3.B+樹更適合外部存儲。由於內節點無 data 域,每個節點能索引的範圍更大更精確

這個很好理解,由於B-樹節點內部每個 key 都帶着 data 域,而B+樹節點只存儲 key 的副本,真實的 key 和 data 域都在葉子節點存儲。前面說過磁盤是分 block 的,一次磁盤 IO 會讀取若干個 block,具體和操作系統有關,那麼由於磁盤 IO 數據大小是固定的,在一次 IO 中,單個元素越小,量就越大這就意味着B+樹單次磁盤 IO 的信息量大於B-樹,從這點來看B+樹相對B-樹磁盤 IO 次數少。

點評:由於B樹的節點都存了key和data,而B+樹只有葉子節點存data,非葉子節點都只是索引值,沒有實際的數據,這就時B+樹在一次IO裏面,能讀出的索引值更多。從而減少查詢時候需要的IO次數!

B/B+樹

從上圖可以看出相同大小的區域,B-樹僅有 2 個 key,而B+樹有 3 個 key。

拓展:MySQL爲什麼使用B-Tree(B+Tree)&& 存儲知識

上文說過,紅黑樹等數據結構也可以用來實現索引,但是文件系統及數據庫系統普遍採用B-/+Tree作爲索引結構,這一節將結合計算機組成原理相關知識討論B-/+Tree作爲索引的理論基礎。

一般來說,索引本身也很大,不可能全部存儲在內存中,因此索引往往以索引文件的形式存儲的磁盤上。這樣的話,索引查找過程中就要產生磁盤I/O消耗,相對於內存存取,I/O存取的消耗要高几個數量級,所以評價一個數據結構作爲索引的優劣最重要的指標就是在查找過程中磁盤I/O操作次數的漸進複雜度。換句話說,索引的結構組織要儘量減少查找過程中磁盤I/O的存取次數。下面先介紹內存和磁盤存取原理,然後再結合這些原理分析B-/+Tree作爲索引的效率。

存儲數據最小單元

我們都知道計算機在存儲數據的時候,有最小存儲單元,這就好比我們今天進行現金的流通最小單位是一毛。

在計算機中磁盤存儲數據最小單元是扇區,一個扇區的大小是512字節,而文件系統(例如XFS/EXT4)他的最小單元是塊,一個塊的大小是4k

而對於我們的InnoDB存儲引擎也有自己的最小儲存單元——頁(Page),一個頁的大小是16K。

下面幾張圖可以幫你理解最小存儲單元:

文件系統中一個文件大小隻有1個字節,但不得不佔磁盤上4KB的空間。

磁盤扇區、文件系統、InnoDB存儲引擎都有各自的最小存儲單元。

image

在MySQL中我們的InnoDB頁的大小默認是16k,當然也可以通過參數設置:

image

數據表中的數據都是存儲在頁中的,所以一個頁中能存儲多少行數據呢?假設一行數據的大小是1k,那麼一個頁可以存放16行這樣的數據。

主存存取原理

目前計算機使用的主存基本都是隨機讀寫存儲器(RAM),現代RAM的結構和存取原理比較複雜,這裏本文拋卻具體差別,抽象出一個十分簡單的存取模型來說明RAM的工作原理。

image

從抽象角度看,主存是一系列的存儲單元組成的矩陣,每個存儲單元存儲固定大小的數據。每個存儲單元有唯一的地址,現代主存的編址規則比較複雜,這裏將其簡化成一個二維地址:通過一個行地址和一個列地址可以唯一定位到一個存儲單元。圖5展示了一個4 x 4的主存模型。

主存的存取過程如下:

當系統需要讀取主存時,則將地址信號放到地址總線上傳給主存,主存讀到地址信號後,解析信號並定位到指定存儲單元,然後將此存儲單元數據放到數據總線上,供其它部件讀取。

寫主存的過程類似,系統將要寫入單元地址和數據分別放在地址總線和數據總線上,主存讀取兩個總線的內容,做相應的寫操作。

這裏可以看出,主存存取的時間僅與存取次數呈線性關係,因爲不存在機械操作,兩次存取的數據的“距離”不會對時間有任何影響,例如,先取A0再取A1和先取A0再取D3的時間消耗是一樣的。

磁盤存取原理

上文說過,索引一般以文件形式存儲在磁盤上,索引檢索需要磁盤I/O操作。與主存不同,磁盤I/O存在機械運動耗費,因此磁盤I/O的時間消耗是巨大的。

圖6是磁盤的整體結構示意圖。

image

一個磁盤由大小相同且同軸的圓形盤片組成,磁盤可以轉動(各個磁盤必須同步轉動)。在磁盤的一側有磁頭支架,磁頭支架固定了一組磁頭,每個磁頭負責存取一個磁盤的內容。磁頭不能轉動,但是可以沿磁盤半徑方向運動(實際是斜切向運動),每個磁頭同一時刻也必須是同軸的,即從正上方向下看,所有磁頭任何時候都是重疊的(不過目前已經有多磁頭獨立技術,可不受此限制)。

圖7是磁盤結構的示意圖。

image

盤片被劃分成一系列同心環,圓心是盤片中心,每個同心環叫做一個磁道,所有半徑相同的磁道組成一個柱面。磁道被沿半徑線劃分成一個個小的段,每個段叫做一個扇區,每個扇區是磁盤的最小存儲單元。爲了簡單起見,我們下面假設磁盤只有一個盤片和一個磁頭。

當需要從磁盤讀取數據時,系統會將數據邏輯地址傳給磁盤,磁盤的控制電路按照尋址邏輯將邏輯地址翻譯成物理地址,即確定要讀的數據在哪個磁道,哪個扇區。爲了讀取這個扇區的數據,需要將磁頭放到這個扇區上方,爲了實現這一點,磁頭需要移動對準相應磁道,這個過程叫做尋道,所耗費時間叫做尋道時間,然後磁盤旋轉將目標扇區旋轉到磁頭下,這個過程耗費的時間叫做旋轉時間。

局部性原理與磁盤預讀

由於存儲介質的特性,磁盤本身存取就比主存慢很多,再加上機械運動耗費,磁盤的存取速度往往是主存的幾百分分之一,因此爲了提高效率,要儘量減少磁盤I/O。爲了達到這個目的,磁盤往往不是嚴格按需讀取,而是每次都會預讀,即使只需要一個字節,磁盤也會從這個位置開始,順序向後讀取一定長度的數據放入內存。這樣做的理論依據是計算機科學中著名的局部性原理:

當一個數據被用到時,其附近的數據也通常會馬上被使用。

程序運行期間所需要的數據通常比較集中。

由於磁盤順序讀取的效率很高(不需要尋道時間,只需很少的旋轉時間),因此對於具有局部性的程序來說,預讀可以提高I/O效率。

預讀的長度一般爲頁(page)的整倍數。頁是計算機管理存儲器的邏輯塊,硬件及操作系統往往將主存和磁盤存儲區分割爲連續的大小相等的塊,每個存儲塊稱爲一頁(在許多操作系統中,頁得大小通常爲4k),主存和磁盤以頁爲單位交換數據。當程序要讀取的數據不在主存中時,會觸發一個缺頁異常,此時系統會向磁盤發出讀盤信號,磁盤會找到數據的起始位置並向後連續讀取一頁或幾頁載入內存中,然後異常返回,程序繼續運行。

所以IO一次就是讀一頁的大小

總結

MySQL的B樹和B+樹原理就說到這裏了,希望大家看完之後以後面試碰到這題會沒有困難!

另外,寫原創技術文章不易,要花費好多時間和精力,希望大家看到文章也能有所收穫!你們的點贊和收藏就能成爲我繼續堅持輸出原創文章的動力!大家也可以關注我的公衆號【碼農富哥】,訂閱更多我的文章!

歡迎關注公衆號:「碼農富哥」,致力於分享後端技術 (高併發架構,分佈式集羣系統,消息隊列中間件,網絡,微服務,Linux, TCP/IP, HTTP, MySQL, Redis), Python 等 原創乾貨 和 面試指南
關注公衆號後回覆【資源】免費獲取 2T 編程視頻和電子書

關注公衆號:碼農富哥

參考

從 MongoDB 及 Mysql 談B/B+樹
MySQL索引背後的數據結構及算法原理
面試官問你B樹和B+樹,就把這篇文章丟給他
面試官:爲什麼 MySQL 索引要使用 B+樹而不是其它樹形結構?比如 B 樹?
由 B-/B+樹看 MySQL索引結構

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