PostgreSQL中indexing的資料結構:B-Tree

David Lai
Practicode
Published in
7 min readJun 10, 2017

相信多數 Rails developer 在開發過程中,都會對一些 attributes 進行indexing,好處在於可以加快 query 所需的時間。通常會進行 indexing 的 attributes:

  1. primary key(Rails 會自動 index), foreign key
  2. 需要被排序的 attribute
  3. 時常需要被搜尋、query 的 attribute

而在 PostgreSQL 中預設的 index type 就是這篇文章介紹的資料結構:B-Tree

B-tree

我們先假設大家都知道 Tree 這個資料結構,而這裡的「B」指的就是「Balanced」的意思,是屬於其中一種「平衡樹」,其基於 Binary Search Tree 再進行改良,使其能自己保持 Balanced 的資料結構。

「平衡(Balanced)」便是指 Tree 的兩邊資料量、深度相差不多,因此可以有較快查找速度、較低的查找複雜度。

「平衡樹(Balanced Tree)」便是指能夠在進行新增、刪除時透過「樹旋轉(rotate)」維持住「平衡(Balanced)」狀態的 Tree 資料結構,而 B-Tree 就是屬於其中一種。

B-Tree 的複雜度如下圖所示:

source from WIKI https://zh.wikipedia.org/wiki/B%E6%A0%91

B-Tree 基本特性:

  • 每一個節點(包含根)所能擁有的鍵值數量範圍是先被定義好的,假設 d 為大於等於 2 的整數,通常定義為 d-1 與 2d-1 之間。換句話說,每一個節點擁有 d-1~2d-1 個值。
  • 因此可以推斷出每個節點擁有的子節點數量為 d~2d 之間(除了葉節點)。
  • B-Tree 中的所有葉節點深度(depth)皆相同。

以下我們用一個例子來說明:

source from http://flylib.com/books/en/2.300.1.160/1/

這個例子中 d=2,因此每個節點最少有 1 個鍵值、最多有 3 個鍵值,而每個節點(除了葉節點)依照鍵值數量分別擁有 2~4 個子節點,而所有葉節點深度皆為 2,符合上述對 B-Tree 的描述。

操作 — 搜尋

在 B-Tree 進行搜尋的方式很類似在 Binary Search Tree 中的搜尋方式:搜尋數值 K,則從根開始,若 K 大於該節點值便往右子樹搜尋;反之則往左子樹搜尋,較不一樣的為當該節點有多值時,會從第一個值開始搜尋,直到找到該值或是找到其應該往子樹繼續下去搜尋的範圍。

範例:在上面的Tree中搜尋「15」

  1. 由根節點開始,15 >9,因此往右子樹搜尋
  2. 15介於13與17中間,因此往中間的葉節點搜尋
  3. 在葉節點中找到15,結束搜尋
Search 順序圖示,紅點分別為 「< 13」, 「> 13 且 < 17」, 「> 17」

操作 — 插入

在 B-Tree 中的插入操作永遠從葉節點進行,另外必須考慮其每個節點鍵值數量的限制,若超過限制時會用到 split 的技巧。

範例1:插入值「20」

  1. 藉由搜尋的技巧發現20應該屬於最右下角的[18, 19]這個葉節點之中
  2. 將20插入這個節點中得到[18, 19, 20],插入成功

範例2:插入值「16.5」

  1. 藉由搜尋的技巧發現 16.5 應該屬於[14, 15, 16]這個節點之中
  2. 將 16.5 插入這個節點得到[14, 15, 16, 16.5]超出節點鍵值數量限制
  3. split : 將這個節點中,中間值(15或16)往父節點提升,這裡以16為例
  4. 父節點[13, 17]變為[13, 16, 17],而[14, 15, 16, 16.5]被拆為[14, 15]成為13, 16間的葉節點;[16.5]成為16, 17間的葉節點。插入成功
插入16.5並把中間值16往父節點提升
提升16後,底下所屬節點重新分配

*若往上提升後導致父節點也超過鍵值數量限制,則必須再用同樣的 split 方法再向上提升。

操作 — 刪除

刪除操作較為複雜,需分為1. 刪除的值非葉節點 2. 刪除的值為葉節點,又得再分為刪除後是否造成空節點:

刪除的值為葉節點

  1. 刪除後並未造成空節點,則可以直接刪除,例如刪除「1」,便是先找到1在[0, 1]的節點中,直接把1刪除,此節點剩下[0]。
  2. 刪除後造成空節點,則必須進行「樹旋轉(rotate)」,例如刪除「3」後,2, 4之間的葉節點為空,便進行 rotate:[0, 1]的 1 提升到父節點,而2往下移填補3的空位。
刪除3後,進行rotate:2往下遞補而1上提升

刪除的值非葉節點

策略:先向左右借,借不到就合併

  1. 刪除「17」,刪除後需要有值補17留下的空位:先向左右借,因此向左邊的最大值「借」16來補17的位置(亦可借右邊的最小值18)。
刪除17後,向左邊借最大值16(也可向右邊最小值借18)提升
  1. 刪除「4」,刪除後需要有值補4的位置,但是向左右借都會造成空節點,於是進行合併節點:將3, 5合併成一個節點接在[2, 6]之下。
刪除4後向左右借失敗,便合併節點 3, 5 連接到2, 6中間

小結

在 Database 中 B-Tree 與 index 的關連性是什麼呢?

猶如前面所說,index 可以加快搜尋的時間,便是因為當資料儲存在 Disk 時其實是不連續的,其很有可能散落在不同的記憶區塊,因此建立一張表用來連結到不同區塊的資料便可以有效加速搜尋的時間,也就是 index 的原理,而 B-Tree 就是這張巨大的表所用的資料結構。

讓我們看看 Wiki 怎麼說 B-Tree的優點:

  • keeps keys in sorted order for sequential traversing
  • uses a hierarchical index to minimize the number of disk reads
  • uses partially full blocks to speed insertions and deletions
  • keeps the index balanced with an elegant recursive algorithm

由於其結構化的排序,並且垂直且一個節點可以容納多個鍵值的特性,使得比基本的 Binary Search Tree 更扁平,減少搜索的深度,以及擁有自平衡的演算法維持樹在最小的深度。

是不是對平常使用的 index 有更多瞭解了呢?

參考資料

--

--

David Lai
Practicode

Work in recommendation system and NLP algorithms. Microsofter in Redmond, Suzhou and Taiwan