為什么 MongoDB (索引)使用B-樹而 Mysql 使用 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,這個因子指明了樹的分裂及合並的規則,這個規則維持了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 次數少。

為什么 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 的格式表示如下:

 

相對於 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 域,每個節點能索引的范圍更大更精確


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM