標籤:
原文來自於:http://blog.csdn.net/cjfeii/article/details/10858721
1. B+樹索引概述
在上一篇文章中,我們討論了關於index的幾個中重要的課題:
A) index是儲存在磁碟上的一種資料結構,用於提高查詢或是掃描record的速度。
B) 排序索引樹通過儲存page的指標加速record的尋找。(ISAM)
C) 維護排序索引樹的代價很高,因此,ISAM通過建立overflow page來解決這個問題,但是過多的overflow page會使查詢效能從log(指數log)層級降低到線性遍曆。
下面我們將介紹一種高度健壯的、比較流行的一種資料結構——B+樹,作為ISAM的擴充。
一般說來,B+樹是一種高效的基於磁碟儲存的資料結構,主要儲存(key, value)pair。它支援對key的高效尋找,和高效的範圍迭代。
B+樹提供了這些功能:
A) 快速的record尋找
B) 快速的record遍曆
C) 不通過overflow page的形式維護排序樹結構
B+樹背後的關鍵思想是利用有序平衡樹,替代ISAM中的排序樹。
2. B+樹的定義
B+樹是用磁碟上的page作為node節點的樹。B+樹中的節點可以區分為leaf node(葉子節點)和interior node(內部節點)。
由於每一個node剛好是磁碟中的一個page,在B+樹中,我們使用的術語node和page是可以互換的。
2.1 leaf node
leaf node儲存資料entry(條目,相當於record),entry的形式是(key, value)。所有的leaf node也被組織成page鏈表的形式。B+樹的leaf node如所示:
下面是leaf node抽象的資料結構定義:
struct LeafNode { vector<Key> keys; vector<Value> values; PagePointer next_page;};
對任意的leaf node,下面的公式都是成立的:
p.keys.size() == p.values.size()
2.2 interior node
Interior node組織成一個樹的形式,從root node(根節點,根節點也是一個interior node)開始,通過儲存一系列key,來加速查詢leaf node。
Interior node儲存著一系列key和page指標,它的結構:
下面是interior node抽象的資料結構定義:
struct InteriorNode { vector<Key> keys; vector<PagePointer> pointers;};
對任意的interior node來時,下面的公式都是成立的:
p.keys.size() +1 == p.pointers.size()
有一個定義:Neighbouring pointer(臨近指標)
對於一個key ki,我們定義before(ki)是ki前面臨近的page指標,after(ki)是ki後面臨近的指標。也就是說:
p.before(ki) = p.pointers[i]
p.after(ki) = p.pointers[i+1]
2.3 B+樹的屬性和約束條件
2.3.1 node中的key都是排好序的。
假設,p是B+樹中的node,那麼我們必須維持p.keys關於value是有序的。
2.3.2 各個node之間也是按key進行排序的。
B+樹是有序樹,表現在一下幾個方面:
A) leaf node是有序的:
∀p∈LeafNode,∀k∈p.keys,∀k′∈p.next_page.keys,k<k′
多個leaf node組成一個有序鏈表,在各個leaf node之間使得高效的對(key, value)遍曆成為可能。
B) interior node是有序的:
B+樹對所有的key k,和其臨近的指標after(k) 、after(k),滿足下面的條件:
k>max(keys(before(k)))
k≤min(keys(after(k)))
換句話說,k是介於before(k)、after(k) 的key之間的key。
2.3.3 B+樹是平衡樹
B+樹是平衡樹,所有從root node到任何leaf node的路徑長度是相等的。
2.3.4 B+樹node是充分填充的
B+樹允許它的node部分填充。主要是設計了一個填滿因數的參數,用來限定每個non-root node(非根節點)的最小填充度。
如果一個non-root node的填充度不夠,我們就說該node underflow,在B+樹裡只有root node可以underflow。
這裡有一個不合格的B+數的例子。假設我們定義了下面的參數:
Capacity of each node: 4 keys
Fill factor: 50%
當該樹經過平衡和排序後,它的結構如所示:
它存在著這樣一個問題:沒有滿足我們上面定義的填滿因數(fill factor)50%:
3. B+樹的查詢search和插入insert
B+樹的主要操作有:
/** * finds the leaf node that _should_ contain the entry with the specified key */LeafNode search(Node root, Key key)/** * Inserts a key/val pair into the tree. * Returns the root of the new tree which _may_ be different * from the old root node. */InteriorNode insert_into_tree(InteriorNode root, Key newkey, Value val)
B+樹的insert演算法必須保證執行相應的操作之後使得樹依然滿足B+的所有屬性和約束。
3.1 Searching
B+樹的查詢演算法就是簡單直接的樹的尋找演算法:
LeafNode search(Node p, Key key) { if(p is LeafNode) return root; else { if(key < p.keys[0]) return search(before(p.keys[0]), key); else if(key > p.keys[-1]) return search(after(p.keys[-1]), key); else { let i be p.keys[i] <= key < p.keys[i+1] return search(after(p.keys[i]), key) } }}
3.2 Inserting
B+樹的insert操作是非常棘手的。它不像AVL的insert操作那樣簡單,B+樹還需要考慮node的overflow和underflow。
Insert演算法從這開始:
1) 尋找insert的正確的目標leaf node
2) 向目標leaf node中嘗試insert操作
InteriorNode insert_into_tree(InteriorNode root, Key newkey, Value val) { LeafNode leaf = search(root, newkey); return insert_into_node(leaf, newkey, val);}
其中,insert_into_node中,要做如下的一些事:
/** * Tries to inserts the (newkey/val) pair into * the node. * * If `target` is an interior node, then `val` must be a page pointer. */InteriorNode insert_into_node(Node target, newkey, val) { if( ... CASE 1 ... ) { /* handle CASE 1 */ } else if( ... CASE 2 ... ) { /* handle CASE 2 */ } else if( ... CASE 3 ... ) { /* handle CASE 2 */ }}
其中三個不同的case包括:
A) 目標leaf node有足夠的空間儲存key
B) 目標leaf node已滿,但是它的parent node(父節點)有足夠的空間儲存key
C) 目標leaf node和它的parent node已滿。
Case 1:
這是最簡單的一種情況,將entry(newkey, value) 插入到目標leaf node即可。
A) Root node 不需要改變
B) 磁碟I/O也無需討論。所有的操作都在一個page中。buffer manager(緩衝管理)可以被用到,彷彿所有的node都儲存在記憶體。
示:
Case 2:
在這種情況下,target node已滿,但是它的parent node有足夠的空間儲存一個key。
A) 建立一個target node的兄弟node作為new_target node,將new_target node插入帶target node之後。
B) 將target node中的所有entry和我們需要增加的entry分配儲存到target node和new_target node中。由於分配之前target node是滿的,那麼就可以斷定分配之後,這兩個node不會存在underflow。
C) 將new_target的指標(k,p) = (leaf2.keys[0], ADDRESS[leaf2]) insert到target node的parent node中。由於parent node有足夠的空間儲存一個key,所以parent node不會出現overflow。
示:
Case 3:
這種情況是target和parent[target]都滿了的情況。我們需要遞迴的嘗試將新的key insert到target的祖先node中。甚至都有這種情況:root node也沒有足夠的空間儲存這個新key ,這種情況下,我們必須對root進行分割,建立一個新的node作為B+樹的root node。
具體細節如下:
A) 建立一個new_target node,insert到target之後。
B) 將target中的entry分配儲存到target和new_target中。
現在我們需要將new_target node的指標(k,p) = (leaf2.keys[0], ADDRESS[leaf2])插入到它的PARENT[target],但是PARENT[target]已經滿了。
A) 令target_parent = PARENT[target]
B) 令all_keys = sorted(target_parent.keys ∪ {k})
C) 申請一個新的node:new_interior
D) 令i = floor(all_keys.size() / 2)
middle_key = all_keys[i]
E) 將all_keys[0 .. i-1]儲存到target_parent中,將all_keys[i+1 .. n]儲存new_interior到中。
F) 如果target_parent是root,那麼我們就建立一個新的node作為grandparent ,令grandparent = PARENT[target_parent]。
G) 遞迴地調用:
insert_into_node(grandparent, middle_key, ADDRESS[new_interior])
:
4. B+樹的其它的東西
a) B+樹也支援高效的刪除delete。刪除演算法是insert演算法的逆過程。在delete的演算法中會通過merge(合并)node,去避免underflow。如果發生merge node,那麼會在parent node遞迴的delete這個(Key, PagePointer)。
b) 如果所有的data entry都儲存在sequential file中,並且關於key排序,那麼就可以非常有效將sequential file裝載到B+樹。
c) B+樹可以作為基於磁碟有序儲存的排序演算法。
【轉】 資料庫系統——B+樹索引