1. 程式人生 > >B-樹和B+樹的應用:資料搜尋和資料庫索引

B-樹和B+樹的應用:資料搜尋和資料庫索引

B-樹

1 .B-樹定義

B-樹是一種平衡的多路查詢樹,它在檔案系統中很有用。

定義:一棵m 階的B-樹,或者為空樹,或為滿足下列特性的m 叉樹:
⑴樹中每個結點至多有m 棵子樹;
⑵若根結點不是葉子結點,則至少有兩棵子樹;

⑶除根結點之外的所有非終端結點至少有[m/2] 棵子樹;
⑷所有的非終端結點中包含以下資訊資料:

      (n,A0,K1,A1,K2,…,Kn,An)
其中:Ki(i=1,2,…,n)為關鍵碼,且Ki<Ki+1,

           Ai 為指向子樹根結點的指標(i=0,1,…,n),且指標Ai-1 所指子樹中所有結點的關鍵碼均小於Ki (i=1,2,…,n),An 所指子樹中所有結點的關鍵碼均大於Kn.

           n   為關鍵碼的個數。
⑸所有的葉子結點都出現在同一層次上,並且不帶資訊(可以看作是外部結點或查詢失敗的結點,實際上這些結點不存在,指向這些結點的指標為空)。

   即所有葉節點具有相同的深度,等於樹高度。

 如一棵四階B-樹,其深度為4.

          

B-樹的查詢類似二叉排序樹的查詢,所不同的是B-樹每個結點上是多關鍵碼的有序表,在到達某個結點時,先在有序表中查詢,若找到,則查詢成功;否則,到按照對應的指標資訊指向的子樹中去查詢,當到達葉子結點時,則說明樹中沒有對應的關鍵碼。

在上圖的B-樹上查詢關鍵字47的過程如下:

1)首先從更開始,根據根節點指標找到 *節點,因為 *a 節點中只有一個關鍵字,且給定值47 > 關鍵字35,則若存在必在指標A1所指的子樹內。

2)順指標找到 *c節點,該節點有兩個關鍵字(43和 78),而43 < 47 < 78,若存在比在指標A1所指的子樹中。

3)同樣,順指標找到 *g節點,在該節點找到關鍵字47,查詢成功。

2. 查詢演算法

typedef int KeyType ;
#define m 5					/*B 樹的階,暫設為5*/
typedef struct Node{
	int keynum;				/* 結點中關鍵碼的個數,即結點的大小*/
	struct Node *parent;	/*指向雙親結點*/ 
	KeyType	key[m+1];		/*關鍵碼向量,0 號單元未用*/ 
	struct Node *ptr[m+1];	/*子樹指標向量*/ 
	Record *recptr[m+1];	/*記錄指標向量*/
}NodeType;					/*B 樹結點型別*/

typedef struct{
	NodeType *pt;			/*指向找到的結點*/
	int i;					/*在結點中的關鍵碼序號,結點序號區間[1…m]*/
	int tag;				/* 1:查詢成功,0:查詢失敗*/
}Result;					/*B 樹的查詢結果型別*/

Result SearchBTree(NodeType *t,KeyType kx)
{ 
	/*在m 階B 樹t 上查詢關鍵碼kx,反回(pt,i,tag)。若查詢成功,則特徵值tag=1,*/
	/*指標pt 所指結點中第i 個關鍵碼等於kx;否則,特徵值tag=0,等於kx 的關鍵碼記錄*/
	/*應插入在指標pt 所指結點中第i 個和第i+1 個關鍵碼之間*/
	p=t;q=NULL;found=FALSE;i=0; /*初始化,p 指向待查結點,q 指向p 的雙親*/
	while(p&&!found)
	{	n=p->keynum;i=Search(p,kx);			/*在p-->key[1…keynum]中查詢*/
		if(i>0&&p->key[i]= =kx) found=TRUE; /*找到*/
		else {q=p;p=p->ptr[i];}
	}
	if(found) return (p,i,1);				/*查詢成功*/
	else return (q,i,0);					/*查詢不成功,反回kx 的插入位置資訊*/
}

B- 樹查詢演算法分析

從查詢演算法中可以看出, 在B- 樹中進行查詢包含兩種基本操作:

        ( 1) 在B- 樹中查詢結點;

        ( 2) 在結點中查詢關鍵字。

       由於B- 樹通常儲存在磁碟上, 則前一查詢操作是在磁碟上進行的, 而後一查詢操作是在記憶體中進行的, 即在磁碟上找到指標p 所指結點後, 先將結點中的資訊讀入記憶體, 然後再利用順序查詢或折半查詢查詢等於K 的關鍵字。顯然, 在磁碟上進行一次查詢比在記憶體中進行一次查詢的時間消耗多得多.

      因此, 在磁碟上進行查詢的次數、即待查詢關鍵字所在結點在B- 樹上的層次樹, 是決定B樹查詢效率的首要因素

        那麼,對含有n 個關鍵碼的m 階B-樹,最壞情況下達到多深呢?可按二叉平衡樹進行類似分析。首先,討論m 階B-數各層上的最少結點數。

       由B樹定義:B樹包含n個關鍵字。因此有n+1個樹葉都在第J+1 層。

    1)第一層為根,至少一個結點,根至少有兩個孩子,因此在第二層至少有兩個結點。

    2)除根和樹葉外,其它結點至少有[m/2]個孩子,因此第三層至少有2*[m/2]個結點,在第四層至少有2*[m/2]2 個結點…

    3)那麼在第J+1層至少有2*[m/2]J-1個結點,而J+1層的結點為葉子結點,於是葉子結點的個數n+1。有:

          

        也就是說在n個關鍵字的B樹查詢,從根節點到關鍵字所在的節點所涉及的節點數不超過:

     

3.B-樹的插入

  B-樹的生成也是從空樹起,逐個插入關鍵字而得。但由於B-樹結點中的關鍵字個數必須≥ceil(m/2)-1,因此,每次插入一個關鍵字不是在樹中新增一個葉子結點,而是首先在最低層的某個非終端結點中新增一個關鍵字,若該結點的關鍵字個數不超過m-1,則插入完成,否則要產生結點的“分裂”,

如圖(a) 為3階的B-樹(圖中略去F結點(即葉子結點)),假設需依次插入關鍵字30,26,85。


1) 首先通過查詢確定插入的位置。由根*a 起進行查詢,確定30應插入的在*d 節點中。由於*d 中關鍵字數目不超過2(即m-1),故第一個關鍵字插入完成:如(b)


2) 同樣,通過查詢確定關鍵字26亦應插入 *d. 由於*d節點關鍵字數目超過2,此時需要將 *d分裂成兩個節點,關鍵字26及其前、後兩個指標仍保留在 *d 節點中,而關鍵字37 及其前、後兩個指標儲存到新的產生的節點 *d` 中。同時將關鍵字30 和指示節點 *d `的指標插入到其雙親的節點中。由於 *b節點中的關鍵字數目沒有超過2,則插入完成.如(c)(d)


3) (e) -(g) 為插入85後;


插入演算法:

int InserBTree(NodeType **t,KeyType kx,NodeType *q,int i){ 
	/* 在m 階B 樹*t 上結點*q 的key[i],key[i+1]之間插入關鍵碼kx*/ 
	/*若引起結點過大,則沿雙親鏈進行必要的結點分裂調整,使*t仍為m 階B 樹*/
	x=kx;ap=NULL;finished=FALSE;
	while(q&&!finished)
	{ 
		Insert(q,i,x,ap);				/*將x 和ap 分別插入到q->key[i+1]和q->ptr[i+1]*/
		if(q->keynum<m) finished=TRUE;	/*插入完成*/
		else
		{								/*分裂結點*p*/
			s=m/2;split(q,ap);x=q->key[s];
			/*將q->key[s+1…m],q->ptr[s…m]和q->recptr[s+1…m]移入新結點*ap*/
			q=q->parent;
			if(q) i=Search(q,kx); /*在雙親結點*q 中查詢kx 的插入位置*/
		}
	}
	if(!finished)			/*(*t)是空樹或根結點已分裂為*q*和ap*/
	NewRoot(t,q,x,ap); /*生成含資訊(t,x,ap)的新的根結點*t,原*t 和ap 為子樹指標*/
}
4. B-樹的刪除

      反之,若在B-樹上刪除一個關鍵字,則首先應找到該關鍵字所在結點,並從中刪除之,若該結點為最下層的非終端結點,且其中的關鍵字數目不少於ceil(m/2),則刪除完成,否則要進行“合併”結點的操作。假若所刪關鍵字為非終端結點中的Ki,則可以指標Ai所指子樹中的最小關鍵字Y替代Ki,然後在相應的結點中刪去Y。例如,在下圖  圖4.1( a)的B-樹上刪去45,可以*f結點中的50替代45,然後在*f結點中刪去50。


                                圖4.1( a)

因此,下面我們可以只需討論刪除最下層非終端結點中的關鍵字的情形。有下列三種可能:

    (1)被刪關鍵字所在結點中的關鍵字數目不小於ceil(m/2),則只需從該結點中刪去該關鍵字Ki和相應指標Ai,樹的其它部分不變,例如,從圖  圖4.1( a)所示B-樹中刪去關鍵字12,刪除後的B-樹如圖  圖4.2( a)所示:


                           圖4.2( a)

   (2)被刪關鍵字所在結點中的關鍵字數目等於ceil(m/2)-1,而與該結點相鄰的右兄弟(或左兄弟)結點中的關鍵字數目大於ceil(m/2)-1,則需將其兄弟結點中的最小(或最大)的關鍵字上移至雙親結點中,而將雙親結點中小於(或大於)且緊靠該上移關鍵字的關鍵字下移至被刪關鍵字所在結點中。

[例如],從圖圖4.2( a)中刪去50,需將其右兄弟結點中的61上移至*e結點中,而將*e結點中的53移至*f,從而使*f和*g中關鍵字數目均不小於ceil(m-1)-1,而雙親結點中的關鍵字數目不變,如圖圖4.2(b)所示。


                               圖4.2(b)

       (3)被刪關鍵字所在結點和其相鄰的兄弟結點中的關鍵字數目均等於ceil(m/2)-1。假設該結點有右兄弟,且其右兄弟結點地址由雙親結點中的指標Ai所指,則在刪去關鍵字之後,它所在結點中剩餘的關鍵字和指標,加上雙親結點中的關鍵字Ki一起,合併到 Ai所指兄弟結點中(若沒有右兄弟,則合併至左兄弟結點中)。

[例如],從圖4.2(b)所示 B-樹中刪去53,則應刪去*f結點,並將*f中的剩餘資訊(指標“空”)和雙親*e結點中的 61一起合併到右兄弟結點*g中。刪除後的樹如圖4.2(c)所示。

 

                                圖4.2(c)

 如果因此使雙親結點中的關鍵字數目小於ceil(m/2)-1,則依次類推。

[例如],在 圖4.2(c)的B-樹中刪去關鍵字37之後,雙親b結點中剩餘資訊(“指標c”)應和其雙親*a結點中關鍵字45一起合併至右兄弟結點*e中,刪除後的B-樹如圖 4.2(d)所示。  

                         圖 4.2(d)

B-樹主要應用在檔案系統

為了將大型資料庫檔案儲存在硬碟上 以減少訪問硬碟次數為目的 在此提出了一種平衡多路查詢樹——B-樹結構 由其效能分析可知它的檢索效率是相當高的 為了提高 B-樹效能’還有很多種B-樹的變型,力圖對B-樹進行改進

B+樹

      B+樹是應檔案系統所需而產生的一種B-樹的變形樹。一棵m 階的B+樹和m 階的B-
樹的差異在於:
⑴有n 棵子樹的結點中含有n 個關鍵碼;
⑵所有的葉子結點中包含了全部關鍵碼的資訊,及指向含有這些關鍵碼記錄的指標,且
葉子結點本身依關鍵碼的大小自小而大的順序連結。
⑶所有的非終端結點可以看成是索引部分,結點中僅含有其子樹根結點中最大(或最小)關鍵碼。
 如圖一棵3階的B+樹:
通常在B+樹上有兩個頭指標,一個指向根節點,另一個指向關鍵字最小的葉子節點。因此可以對B+樹進行兩種查詢運算:一種是從最小關鍵字起順序查詢,另一種是從根節點開始,進行隨機查詢。  在B+樹上進行隨機查詢、插入和刪除的過程基本上與B-樹類似。只是在查詢時,若非終端結點上的關鍵碼等於給定值,並不終止,而是繼續向下直到葉子結點。因此,在B+
樹,不管查詢成功與否,每次查詢都是走了一條從根到葉子結點的路徑。

B+樹在資料庫中的應用

1. 索引在資料庫中的作用 

        在資料庫系統的使用過程當中,資料的查詢是使用最頻繁的一種資料操作。

        最基本的查詢演算法當然是順序查詢(linear search),遍歷表然後逐行匹配行值是否等於待查詢的關鍵字,其時間複雜度為O(n)。但時間複雜度為O(n)的演算法規模小的表,負載輕的資料庫,也能有好的效能。  但是資料增大的時候,時間複雜度為O(n)的演算法顯然是糟糕的,效能就很快下降了。

       好在電腦科學的發展提供了很多更優秀的查詢演算法,例如二分查詢(binary search)、二叉樹查詢(binary tree search)等。如果稍微分析一下會發現,每種查詢演算法都只能應用於特定的資料結構之上,例如二分查詢要求被檢索資料有序,而二叉樹查詢只能應用於二叉查詢樹上,但是資料本身的組織結構不可能完全滿足各種資料結構(例如,理論上不可能同時將兩列都按順序進行組織),所以,在資料之外,資料庫系統還維護著滿足特定查詢演算法的資料結構,這些資料結構以某種方式引用(指向)資料,這樣就可以在這些資料結構上實現高階查詢演算法。這種資料結構,就是索引。

       索引是對資料庫表 中一個或多個列的值進行排序的結構。與在表 中搜索所有的行相比,索引用指標 指向儲存在表中指定列的資料值,然後根據指定的次序排列這些指標,有助於更快地獲取資訊。通常情 況下 ,只有當經常查詢索引列中的資料時 ,才需要在表上建立索引。索引將佔用磁碟空間,並且影響數 據更新的速度。但是在多數情況下 ,索引所帶來的資料檢索速度優勢大大超過它的不足之處。

2. B+樹在資料庫索引中的應用


目前大部分資料庫系統及檔案系統都採用B-Tree或其變種B+Tree作為索引結構

1)在資料庫索引的應用

在資料庫索引的應用中,B+樹按照下列方式進行組織   :

①  葉結點的組織方式 。B+樹的查詢鍵 是資料檔案的主鍵 ,且索引是稠密的。也就是說 ,葉結點 中為資料檔案的第一個記錄設有一個鍵、指標對 ,該資料檔案可以按主鍵排序,也可以不按主鍵排序 ;資料檔案按主鍵排序,且 B +樹是稀疏索引 ,  在葉結點中為資料檔案的每一個塊設有一個鍵、指標對 ;資料檔案不按鍵屬性排序 ,且該屬性是 B +樹 的查詢鍵 , 葉結點中為資料檔案裡出現的每個屬性K設有一個鍵 、 指標對 , 其中指標執行排序鍵值為 K的 記錄中的第一個。

非葉結點 的組織方式。B+樹 中的非葉結點形成 了葉結點上的一個多級稀疏索引。  每個非葉結點中至少有ceil( m/2 ) 個指標 , 至多有 m 個指標 。  

2)B+樹索引的插入和刪除

①在向資料庫中插入新的資料時,同時也需要向資料庫索引中插入相應的索引鍵值 ,則需要向 B+樹 中插入新的鍵值。即上面我們提到的B-樹插入演算法。

②當從資料庫中刪除資料時,同時也需要從資料庫索引中刪除相應的索引鍵值 ,則需要從 B+樹 中刪 除該鍵值 。即B-樹刪除演算法

為什麼使用B-Tree(B+Tree)

     二叉查詢樹進化品種的紅黑樹等資料結構也可以用來實現索引,但是檔案系統及資料庫系統普遍採用B-/+Tree作為索引結構。

 一般來說,索引本身也很大,不可能全部儲存在記憶體中,因此索引往往以索引檔案的形式儲存的磁碟上。這樣的話,索引查詢過程中就要產生磁碟I/O消耗,相對於記憶體存取,I/O存取的消耗要高几個數量級,所以評價一個數據結構作為索引的優劣最重要的指標就是在查詢過程中磁碟I/O操作次數的漸進複雜度。換句話說,索引的結構組織要儘量減少查詢過程中磁碟I/O的存取次數。為什麼使用B-/+Tree,還跟磁碟存取原理有關。

       區域性性原理與磁碟預讀

  由於儲存介質的特性,磁碟本身存取就比主存慢很多,再加上機械運動耗費,磁碟的存取速度往往是主存的幾百分分之一,因此為了提高效率,要儘量減少磁碟I/O。為了達到這個目的,磁碟往往不是嚴格按需讀取,而是每次都會預讀,即使只需要一個位元組,磁碟也會從這個位置開始,順序向後讀取一定長度的資料放入記憶體。這樣做的理論依據是電腦科學中著名的區域性性原理:

  當一個數據被用到時,其附近的資料也通常會馬上被使用。

  程式執行期間所需要的資料通常比較集中。

  由於磁碟順序讀取的效率很高(不需要尋道時間,只需很少的旋轉時間),因此對於具有區域性性的程式來說,預讀可以提高I/O效率。

  預讀的長度一般為頁(page)的整倍數。頁是計算機管理儲存器的邏輯塊,硬體及作業系統往往將主存和磁碟儲存區分割為連續的大小相等的塊,每個儲存塊稱為一頁(在許多作業系統中,頁得大小通常為4k),主存和磁碟以頁為單位交換資料。當程式要讀取的資料不在主存中時,會觸發一個缺頁異常,此時系統會向磁碟發出讀盤訊號,磁碟會找到資料的起始位置並向後連續讀取一頁或幾頁載入記憶體中,然後異常返回,程式繼續執行。

      我們上面分析B-/+Tree檢索一次最多需要訪問節點:

     h =

   

      資料庫系統巧妙利用了磁碟預讀原理,將一個節點的大小設為等於一個頁,這樣每個節點只需要一次I/O就可以完全載入。為了達到這個目的,在實際實現B- Tree還需要使用如下技巧:

      每次新建節點時,直接申請一個頁的空間,這樣就保證一個節點物理上也儲存在一個頁裡,加之計算機儲存分配都是按頁對齊的,就實現了一個node只需一次I/O。

  B-Tree中一次檢索最多需要h-1次I/O(根節點常駐記憶體),漸進複雜度為O(h)=O(logmN)。一般實際應用中,m是非常大的數字,通常超過100,因此h非常小(通常不超過3)。

  綜上所述,用B-Tree作為索引結構效率是非常高的。

  而紅黑樹這種結構,h明顯要深的多。由於邏輯上很近的節點(父子)物理上可能很遠,無法利用區域性性,所以紅黑樹的I/O漸進複雜度也為O(h),效率明顯比B-Tree差很多。

MySQL的B-Tree索引(技術上說B+Tree)

       在 MySQL 中,主要有四種類型的索引,分別為: B-Tree 索引, Hash 索引, Fulltext 索引和 R-Tree 索引。我們主要分析B-Tree 索引。

        B-Tree 索引是 MySQL 資料庫中使用最為頻繁的索引型別,除了 Archive 儲存引擎之外的其他所有的儲存引擎都支援 B-Tree 索引。Archive 引擎直到 MySQL 5.1 才支援索引,而且只支援索引單個 AUTO_INCREMENT 列。

       不僅僅在 MySQL 中是如此,實際上在其他的很多資料庫管理系統中B-Tree 索引也同樣是作為最主要的索引型別,這主要是因為 B-Tree 索引的儲存結構在資料庫的資料檢索中有非常優異的表現。

     一般來說, MySQL 中的 B-Tree 索引的物理檔案大多都是以 Balance Tree 的結構來儲存的,也就是所有實際需要的資料都存放於 Tree 的 Leaf Node(葉子節點) ,而且到任何一個 Leaf Node 的最短路徑的長度都是完全相同的,所以我們大家都稱之為 B-Tree 索引。當然,可能各種資料庫(或 MySQL 的各種儲存引擎)在存放自己的 B-Tree 索引的時候會對儲存結構稍作改造。如 Innodb 儲存引擎的 B-Tree 索引實際使用的儲存結構實際上是 B+Tree,也就是在 B-Tree 資料結構的基礎上做了很小的改造,在每一個Leaf Node 上面出了存放索引鍵的相關資訊之外,還儲存了指向與該 Leaf Node 相鄰的後一個 LeafNode 的指標資訊(增加了順序訪問指標),這主要是為了加快檢索多個相鄰 Leaf Node 的效率考慮。

下面主要討論MyISAM和InnoDB兩個儲存引擎的索引實現方式:

1. MyISAM索引實現:

1)主鍵索引:

MyISAM引擎使用B+Tree作為索引結構,葉節點的data域存放的是資料記錄的地址。下圖是MyISAM主鍵索引的原理圖:


                                                                           (圖myisam1)

這裡設表一共有三列,假設我們以Col1為主鍵,圖myisam1是一個MyISAM表的主索引(Primary key)示意。可以看出MyISAM的索引檔案僅僅儲存資料記錄的地址。

2)輔助索引(Secondary key)

在MyISAM中,主索引和輔助索引(Secondary key)在結構上沒有任何區別,只是主索引要求key是唯一的,而輔助索引的key可以重複。如果我們在Col2上建立一個輔助索引,則此索引的結構如下圖所示:
  

同樣也是一顆B+Tree,data域儲存資料記錄的地址。因此,MyISAM中索引檢索的演算法為首先按照B+Tree搜尋演算法搜尋索引,如果指定的Key存在,則取出其data域的值,然後以data域的值為地址,讀取相應資料記錄。

MyISAM的索引方式也叫做“非聚集”的,之所以這麼稱呼是為了與InnoDB的聚集索引區分。

2. InnoDB索引實現

然InnoDB也使用B+Tree作為索引結構,但具體實現方式卻與MyISAM截然不同.

1)主鍵索引:

MyISAM索引檔案和資料檔案是分離的,索引檔案僅儲存資料記錄的地址。而在InnoDB中,表資料檔案本身就是按B+Tree組織的一個索引結構,這棵樹的葉節點data域儲存了完整的資料記錄。這個索引的key是資料表的主鍵,因此InnoDB表資料檔案本身就是主索引。

               (圖inndb主鍵索引)

(圖inndb主鍵索引)是InnoDB主索引(同時也是資料檔案)的示意圖,可以看到葉節點包含了完整的資料記錄。這種索引叫做聚集索引。因為InnoDB的資料檔案本身要按主鍵聚集,所以InnoDB要求表必須有主鍵(MyISAM可以沒有),如果沒有顯式指定,則MySQL系統會自動選擇一個可以唯一標識資料記錄的列作為主鍵,如果不存在這種列,則MySQL自動為InnoDB表生成一個隱含欄位作為主鍵,這個欄位長度為6個位元組,型別為長整形。

2). InnoDB的輔助索引

       InnoDB的所有輔助索引都引用主鍵作為data域。例如,下圖為定義在Col3上的一個輔助索引:

        InnoDB 表是基於聚簇索引建立的。因此InnoDB 的索引能提供一種非常快速的主鍵查詢效能。不過,它的輔助索引(Secondary Index, 也就是非主鍵索引)也會包含主鍵列,所以,如果主鍵定義的比較大,其他索引也將很大。如果想在表上定義 、很多索引,則爭取儘量把主鍵定義得小一些。InnoDB 不會壓縮索引。

      文字元的ASCII碼作為比較準則。聚集索引這種實現方式使得按主鍵的搜尋十分高效,但是輔助索引搜尋需要檢索兩遍索引:首先檢索輔助索引獲得主鍵,然後用主鍵到主索引中檢索獲得記錄。

      不同儲存引擎的索引實現方式對於正確使用和優化索引都非常有幫助,例如知道了InnoDB的索引實現後,就很容易明白為什麼不建議使用過長的欄位作為主鍵,因為所有輔助索引都引用主索引,過長的主索引會令輔助索引變得過大。再例如,用非單調的欄位作為主鍵在InnoDB中不是個好主意,因為InnoDB資料檔案本身是一顆B+Tree,非單調的主鍵會造成在插入新記錄時資料檔案為了維持B+Tree的特性而頻繁的分裂調整,十分低效,而使用自增欄位作為主鍵則是一個很好的選擇。

InnoDB索引MyISAM索引的區別:

一是主索引的區別,InnoDB的資料檔案本身就是索引檔案。而MyISAM的索引和資料是分開的。

二是輔助索引的區別:InnoDB的輔助索引data域儲存相應記錄主鍵的值而不是地址。而MyISAM的輔助索引和主索引沒有多大區別。