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-樹進行改進
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的輔助索引和主索引沒有多大區別。