B-tree是怎麼讓查詢變快的?

B-tree是一種用來搜索大量數據的結構。它是40多年前發明的,但它仍然被大多數現代數據庫所使用。儘管有較新的索引結構,如LSM樹,但B樹在處理大多數數據庫查詢時仍然是無與倫比的。

下面我們來了解B-tree是如何組織數據的,以及它是如何執行搜索查詢的。

一、起源

爲了理解B-tree,讓我們首先了解二進制搜索樹(BST)Binary Search Tree。

看名字是不是看起來差不多?

那麼“B”代表什麼?

據wikipedia.org報道,B-tree的發明者Edward M.McCreight曾說過:

“你越想B-tree中的B是什麼意思,就越能理解B-tree。”

混淆B-tree和BST是非常常見的。無論如何,在我看來,BST是理解B樹的一個很好的起點。讓我們從BST的一個簡單示例開始:

具有三個節點的二進制搜索樹

 

 

數字越大總是在右邊,數字越低總是在左邊。當我們增加更多的數字時,情況可能會變得更清楚。

 

這個樹包含七個數字,我們最多需要訪問三個節點就能找到任何數字。以下示例搜索14。我使用SQL來定義查詢,以便將此樹對應實際的數據庫索引。

 

二、硬件
理論上,使用二進制搜索樹來運行我們的查詢看起來很好。其時間複雜性(搜索時)爲
O(logn)
,與B-樹相同。然而,在實踐中,這種數據結構需要在實際硬件上工作。

一般的,索引必須存儲在計算機上的某個位置。
計算機有三個地方可以存儲數據:
·CPU緩存
·RAM(內存)
·磁盤(存儲)
緩存完全由CPU管理。此外,它相對較小,通常只有幾兆字節。索引可能包含千兆字節的數據,所以它不適合放在CPU緩存
數據庫大量使用內存(RAM)。它有一些很大的優點:
確保快速隨機訪問(下一段將詳細介紹)
其大小可能相當大(例如,AWS RDS雲服務爲實例提供了幾TB的可用內存)。
但是,當電源關閉時,數據會丟失。此外,與磁盤相比,它相當昂貴

最後,內存的缺點是磁盤存儲的優點。它很便宜,即使我們失去電源,數據也會留在那裏。所以,看起來磁盤纔是最終選擇。但是讀取速度是一個問題,

從磁盤讀取速度很快,但只能在特定條件下進行!簡單地解釋一下。
(1)隨機和順序訪問
存儲器可以看做值的一行容器,其中每個容器都有編號。

(隨機訪問)現在讓我們假設我們想要從容器1、4和6中讀取數據。它需要隨機訪問:

 

(順序訪問)然後讓我們將其與讀取容器3、4和5進行比較。可以按順序進行:

 

“隨機跳轉”和“順序讀取”之間的區別可以根據硬盤驅動器來解釋。它由磁頭和磁盤組成。

 

“隨機跳躍”需要將磁頭移動到磁盤上的指定位置“順序讀取”就是簡單地旋轉磁盤,允許磁頭讀取連續的值。當讀取兆字節的數據時,這兩種類型的訪問之間的差異是巨大的。使用“順序讀取”顯著降低了獲取數據所需的時間
Adam Jacobs發表在Acm Queue上的文章《The Pathologies of Big Data》研究了隨機訪問和順序訪問之間的速度差異:

·HDD上的順序訪問可能比隨機訪問快數十萬倍
·從磁盤順序讀取可能比從存儲器隨機讀取更快。
現在誰還會用HDD硬盤?都是SSD,這項研究表明,從HDD完全按順序讀取可能比SSD更快。然而,請注意,這篇文章來自2009年,SSD在過去十年中得到了顯著發展,因此這些結果可能已經過時。
總之,關鍵的收穫是儘可能地選擇順序訪問。接下來,我將解釋如何將順序訪問應用於我們的索引結構。

三、優化樹以實現順序訪問

二進制搜索樹可以與堆相同的方式在內存中表示:

父節點位置爲i

左側節點位置爲2i

右側節點位置爲2i+1

這就是基於示例計算這些位置的方式(父節點從1開始):

 

根據計算出的位置,將節點排列到內存中:

你還記得上面的可視化的查詢嗎?

 

這就是它在存儲器上的樣子:

 

當執行查詢時,需要訪問存儲器地址1、3和6。訪問三個節點不是問題;然而,當我們存儲更多的數據時,樹會變得更高。

存儲超過一百萬個值需要一棵高度至少爲20的樹。這意味着必須讀取內存中不同位置的20個值。完全就是隨機訪問

四、頁
當一棵樹長得越來越高時,隨機訪問會造成越來越多的延遲減少這個問題的解決方案很簡單:把樹長得寬而不是高。它可以通過將多個值存放到單個節點中來實現。

 

它給我們帶來了以下好處:
·這棵樹比較淺(兩層而不是三層)
·它仍然有很大的空間來創造新的價值觀,而不需要進一步增長
對此類索引執行的查詢如下所示:

 

請注意,每次訪問節點時,我們都需要加載其所有值。在本例中,我們需要加載4個值(如果樹已滿,則加載6個值),以便達到我們要查找的值。下面,您將在內存中找到這棵樹的可視化視圖:

 

 

與前面的例子(樹的高度)相比,這個搜索應該更快。我們只需要隨機訪問兩次(跳轉到單元格0和9),然後依次讀取其餘值。
隨着數據庫的增長,此解決方案的效果越來越好。如果您想存儲一百萬個值,那麼您需要:

具有20個級別的二進制搜索樹

或者
具有10個級別的3值節點樹

來自單個節點的值構成一個頁面。在上面的示例中,每個頁面由三個值組成。頁面是相鄰放置在磁盤上的一組值,因此數據庫可以通過一次順序讀取一次訪問整個頁面。
它是如何與現實相聯繫的?Postgres頁面大小爲8kB。假設20%用於元數據,那麼它還有6kB。頁面的一半用於存儲指向節點子節點的指針,因此它爲我們提供了3kB的值。BIGINT大小爲8個字節,因此我們可以在單個頁面中存儲約375個值。

假設數據庫中一些相當大的表有10億行,我們需要在Postgres樹中存儲多少級?根據上面的計算,如果我們創建一個可以在單個節點中處理375個值的樹,那麼它可能會用一個只有四個級別的樹存儲10億個值。二進制搜索樹需要30個級別的數據量。
總之,在樹的單個節點中放置多個值有助於我們降低其高度,從而利用順序訪問的好處。此外,B樹不僅可以在高度上生長,還可以在寬度上生長(通過使用較大的頁面)。

五、平衡
數據庫中有兩種類型的操作:寫入和讀取。在上一節中,我們討論了從B-樹中讀取數據的問題。但是,寫入也是至關重要的一部分。將數據寫入數據庫時,B樹需要不斷更新新值。
樹形取決於添加到樹中的值的順序。它在二叉樹中很容易看到。如果按不正確的順序添加值,我們可能會獲得不同深度的樹。

 

當樹在不同的節點上具有不同的深度時,它被稱爲不平衡樹。基本上有兩種方法可以使這樣的樹恢復到平衡狀態:
1.只需按正確的順序添加值,即可從頭開始重建它。
2.隨着新值的增加,始終保持平衡。
B樹實現了第二個選項。使樹始終保持平衡的一個特性稱爲自平衡。

六、自平衡算法實例
只需創建一個節點並添加新值,直到其中沒有可用空間,就可以開始構建B樹。

 

如果相應的頁面上沒有空間,則需要對其進行拆分。要執行拆分,請選擇“拆分點”。在這種情況下,它將是12,因爲它是在中間。“分割點”是一個將移動到上頁的值。

 

現在,它把我們帶到了一個有趣的地方,那裏沒有上一頁。在這種情況下,需要生成一個新的根頁面(它將成爲新的根頁!)。

 

最後,在這三個中有一些自由空間,所以可以加上值14。

 

按照這個算法,我們可以不斷地向B樹添加新的值,它將始終保持平衡!

 

在這一點上,你可能有一個合理的擔憂,即有很多空閒空間沒有機會被填滿。例如,值14、15和16在不同的頁面上,因此這些頁面將永遠只保留一個值和兩個可用空間。
這是由拆分位置選擇引起的。我們總是把頁面在中間分開。但每次我們進行拆分時,我們都可以選擇任何我們想要的拆分位置。
Postgres有一個算法,每次執行拆分時都會運行!它的實現可以在Postgres源代碼中的_bt_findsplilloc()函數中找到。它的目標是儘可能少地留出空閒空間。

總結
在本文中,您瞭解了B樹是如何工作的。總而言之,它可以簡單地描述爲一個二進制搜索樹產生了兩個變化
1.每個節點可以包含多個值
2.插入新值之後是自平衡算法。
儘管現代數據庫使用的結構通常是B-樹(如B+樹)的一些變體,但它們仍然基於原始概念。在我看來,B樹的一大優勢是它被直接設計爲在實際硬件上處理大量數據。這可能是B樹伴隨我們這麼長時間的原因。

轉載:https://blog.allegro.tech/2023/11/how-does-btree-make-your-queries-fast.html

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