數據庫索引算法——B樹與B+樹

B-樹

數據庫索引爲何要使用樹結構存儲呢?
這個還不簡單,樹的查詢效率高,並且能夠保持有序。
既然這樣,爲何索引沒有使用二叉查找樹來實現呢?
這就不明白了,明明二叉查找樹時間複雜度是O(logN),性能已經足夠高了,難道B樹能夠比它更快? html

其實從算法邏輯來說,二叉樹查找樹的查找速度和比較次數都是最小的,可是咱們不得不考慮一個現實問題:磁盤IO ;數據索引是存儲在磁盤上的,當數據量比較大的時候,索引的大小可能有幾個G甚至更多。
當咱們利用索引查詢的時候,能把整個索引所有加載到內存嗎?顯然不可能。能作的只有逐一加載每一個磁盤頁,這裏的磁盤頁對應索引樹的節點。
在二叉查找樹裏,磁盤的IO次數等於索引樹的高度。 mysql

既然如此,爲了減小磁盤的IO次數,咱們就須要把本來「瘦高」的樹結構變成「矮胖」,這是B-樹的特徵之一。
B-樹是一種多路平衡查找樹,它的每個節點最多包含k個孩子,k被稱爲B樹的階。
k的大小取決於磁盤頁的大小。 算法

下面來具體介紹如下B-樹(Balance Tree),一個m階的B樹具備以下幾個特徵:sql

  1. 根節點至少有兩個子女;
  2. 每一箇中間節點都包含k-1個元素和k個孩子,其中m/2<=k<=m;
  3. 每個葉子節點都包含k-1個元素,其中m/2<=k<=m;
  4. 全部葉子節點都位於同一層;
  5. 每一個節點中的元素從小到大排列,節點當中k-1個元素正好是k個孩子包含的元素的值域劃分。

下面以3階的B-樹爲例,來看看B-樹的具體結構。數據庫

【     9    】
           /                  \
          /                    \
     【2  6】                 【12】
   /   |      \            /       \ 
  /    |       \          /        \ 
 /     |        \        /          \
1    【3 5】     8       11       【13 15】

這棵樹中,重點看【2 6】節點,該節點有兩個元素2和6,又有三個孩子1,【3 5】和8。
其中1小於元素2,6之間,8大於【3 5】,正好符合剛剛所列的幾條特徵。 性能

演示下B-樹的查詢過程,假如咱們要查詢數值爲5的節點。
第一次IO,查到 9,比較9
第二次IO,查到 【2 6】 比較2,比較6
第三次IO,查到 【3 5】 比較3, 比較5優化

雖然 比較次數 並不比二叉查找樹少,尤爲當單一節點中的元素數量不少時。
可是相比磁盤IO的速度,內存中 比較的操做 耗時幾乎能夠忽略,因此只要樹的高度足夠低,IO次數足夠少,就能夠提升查找性能。
相比之下節點內部元素多一些並不要緊,最多就是幾回內存交換操做而已,只要不超過磁盤頁的大小,這就是B-樹的優點之一。 操作系統

但B-樹,插入新節點的過程就比較複雜,並且分紅不少種狀況。因此咱們舉個典型的例子,上圖中插入4.
自頂下查找4的節點位置,發現4應該插入【3 5】之間
節點【3 5】已是兩個元素節點,根據特徵2要求,沒法再增長了。父節點【2 6】也是兩元素節點,也沒法再增長,根節點9是單元素節點,能夠升級爲兩元素節點。因而拆分節點【3 5】和節點【2 6】,讓根節點升級爲兩元素節點【4 9】,節點6獨立成爲根節點的第二個孩子。指針

【    4 9    】
         /         |         \
        /          |          \
       2          6          【12】
   /   |         / \        /      \ 
  /    |        /   \      /        \ 
 /     |       /     \    /          \
1      3      5       8  11       【13 15】

雖然維護樹結構麻煩,但也正由於如此,讓B-樹可以始終維持多路平衡。這就是B-樹的一大優點:自平衡。 code

下面在舉例說說B-樹的刪除,繼續上一顆樹,咱們要刪除元素11
自頂向下查找元素11的節點位置。
刪除11後,節點12只有一個孩子,不符合特徵1和5 (很差意思,我猜的,原文就直說不符合B樹規範)。
所以找出12,13,15這個節點的中位數,取代節點12,而節點12自身下已成爲第一個孩子。(此過程爲左旋

【    4 9    】
         /         |         \
        /          |          \
       2          6            13
   /   |         / \        /      \ 
  /    |        /   \      /        \ 
 /     |       /     \    /          \
1      3      5       8  12          15

雖然B-樹的插入和刪除,很複雜,沒看懂也不要緊,關鍵是理解B-樹的核心思想:B-樹主要應用於文件系統以及部分數據庫索引,好比著名的非關係型數據庫MongoDB。

不過大部分關係型數據庫,好比MySql,則使用B+樹做爲索引。

不少文檔裏,有時寫B-樹,有些寫B樹,但都是指balance tree,而不是balance binary tree

B+樹

B+樹是基於B-樹的一種變體,有着比B-樹更高的查詢性能。

一個m階的B+樹具備以下幾個特徵:

  1. 有k個子樹的中間節點包含k個元素(B-樹中是k-1個元素),每一個元素不保存數據,只用來索引,全部數據都保存在葉子節點。
  2. 全部葉子節點中包含了所有元素的信息,及指向含這些元素記錄的指針,且葉子節點自己 按關鍵字的大小自小二大的順序連接。
  3. 全部的中間節點元素都同時存在於子節點,在子節點中是最大(或最小)元素。
【    8 15    】
           /                   \
          /                     \
      【2 5 8】               【11 15】
     /    |     \              /      \ 
    /     |      \            /        \ 
   /      |       \          /          \
【1 2】->【3 5】->【6 8】->【9 11】--->【13 15】

在上面棵B+樹中,根節點元素8是子節點【2 5 8】的最大元素,也是葉子節點【6 8】的最大元素
根節點元素15也是子節點【11 15】的最大元素,也是葉子節點的【13 15】的最大元素

須要注意的是,根節點的最大元素(這裏是15),也就等同於整個B+樹的最大元素。之後不管插入刪除多少元素,始終要保持最大元素的根節點當中。

至於葉子節點,因爲父節點的元素出如今子節點,所以全部葉子節點包含了全量元素信息。
而且每個葉子節點都帶有指向下一個節點的指針,造成了一個有序鏈表。

B+樹還有一個特色,這個特色是在索引以外,確實是相當重要的特色,那就是【衛星數據】的位置。
所謂衛星數據,指的是索引元素所指向的數據記錄,好比數據庫中的某一行。在B-樹中,不管中間節點仍是葉子節點都帶有衛星數據。

而在B+樹中,只有葉子節點帶有衛星數據,其他中間節點僅僅是索引,沒有任何數據關聯。
須要補充的是,在數據庫的彙集索引(Clustered Index)中,葉子節點直接包含衛星數據。在非彙集索引(NonClustered Index)中,葉子節點帶有指向衛星數據的指針。
B+樹的好處主要體如今查詢性能上:

  • 在單元素查詢的時候,B+樹會自頂向下逐層查找節點,最終找到匹配的葉子節點。 這跟B-樹不同的兩點是,首先,B+樹的中間節點沒有衛星數據,因此一樣大小的磁盤頁能夠容納更多的節點元素。這意味,數據量相同的狀況下,B+樹的結果比B-樹更加」矮胖「,所以查詢時IO次數也更少。第二,B+樹的查詢必須最終查找到葉子節點,而B-樹只要找到匹配元素便可,不管匹配元素處於中間節點仍是在葉子節點,所以,B-樹是查找性能並不穩定(最好狀況是隻查根節點,最壞狀況是查到葉子節點)。而B+樹的每一次查找都是穩定的。
  • 在範圍查詢的時候,B-樹只能依靠繁瑣的中序遍歷。而B+樹,很簡單,只須要在鏈表上作遍歷便可。

綜合起來,B+樹相比B-樹的優點有三個:

  1. IO次數更少;
  2. 查詢性能穩定;
  3. 範圍查詢簡便。

Mysql的階樹計算

 咱們能夠來算一筆帳,以InnoDB存儲引擎中默認每一個頁的大小爲16KB來計算,假設以int型的ID做爲索引關鍵字,那麼 一個int佔用4byte,由上圖能夠知道還有其餘的除主鍵之外的數據,姑且頁當成4byte,那麼這裏就是8byte,那麼16KB=161024byte,那麼咱們在這種場景下,能夠定義這個B-Tree的階樹爲 (161024)/8=2048.那麼這個樹將會有2048-1路,也就是原來平衡二叉樹(兩路)的1024倍左右,從而大大提升了查找效率與下降IO讀寫次數。

參考:https://www.cnblogs.com/wuzhe...

Mysql Innodb數據頁

其實mysql數據頁結構不是單純16KB都給數據用,請參考https://www.cnblogs.com/bdsir...

看來不管是這裏的頁,仍是操做系統內存的頁page的概念,都是相似c語言的structure結構體的概念。

B+樹索引的分裂優化

在4階的B+樹中(爲了圖好看直觀,*表明是頁面的可用空間)

【1 3 * *】
     /     \
    /       \
   /         \
【1 2 * *】->【3 4 5 6】

插入記錄7時,因爲葉節點的頁面(下文簡稱葉頁面)中只能存放4條記錄,插入記錄7時,會致使葉頁面分裂,產生一個新的葉頁面。

【1 3 5 *】
         /     |       \
        /      |        \
       /       |         \
      /        |          \
【1 2 * *】->【3 4 * *】->【5 6 7 *】

傳統B+樹頁面裂變操做及分析:

  • 按照原頁面中50%的數據量進行分裂,針對當前這個分裂操做,3,4記錄保留在原有頁面,5,6記錄移動到新的頁面。最後將新記錄7插入到新的頁面中;
  • 50%分裂策略的優點:

    • 分裂以後,亮哥頁面的空間利用率是同樣的;若是新的插入是隨機在兩個頁面中挑選進行,那麼下一個分裂的操做就會更晚觸發。
  • 50%分裂策略的劣勢:

    • 空間利用率不高:按照傳統50%的頁面分裂策略,索引頁面的空間利用率在50%左右;
    • 分裂頻率較大:針對如上所示的遞增插入(遞減插入),每新插入兩條記錄,就會致使最後的葉頁面在此發送分裂。

因爲傳統50%分裂的策略有不足之處。所以,針對B+樹索引的遞增/遞減插入進行了優化(目前全部的關係型數據庫,包括Oracle/InnoDB/PostgreSQL)。通過優化,上述B+樹索引,在記錄6插入完畢,記錄7插入引發分裂以後,新的B+樹結構以下:

【1 3 5 *】
         /     |       \
        /      |        \
       /       |         \
      /        |          \
【1 2 * *】->【3 4 5 6】->【7 * * *】

對比上下兩個插入記錄7以後,B+樹索引的結構圖,能夠發現兩者有不少不一樣之處:

  • 新的分裂策略,在插入7時,不移動原有頁面的任何記錄,只是將新插入的記錄7寫到新的頁面中。
  • 原有頁面的利用率,仍舊是100%;
  • 優化分裂策略的優點:

    • 索引分裂的代價小:不須要移動記錄;
    • 索引分裂的機率下降:若是接下來的插入,仍舊是遞增插入,那麼須要插入4條記錄,才能再次引發頁面的分裂。50%分裂策略,分裂的概念下降了一半。
    • 索引頁面的空間利用率提升:新的分裂策略,可以保證分裂前的頁面,仍舊保持100%的利用率,提升索引的空間利用率。
  • 優化分裂策略的優點:

    • 若是新的插入,再也不知足遞增插入的條件,而是插入到原有頁面,那麼就會致使原有頁面在此分裂,增長分裂的機率。

所以,此分裂的優化策略,僅僅是針對遞增遞減插入有效,針對隨機插入,就是去了優化的意義,反而帶來更高的分裂機率。 在InnoDB的實現中,爲每一個索引頁面維護了一個上次插入的位置,以及上次的插入是遞增/遞減的標識。根據這些信息,InnoDB可以判斷出新插入的頁面中的記錄,是否仍舊知足遞增/遞減的約束,若知足約束,則採用優化後的分裂策略;若不知足越蘇,則退回到50%的分類策略。這裏提下,遞增和遞減,指的是趨勢,因此Snowflake算法生成的序列是知足遞增的約束。

相關文章
相關標籤/搜索