像 Mysql 和 MongoDB 這種大型軟件在設計上都是精益求精的,它們爲什麼選擇B樹,B+樹這些數據結構?

爲什麼 MongoDB (索引)使用B-樹而 Mysql 使用 B+樹?

B 樹與 B+ 樹,其比較大的特點是:B 樹對於特定記錄的查詢,其時間複雜度更低。而 B+ 樹對於範圍查詢則更加方便,另外 B+ 樹相對於 B 樹來說更加扁平。

對於 MongoDb 來說,其是非關係型數據庫,較少做聯表的範圍查詢。

因爲像 Mysql 和 MongoDB 這種經久考驗的大型軟件在設計上都是精益求精的,它們爲什麼選擇這些數據結構?:)

 

本文從實際應用的角度來介紹以及分析B-樹和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-樹的高度,爲底數很大的 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-樹,B-樹的每個節點有 d~2d 個 key,2 這個因子指明瞭樹的分裂及合併的規則,這個規則維持了B-樹的平衡。

B-樹的插入和刪除就不具體介紹了,很多資料都描述了這一過程。在普通平衡二叉樹中,插入刪除後若不滿足平衡條件則進行 旋轉 操作,而在B-樹中,插入刪除後不滿足條件則進行分裂及合併操作。

簡單敘述下分裂及合併操作。

分裂:如果有一個節點有 2d 個 key,增加一個後爲 2d+1 個 key,不符合上述規則 B-樹的每個節點有 d~2d 個 key,大於 2d,則將該節點進行分裂,分裂爲兩個 d 個 key 的節點並將中值 key 歸還給父節點。
合併:如果有一個節點有 d 個 key,刪除一個後爲 d-1 個 key,不符合上述規則 B-樹的每個節點有 d~2d 個 key,小於 d,則將該節點進行合併,合併後若滿足條件則合併完成,不滿足則均分爲兩個節點。

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+樹中,key 的副本存儲在內部節點,真正的 key 和 data 存儲在葉子節點上 。
n 個 key 值的節點指針域爲 n 而不是 n+1。
如下圖爲一顆B+樹:

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

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

B-樹和B+樹的區別

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

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

B-樹

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

B+樹

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

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

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

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

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

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

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

爲什麼 MongoDB 索引選擇B-樹,而 Mysql 索引選擇B+樹
這些內容瞭解後,我們來看爲什麼 MongoDB 索引選擇B-樹,而 Mysql (InooDB 引擎)索引選擇B+樹。

Mysql 大家應該比較熟悉,傳統的關係型數據庫,下面介紹下 MongoDB。

來看下 wiki 百科上 MongoDB 的定義:

MongoDB (from humongous) is a cross-platform document-oriented database. Classified as a NoSQL database, MongoDB eschews the traditional table-based relational database structure in favor of JSON-like documents with dynamic schemas (MongoDB calls the format BSON)

這段話的大致意思是 MongoDB 是文檔型的數據庫,是一種 nosql,它使用類 Json 格式保存數據。

文檔型數據庫和我們常見的關係型數據庫不同,一般使用 XML 或 Json 格式來保存數據,歸屬於聚合型數據庫。

鍵值數據庫也屬於聚合型數據庫,熟悉 Redis 的同學應該很好理解。

舉個例子:

加入我們要建立一個電子商務網站,類似淘寶這種將商品銷售給用戶,那麼必須存儲用戶信息、商品目錄、訂單、收貨地址、賬單地址、付款方式等。

看下傳統的關係型數據庫是如何存儲的:

聚合型數據庫存儲模型:

用類似 Json 的格式表示如下:

//Customer
{
        "id":1,
        "name":Tom,
        "billingAddress":[{"city":"China"}]
}

//Orders
{
        "id":99,
        "orderItem":[
                "productId"27,
                "price":100,
                "productName":book
         ],
         "shippingAddress":[{"city":"china"}],
         "orderPayment":[
            ...
        ]   
}


相對於 Mysql 關係型數據庫,MongoDB 這類 nosql 適用於數據模型簡單,性能要求高的場合

 

爲什麼 MongoDB 使用B-樹

MongoDB 是一種 nosql,也存儲在磁盤上,被設計用在 數據模型簡單,性能要求高的場合。

性能要求高,看看B/B+樹的區別第一點:

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

我們說過,儘可能少的磁盤 IO 是提高性能的有效手段。MongoDB 是聚合型數據庫,而 B-樹恰好 key 和 data 域聚合在一起。

爲什麼 Mysql 使用B+樹


Mysql 是一種關係型數據庫,區間訪問是常見的一種情況,而 B-樹並不支持區間訪問(可參見上圖),而B+樹由於數據全部存儲在葉子節點,並且通過指針串在一起,這樣就很容易的進行區間遍歷甚至全部遍歷。

見B/B+樹的區別第二點:

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

其次,B+樹的查詢效率更加穩定,數據全部存儲在葉子節點,查詢時間複雜度固定爲 O(log n)。

最後第三點:

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

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