B-Tree的介紹與數據庫中應用分析



導讀:在一個有100萬條記錄的數據表中,利用二分查找定位一條記錄,大概需要20次操作,理論上也就是20次磁盤讀操作,需要花費大概0.2秒,有沒有辦法將磁盤操作次數降到3次呢?下面我們就介紹一下如何將20次的操作降到3次



關於B-Tree的一些基本介紹


在計算機科學中,B-Tree是一種自平衡的樹型數據結構,它保持數據有序,並且允許在O(log n)時間內進行檢索,順序訪問,插入以及刪除操作。B-Tree是一種允許節點的子節點個數大於2的泛化二叉搜索樹(BST)。不同於自平衡二叉查找樹,對於大數據塊讀寫操作的系統來說,b-Tree是一種最優的選擇。B-Tree是一個很好的外部存儲的數據結構的例子,因此,它通常被應用於數據庫和文件系統。


B-Tree中,內部節點(非葉子節點)有可變個子節點,這個數目是在一個預定義的範圍內的。當數據插入一個內部節點或者從一個內部節點的子節點中刪除時,這個內部節點的子節點的個數就會發生改變,這個時候這個內部節點有可能面臨合併(刪除時)或者分裂(插入時)。因爲B-Tree的內部節點的子節點個數是在一個範圍內的,因此,不需要像其他平衡搜索樹(BSTAVLRB-Tree)一樣頻繁的進行再平衡操作。正因爲如此,當一個內部節點的子節點並不飽滿(達到最大子節點樹)時,就會導致了一部分空間的浪費(雖然,內部節點的子節點個數是個可變的值,但是一個內部節點所能包含的最大子節點的個數一般是固定的,四叉樹或者八叉樹等,因爲,對於一棵四叉樹來說,如果其子節點只有兩個,就浪費了兩個節點的空間)。2-3樹爲例


B-Tree的每一個內部節點都包含了一些keyskeys代表的是分裂子樹的時候的分裂值(其實就是根絕keys的個數來判斷有幾個子節點)。例如,如果一個內部節點有三個子節點,那麼它就應該有兩個keysa1a2。左邊子樹的所有值都是小於a1這個值,中間子樹的所有數據都是大於a1並且小於a2的,而右邊子樹的所有數據都是大於a2的。

如上圖,兩個keys分別是719,有三個子節點(也可以說是三棵subtree)。

 

通常,keys的分數應該在d2d之間,d代表的是最小的keys個數,d+1就代表了這棵樹的最小度(度的下限)或者說是最小的分支數。事實上,在一個結點中,keys佔據了大多數的空間。The factor of 2 will guarantee that nodes can be split or combined.(着實不知道怎麼翻譯好,個人理解大概的意思是說之所以範圍是d2d,因爲這樣能保證分裂或者合併的時候滿足B-Tree的性質,後面會說到B-Tree的性質)。如果一個內部節點有2dkeys,那麼再往這個內部節點增加數據的時候,這個節點就要分裂成兩個節點,而且這兩個節點的keys的個數都是d,這個節點分裂前中間的結點要插入到其父節點中(具體的插入過程,後面會詳細介紹)。這樣,分裂出來的兩個節點都滿足最少有dkeys這一性質。同理,如果一個結點(B)只有dkeys,鄰居(兄弟)節點(C)也只有dkeys,而且現在又要從這個節點(B)中刪除一個keys,那麼這個結點就要和它的鄰居節點合併成一個新節點(D),合併完事以後,本來D應該有2d-1個結點,但是因爲兩個節點合併,影響了父節點(A),所以,父節點中就得降下來一個結點,插入到合併的結點D中,D中實際上應該是滿的,即2d個元素。(爲什麼會影響到父節點?要降下來一個結點呢?因爲原來的節點BC是父節點的兩個分支,比如,B裏面放的都是小於10的數據,C節點裏面放的都是大於10的數據,那麼,在父節點A中,定然存在一個keys,且值爲10,一旦BC合併了,父節點中的這個值是10keys就是去意義了,因此要將來,放到新合併成的結點D中。)


在一個節點裏,其分支個數(子節點分數)的值始終並這個節點包含的keys的個數多一個。在一棵2-3樹中,一個內部節點可能會存儲1keys(對應2個分支)或者2keys(對應3個分支)。一個B-Tree有時可以被描述成(d+1-2d+1)樹或者用最大分支數2d+1,比如2-3 tree2代表的是d+1,3代表的是2d+1,這裏d就是1,也可以把2-3 tree叫做 3-tree


B-Tree是一棵平衡樹,因此需要它所有葉子節點的深度一樣。在往樹中增加元素的時樹的深度慢慢增加,但是並不頻繁,並且即使深度增長,結果只會讓葉子節點到根節點的距離增長1而已。


在訪問數據遠比操作數據多的情況下,B-Tree無疑在現實的選擇中佔有很大的優勢。because then the cost of accessing the node may be amortized overmultiple operations within the node.因爲訪問結點的消耗會被平攤到節點內部的多重操作上。這種情況經常發生,當節點數據是存放在二級存儲器上時,如在磁盤上時。最大化keys的個數可以使的樹的高度變小,並且節點的訪問也會減少。另外,樹的再平衡操作也是不常發生的。這個最大化keys個數具體是多少,這要根據每個子節點所存儲信息的多少以及磁盤塊的大小或者類似的二級存儲器的數據塊存儲大小來決定。儘管2-3B-Tree很容易解釋,事實上在二級存儲器上使用B-Tree時需要把子節點的個數增大一些,以便達到提升性能的目的(實際上就是說,子節點越多,樹的高度就越低,這樣遍歷的深度就越少,但是具體要設置多少個節點,就要根據節點中存儲的數據信息大小和外部存儲器,磁盤來決定,儘量將一個結點的大小達到一個磁盤塊的大小,這裏就涉及到了磁盤讀寫的原理問題了,詳細參見http://blog.csdn.net/hbhhww/article/details/8206846)。



B-Tree在數據庫中的使用


通常情況下,對於排序和搜索算法,用比較運算操作的次數的大O形式來表示。(比如,O(n)表示進行了n次的比較運算),例如在一個有N條記錄的排序表中使用二分查找,會做次運算。如果這個表是包含了100W條記錄,那麼想要定位到一個指定的記錄,就需要20次操作:.


數據庫數據是放在磁盤驅動器上的。從磁盤驅動上讀取一個記錄的時間遠遠超過了驗證這個數據是否有效的時間。從磁盤上讀取一個記錄所需要的時間包含一次尋道時間和一次旋轉延遲。尋道的時間可能是在020,或者更多毫秒,旋轉延遲的時間平均下來差不多是一個旋轉週期的一半時間(最多旋轉1圈,最少不用旋轉,平均情況下,需要旋轉半圈)。一個7200(轉 /每分鐘)的硬盤,每旋轉一週所需時間爲60×1000÷72008.33毫秒,則平均旋轉延遲時間爲8.33÷24.17毫秒。例如希捷ST3500320NS型號的硬盤,他的track-to-track seek time0.8毫秒,average reading seek time8.5毫秒。這裏,我們先簡單的認爲,一次讀取操作所需要的時間大概是10毫秒左右的樣子。


對於一個有100W記錄的數據庫表,我們定位一個記錄需要進行20次操作(二分查找),一次操作平均需要10毫秒,那麼成功定位一條記錄就需要花費0.2秒。


事實上,是花費不了那麼長時間的。因爲我們知道,磁盤讀寫的最小單位是disk block,一個磁盤塊的大小假如是16KB,如果一個記錄的大小是160B,那麼一個磁盤塊大約可以容納100個記錄,這樣在有100W記錄的表中利用二分查找時,當二分14次以後,其實剩下的查找範圍只有大約61個記錄了,這61個記錄如果都放在同一個磁盤塊中,那麼最後6次所用的時間相當於一次磁盤讀取所用的時間。


最後6次只需要一次磁盤訪問,而前面的14次各需要一次磁盤訪問,這樣還是比較慢,如果還想獲得更快的速度,那麼只能對前14次的操作進行優化。這就涉及到了數據庫的索引了。


下面我也來見證一下奇蹟,看看如何將20次磁盤讀寫縮減到3次的。前面的例子中,我們假設表中有100W條記錄,,理論上定位一個記錄需要20次磁盤讀寫,實際上可能只需要15次,有沒有辦法再提高一下性能呢?有,那就是建立一個稀疏索引表。原始表中的數據存儲時,是100個記錄存放在一個磁盤塊中的,那麼我們將每個磁盤塊的第一條記錄拿出來,做成一個表(保證這個表的記錄還是有序的),這個表就是稀疏表,這個稀疏表中的數據量是原始表的1%,也就是1W條記錄,我們在這一萬條記錄中利用二分查找,只需要8次磁盤操作就可以定位到指定的block,定位一個具體的記錄,那就只需9次磁盤操作即可完成。


同理,我們還可以對這個稀疏表再進行稀疏,10000條記錄的表正好構成了100條記錄的二次稀疏索引表,我們先在二次稀疏表中定位一次,定位到了稀疏表中,這時花費了一個磁盤讀寫時間,在稀疏表中再定位一次,又花了一個磁盤讀寫時間,再去在原始表中定位具體的一條記錄,還是只需要一次磁盤操作,那麼,利用二次稀疏索引表,總共只需要3次磁盤操作,就完成了一條原始表中的數據的定位。(以上操作的前提是,原始表是個順序表,稀疏表和二次稀疏表也是有序的)。下面貼個簡略圖

從上圖是不是看到了B-Tree的影子。


上面介紹的是定位一個記錄的具體時間花費,通過以前我們對查找樹的學習,可以發現,查找一般是最簡單的操作,當然,這裏的簡單是代碼邏輯簡單,其實B-Tree的插入和刪除操作邏輯上也比查找都要麻煩。不過現在先說說插入和刪除的時間花費問題。


如果在一個順序表中插入一條記錄,就需要把插入位置之後的所有記錄都要後移,如果刪除的話,就需要把刪除的記錄後面的所有記錄前移,這是非常浪費時間的操作。而且我們前面建立的索引表也需要更新。有沒有什麼辦法來減少這種耗時的操作呢?這就讓我們又想到了計算機科學“時間和空間”問題。是的,好的辦法就是犧牲空間,來提升性能。像上面說的,如果每一個block都存放慢慢的100條記錄,空間利用率是100%,可是再添加一條數據的時候,是不是就麻煩了,要保證數據有序,但是block中的數據又都是滿的,只能重新分配磁盤塊了。所以,我們應當給每個block“預留”一定的空間,這樣插入數據是,就不會面臨上面那個“牽一髮而動全身”的問題了,同理,刪除的時候,就直接刪除那條記錄,對於block並不進行操作,雖然block上會“空出”一些空間,但是到達了提升性能的任務。這就需要在時間和空間上能做到一個比較好的平衡。B-Tree一個結點的keys個數是在d2d之間,小於d時合併,大於2d時分裂,也就是說,一個block的空間利用率應該是在50%100%之間的。

數據庫中使用B-Tree的幾點建議:

  1. 保持順序遍歷

  2. 使用層級索引,達到最小的磁盤讀操作

  3. 使用部分結點是滿的,部分結點不滿來提升插入和刪除時的性能

  4. 使用簡潔的遞歸算法維持索引的平衡性。

本文主要介紹了B-Tree的一些基本概念,對B-Tree先有一個大概的認識,後面增加如何寫代碼創建一個B-Tree,並且理解B-Tree中插入分裂,刪除合併的問題。

以上內容70%翻譯自維基百科,翻譯錯誤或者個人理解有誤支出還請指出,不要讓我誤導了他人才好,謝謝!


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