1. 程式人生 > 實用技巧 >MySQL和B+樹的那些事&mysql 索引原理

MySQL和B+樹的那些事&mysql 索引原理

一、零鋪墊

在介紹B樹之前,先來看另一棵神奇的樹——二叉排序樹(Binary Sort Tree),首先它是一棵樹,“二叉”這個描述已經很明顯了,就是樹上的一根樹枝開兩個叉,於是遞迴下來就是二叉樹了(下圖所示),而這棵樹上的節點是已經排好序的,具體的排序規則如下:

  • 若左子樹不空,則左子樹上所有節點的值均小於它的根節點的值
  • 若右子樹不空,則右子樹上所有節點的值均大於它的根節點的值
  • 它的左、右子樹也分別為二叉排序數(遞迴定義)

從圖中可以看出,二叉排序樹組織資料時,用於查詢是比較方便的,因為每次經過一次節點時,最多可以減少一半的可能,不過極端情況會出現所有節點都位於同一側,直觀上看就是一條直線,那麼這種查詢的效率就比較低了,因此需要對二叉樹左右子樹的高度進行平衡化處理,於是就有了平衡二叉樹(Balenced Binary Tree)。

所謂“平衡”,說的是這棵樹的各個分支的高度是均勻的,它的左子樹和右子樹的高度之差絕對值小於1,這樣就不會出現一條支路特別長的情況。於是,在這樣的平衡樹中進行查詢時,總共比較節點的次數不超過樹的高度,這就確保了查詢的效率(時間複雜度為O(logn))。

二、B樹的起源

B樹,最早是由德國電腦科學家Rudolf Bayer等人於1972年在論文 《Organization and Maintenance of Large Ordered Indexes》提出的,不過我去看了看原文,發現作者也沒有解釋為什麼就叫B-trees了,所以把B樹的B,簡單地解釋為Balanced或者Binary都不是特別嚴謹,也許作者就是取其名字Bayer的首字母命名的也說不定啊……

三、B樹長啥樣

還是直接看圖比較清楚,圖中所示,B樹事實上是一種平衡的多叉查詢樹,也就是說最多可以開m個叉(m>=2),我們稱之為m階b樹,為了體現本部落格的良心之處,不同於其他地方都能看到2階B樹,這裡特意畫了一棵5階B樹。

總的來說,m階B樹滿足以下條件:

  • 每個節點至多可以擁有m棵子樹
  • 根節點,只有至少有2個節點(要麼極端情況,就是一棵樹就一個根節點,單細胞生物,即是根,也是葉,也是樹)。
  • 非根非葉的節點至少有的Ceil(m/2)個子樹(Ceil表示向上取整,圖中5階B樹,每個節點至少有3個子樹,也就是至少有3個叉)。
  • 非葉節點中的資訊包括[n,A0,K1,A1,K2,A2,…,Kn,An],,其中n表示該節點中儲存的關鍵字個數,K為關鍵字且Ki<Ki+1,A為指向子樹根節點的指標。
  • 從根到葉子的每一條路徑都有相同的長度,也就是說,葉子節在相同的層,並且這些節點不帶資訊,實際上這些節點就表示找不到指定的值,也就是指向這些節點的指標為空。

B樹的查詢過程和二叉排序樹比較類似,從根節點依次比較每個結點,因為每個節點中的關鍵字和左右子樹都是有序的,所以只要比較節點中的關鍵字,或者沿著指標就能很快地找到指定的關鍵字,如果查詢失敗,則會返回葉子節點,即空指標。

例如查詢圖中字母表中的K

  1. 從根節點P開始,K的位置在P之前,進入左側指標
  2. 左子樹中,依次比較C、F、J、M,發現K在J和M之間
  3. 沿著J和M之間的指標,繼續訪問子樹,並依次進行比較,發現第一個關鍵字K即為指定查詢的值

四、Plus版——B+樹

作為B樹的加強版,B+樹與B樹的差異在於

  • 有n棵子樹的節點含有n個關鍵字(也有認為是n-1個關鍵字)
  • 所有的葉子節點包含了全部的關鍵字,及指向含這些關鍵字記錄的指標,且葉子節點本身根據關鍵字自小而大順序連線
  • 非葉子節點可以看成索引部分,節點中僅含有其子樹(根節點)中的最大(或最小)關鍵字

B+樹的查詢過程,與B樹類似,只不過查詢時,如果在非葉子節點上的關鍵字等於給定值,並不終止,而是繼續沿著指標直到葉子節點位置。因此在B+樹,不管查詢成功與否,每次查詢都是走了一條從根到葉子節點的路徑。

五、MySQL是如何使用B樹的

說明:事實上,在MySQL資料庫中,諸多儲存引擎使用的是B+樹,即便其名字看上去是BTREE。

1、innodb的索引機制

先以innodb儲存引擎為例,說明innodb引擎是如何利用B+樹建立索引的。首先建立一張表:zodiac,並插入一些資料

CREATE TABLE `zodiac` (  `id` int(11) NOT NULL AUTO_INCREMENT,  `name` char(4) NOT NULL,  PRIMARY KEY (`id`),  KEY `index_name` (`name`)); insert zodiac(id,name) values(1,''); &nbsp;insert zodiac(id,name) values(2,''); &nbsp;insert zodiac(id,name) values(3,''); &nbsp;insert zodiac(id,name) values(4,''); &nbsp;insert zodiac(id,name) values(5,''); &nbsp;insert zodiac(id,name) values(6,''); &nbsp;insert zodiac(id,name) values(7,''); &nbsp;insert zodiac(id,name) values(8,''); &nbsp;insert zodiac(id,name) values(9,'');insert zodiac(id,name) values(10,''); &nbsp;insert zodiac(id,name) values(11,''); &nbsp;insert zodiac(id,name) values(12,'');

對於innodb來說,只有一個數據檔案,這個資料檔案本身就是用B+樹形式組織B+樹每個節點的關鍵字就是表的主鍵,因此innodb的資料檔案本身就是主索引檔案,如下圖所示,主索引中的葉子頁(leaf page)包含了資料記錄,但非葉子節點只包含了主鍵,術語“聚簇”表示資料行和相鄰的鍵值緊湊地儲存在一起,因此這種索引被稱為聚簇索引,或聚集索引。

這種索引方式,可以提高資料訪問的速度,因為索引和資料是儲存在同一棵B樹之中,從聚簇索引中獲取資料通常比在非聚簇索引中要來得快。

所以可以說,innodb的資料檔案是依靠主鍵組織起來的,這也就是為什麼innodb引擎下建立的表,必須指定主鍵的原因,如果沒有顯式指定主鍵,innodb引擎仍然會對該表隱式地定義一個主鍵作為聚簇索引。

同樣innodb的輔助索引,如下圖所示,假設這些字元是按照生肖的順序排列的(其實我也不知道具體怎麼實現,不要在意這些細節,就是舉個例子),其葉子節點中也包含了記錄的主鍵,因此innodb引擎在查詢輔助索引的時候會查詢兩次,首先通過輔助索引得到主鍵值,然後再查詢主索引略微有點囉嗦。。。

2、MyISAM的索引機制

MyISAM引擎同樣也使用B+樹組織索引,如下圖所示,假設我們的資料不是按照之前的順序插入的,而是按照圖中的是順序插入表,可以看到MyISAM引擎下B+樹葉子節點中包含的是資料記錄的地址(可以簡單理解為“行號”),而MyISAM的輔助索引在結構上和主索引沒有本質的區別,同樣其葉子節點也包含了資料記錄的地址,稍微不同的是輔助索引的關鍵字是允許重複。

六、簡單對比

1、Innodb輔助索引的葉子節點儲存的不是地址,而是主鍵值,這樣的策略減少了當出現行移動或者資料頁分裂時輔助索引的維護工作,雖然使用主鍵值當作指標會讓輔助索引佔用更多空間,但好處是,Innodb在移動行時無需更新輔助索引中的主鍵值,而MyISAM需要調整其葉子節點中的地址。

2、innodb引擎下,資料記錄是儲存在B+樹的葉子節點(大小相當於磁碟上的頁)上,當插入新的資料時,如果主鍵的值是有序的,它會把每一條記錄都儲存在上一條記錄的後面,但是如果主鍵使用的是無序的數值,例如UUID,這樣在插入資料時Innodb無法簡單地把新的資料插入到最後,而是需要為這條資料尋找合適的位置,這就額外增加了工作,這就是innodb引擎寫入效能要略差於MyISAM的原因之一。

Innodb和MyISAM索引的抽象圖

一、索引的本質

MySQL官方對索引的定義為:索引(Index)是幫助MySQL高效獲取資料的資料結構。提取句子主幹,就可以得到索引的本質:索引是資料結構。

我們知道,資料庫查詢是資料庫的最主要功能之一。我們都希望查詢資料的速度能儘可能的快,因此資料庫系統的設計者會從查詢演算法的角度進行優化。最基本的查詢演算法當然是順序查詢(linear search),這種複雜度為O(n)的演算法在資料量很大時顯然是糟糕的,好在電腦科學的發展提供了很多更優秀的查詢演算法,例如二分查詢(binary search)、二叉樹查詢(binary tree search)等。如果稍微分析一下會發現,每種查詢演算法都只能應用於特定的資料結構之上,例如二分查詢要求被檢索資料有序,而二叉樹查詢只能應用於二叉查詢樹上,但是資料本身的組織結構不可能完全滿足各種資料結構(例如,理論上不可能同時將兩列都按順序進行組織),所以,在資料之外,資料庫系統還維護著滿足特定查詢演算法的資料結構,這些資料結構以某種方式引用(指向)資料,這樣就可以在這些資料結構上實現高階查詢演算法。這種資料結構,就是索引。

看一個例子:

上圖展示了一種可能的索引方式。左邊是資料表,一共有兩列七條記錄,最左邊的是資料記錄的實體地址(注意邏輯上相鄰的記錄在磁碟上也並不是一定物理相鄰的)。為了加快Col2的查詢,可以維護一個右邊所示的二叉查詢樹,每個節點分別包含索引鍵值和一個指向對應資料記錄實體地址的指標,這樣就可以運用二叉查詢在O(log2n)的複雜度內獲取到相應資料。

雖然這是一個貨真價實的索引,但是實際的資料庫系統幾乎沒有使用二叉查詢樹或其進化品種紅黑樹(red-black tree)實現的,原因會在下文介紹。

二、B-Tree 和 B+Tree

目前大部分資料庫系統及檔案系統都採用B-Tree或其變種B+Tree作為索引結構,在本文的下一節會結合儲存器原理及計算機存取原理討論為什麼B-Tree和B+Tree在被如此廣泛用於索引,這一節先單純從資料結構角度描述它們。

1、B-Tree

為了描述B-Tree,首先定義一條資料記錄為一個二元組[key, data],key為記錄的鍵值,對於不同資料記錄,key是互不相同的;data為資料記錄除key外的資料。那麼B-Tree是滿足下列條件的資料結構:

d為大於1的一個正整數,稱為B-Tree的度。

h為一個正整數,稱為B-Tree的高度。

每個非葉子節點由n-1個key和n個指標組成,其中d<=n<=2d。

每個葉子節點最少包含一個key和兩個指標,最多包含2d-1個key和2d個指標,葉節點的指標均為null 。

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

key和指標互相間隔,節點兩端是指標。

一個節點中的key從左到右非遞減排列。

所有節點組成樹結構。

每個指標要麼為null,要麼指向另外一個節點。

如果某個指標在節點node最左邊且不為null,則其指向節點的所有key小於v(key1),其中v(key1)為node的第一個key的值。

如果某個指標在節點node最右邊且不為null,則其指向節點的所有key大於v(keym),其中v(keym)為node的最後一個key的值。

如果某個指標在節點node的左右相鄰key分別是keyikeyi+1且不為null,則其指向節點的所有key小於v(keyi+1)且大於v(keyi)

下圖是一個d=2的B-Tree示意圖。

由於B-Tree的特性,在B-Tree中按key檢索資料的演算法非常直觀:首先從根節點進行二分查詢,如果找到則返回對應節點的data,否則對相應區間的指標指向的節點遞迴進行查詢,直到找到節點或找到null指標,前者查詢成功,後者查詢失敗。B-Tree上查詢演算法的虛擬碼如下:

BTree_Search(node, key) {
    if(node == null) return null;
    foreach(node.key){
        if(node.key[i] == key) return node.data[i];
        if(node.key[i] > key) return BTree_Search(point[i]->node);
    }
    return BTree_Search(point[i+1]->node);
}
data = BTree_Search(root, my_key);

關於B-Tree有一系列有趣的性質,例如一個度為d的B-Tree,設其索引N個key,則其樹高h的上限為logd((N+1)/2),檢索一個key,其查詢節點個數的漸進複雜度為O(logdN)。從這點可以看出,B-Tree是一個非常有效率的索引資料結構。

2、B+Tree

B-Tree有許多變種,其中最常見的是B+Tree,例如MySQL就普遍使用B+Tree實現其索引結構。

與B-Tree相比,B+Tree有以下不同點:

每個節點的指標上限為2d而不是2d+1。

內節點不儲存data,只儲存key;葉子節點不儲存指標。

下圖是一個簡單的B+Tree示意。

由於並不是所有節點都具有相同的域,因此B+Tree中葉節點和內節點一般大小不同。這點與B-Tree不同,雖然B-Tree中不同節點存放的key和指標可能數量不一致,但是每個節點的域和上限是一致的,所以在實現中B-Tree往往對每個節點申請同等大小的空間。

一般來說,B+Tree比B-Tree更適合實現外儲存索引結構,具體原因與外儲存器原理及計算機存取原理有關,將在下面討論。

3、帶有順序訪問指標的B+Tree

一般在資料庫系統或檔案系統中使用的B+Tree結構都在經典B+Tree的基礎上進行了優化,增加了順序訪問指標。

如上圖所示,在B+Tree的每個葉子節點增加一個指向相鄰葉子節點的指標,就形成了帶有順序訪問指標的B+Tree。做這個優化的目的是為了提高區間訪問的效能,例如上圖中如果要查詢key為從18到49的所有資料記錄,當找到18後,只需順著節點和指標順序遍歷就可以一次性訪問到所有資料節點,極大提到了區間查詢效率。

這一節對B-Tree和B+Tree進行了一個簡單的介紹,下一節結合儲存器存取原理介紹為什麼目前B+Tree是資料庫系統實現索引的首選資料結構。

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

上文說過,紅黑樹等資料結構也可以用來實現索引,但是檔案系統及資料庫系統普遍採用B-/+Tree作為索引結構,這一節將結合計算機組成原理相關知識討論B-/+Tree作為索引的理論基礎。

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

1、主存存取原理

目前計算機使用的主存基本都是隨機讀寫儲存器(RAM),現代RAM的結構和存取原理比較複雜,這裡本文拋卻具體差別,抽象出一個十分簡單的存取模型來說明RAM的工作原理。

從抽象角度看,主存是一系列的儲存單元組成的矩陣,每個儲存單元儲存固定大小的資料。每個儲存單元有唯一的地址,現代主存的編址規則比較複雜,這裡將其簡化成一個二維地址:通過一個行地址和一個列地址可以唯一定位到一個儲存單元。圖5展示了一個4 x 4的主存模型。

2、主存的存取過程如下:

當系統需要讀取主存時,則將地址訊號放到地址總線上傳給主存,主存讀到地址訊號後,解析訊號並定位到指定儲存單元,然後將此儲存單元資料放到資料匯流排上,供其它部件讀取。

寫主存的過程類似,系統將要寫入單元地址和資料分別放在地址匯流排和資料匯流排上,主存讀取兩個匯流排的內容,做相應的寫操作。

這裡可以看出,主存存取的時間僅與存取次數呈線性關係,因為不存在機械操作,兩次存取的資料的“距離”不會對時間有任何影響,例如,先取A0再取A1和先取A0再取D3的時間消耗是一樣的。

3、磁碟存取原理

上面說過,索引一般以檔案形式儲存在磁碟上,索引檢索需要磁碟I/O操作。與主存不同,磁碟I/O存在機械運動耗費,因此磁碟I/O的時間消耗是巨大的。

下圖是磁碟的整體結構示意圖。

一個磁碟由大小相同且同軸的圓形碟片組成,磁碟可以轉動(各個磁碟必須同步轉動)。在磁碟的一側有磁頭支架,磁頭支架固定了一組磁頭,每個磁頭負責存取一個磁碟的內容。磁頭不能轉動,但是可以沿磁碟半徑方向運動(實際是斜切向運動),每個磁頭同一時刻也必須是同軸的,即從正上方向下看,所有磁頭任何時候都是重疊的(不過目前已經有多磁頭獨立技術,可不受此限制)。

下圖是磁碟結構的示意圖。

碟片被劃分成一系列同心環,圓心是碟片中心,每個同心環叫做一個磁軌,所有半徑相同的磁軌組成一個柱面。磁軌被沿半徑線劃分成一個個小的段,每個段叫做一個扇區,每個扇區是磁碟的最小儲存單元。為了簡單起見,我們下面假設磁碟只有一個碟片和一個磁頭。

當需要從磁碟讀取資料時,系統會將資料邏輯地址傳給磁碟,磁碟的控制電路按照定址邏輯將邏輯地址翻譯成實體地址,即確定要讀的資料在哪個磁軌,哪個扇區。為了讀取這個扇區的資料,需要將磁頭放到這個扇區上方,為了實現這一點,磁頭需要移動對準相應磁軌,這個過程叫做尋道,所耗費時間叫做尋道時間,然後磁碟旋轉將目標扇區旋轉到磁頭下,這個過程耗費的時間叫做旋轉時間。

4、區域性性原理與磁碟預讀

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

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

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

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

5、B-/+Tree索引的效能分析

到這裡終於可以分析B-/+Tree索引的效能了。

上文說過一般使用磁碟I/O次數評價索引結構的優劣。先從B-Tree分析,根據B-Tree的定義,可知檢索一次最多需要訪問h個節點。資料庫系統的設計者巧妙利用了磁碟預讀原理,將一個節點的大小設為等於一個頁,這樣每個節點只需要一次I/O就可以完全載入。為了達到這個目的,在實際實現B-Tree還需要使用如下技巧:

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

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

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

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

上文還說過,B+Tree更適合外存索引,原因和內節點出度d有關。從上面分析可以看到,d越大索引的效能越好,而出度的上限取決於節點內key和data的大小:

dmax=floor(pagesize/(keysize+datasize+pointsize))dmax=floor(pagesize/(keysize+datasize+pointsize))

floor表示向下取整。由於B+Tree內節點去掉了data域,因此可以擁有更大的出度,擁有更好的效能。


B+Tree的定義

B+Tree是B樹的變種,有著比B樹更高的查詢效能,來看下m階B+Tree特徵:

1、有m個子樹的節點包含有m個元素(B-Tree中是m-1)。

2、根節點和分支節點中不儲存資料,只用於索引,所有資料都儲存在葉子節點中。

3、所有分支節點和根節點都同時存在於子節點中,在子節點元素中是最大或者最小的元素。

4、葉子節點會包含所有的關鍵字,以及指向資料記錄的指標,並且葉子節點本身是根據關鍵字的大小從小到大順序連結。

更直觀的圖

1、紅點表示是指向衛星資料的指標,指標指向的是存放實際資料的磁碟頁,衛星資料就是資料庫中一條資料記錄。

2、葉子節點中還有一個指向下一個葉子節點的next指標,所以葉子節點形成了一個有序的連結串列,方便遍歷B+樹。

B+樹的優勢

1、更加高效的單元素查詢

B+樹的查詢元素3的過程:

第一次磁碟IO

第二次磁碟IO

      

第三次磁碟IO

這個過程看下來,貌似與B樹的查詢過程沒有什麼區別。但實際上有兩點不一樣:

a、首先B+樹的中間節點不儲存衛星資料,所以同樣大小的磁碟頁可以容納更多的節點元素,如此一來,相同數量的資料下,B+樹就相對來說要更加矮胖些,磁碟IO的次數更少。

b、由於只有葉子節點才儲存衛星資料,B+樹每次查詢都要到葉子節點;而B樹每次查詢則不一樣,最好的情況是根節點,最壞的情況是葉子節點,沒有B+樹穩定。

2、葉子節點形成有順連結串列,範圍查詢效能更優

B樹範圍查詢3-8的過程

a、先查詢3

b、再查詢4、5、6、7、8,中間過程省略,直接到8的查詢

這裡查詢的範圍跨度越大,則磁碟IO的次數越多,效能越差。

B+樹範圍查詢3-11的過程

先從上到下找到下限元素3,然後通過連結串列指標,依次遍歷得到元素5/6/8/9/11;如此一來,就不用像B樹那樣一個個元素進行查詢。

總結

1.單節點可以儲存更多的元素,使得查詢磁碟IO次數更少。

2.所有查詢都要查詢到葉子節點,查詢效能穩定。

3.所有葉子節點形成有序連結串列,便於範圍查詢。

PS:在資料庫的聚集索引(Clustered Index)中,葉子節點直接包含衛星資料。在非聚集索引(NonClustered Index)中,葉子節點帶有指向衛星資料的指標。

一、索引的本質

MySQL官方對索引的定義為:索引(Index)是幫助MySQL高效獲取資料的資料結構。提取句子主幹,就可以得到索引的本質:索引是資料結構。

我們知道,資料庫查詢是資料庫的最主要功能之一。我們都希望查詢資料的速度能儘可能的快,因此資料庫系統的設計者會從查詢演算法的角度進行優化。最基本的查詢演算法當然是順序查詢(linear search),這種複雜度為O(n)的演算法在資料量很大時顯然是糟糕的,好在電腦科學的發展提供了很多更優秀的查詢演算法,例如二分查詢(binary search)、二叉樹查詢(binary tree search)等。

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

二、B-Tree(平衡多路查詢樹)

B-Tree是為磁碟等外儲存裝置設計的一種平衡查詢樹。因此在講B-Tree之前先了解下磁碟的相關知識。

系統從磁碟讀取資料到記憶體時是以磁碟塊(block)為基本單位的,位於同一個磁碟塊中的資料會被一次性讀取出來,而不是需要什麼取什麼。

InnoDB儲存引擎中有頁(Page)的概念,頁是其磁碟管理的最小單位。InnoDB儲存引擎中預設每個頁的大小為16KB,可通過引數innodb_page_size將頁的大小設定為4K、8K、16K,在MySQL中可通過如下命令檢視頁的大小:

mysql> show variables like 'innodb_page_size';

而系統一個磁碟塊的儲存空間往往沒有這麼大,因此InnoDB每次申請磁碟空間時都會是若干地址連續磁碟塊來達到頁的大小16KB。InnoDB在把磁碟資料讀入到磁碟時會以頁為基本單位,在查詢資料時如果一個頁中的每條資料都能有助於定位資料記錄的位置,這將會減少磁碟I/O次數,提高查詢效率。

B-Tree結構的資料可以讓系統高效的找到資料所在的磁碟塊。為了描述B-Tree,首先定義一條記錄為一個二元組[key, data] ,key為記錄的鍵值,對應表中的主鍵值,data為一行記錄中除主鍵外的資料。對於不同的記錄,key值互不相同。

一棵m階的B-Tree有如下特性:
1. 每個節點最多有m個孩子。
2. 除了根節點和葉子節點外,其它每個節點至少有Ceil(m/2)個孩子。
3. 若根節點不是葉子節點,則至少有2個孩子。
4. 所有葉子節點都在同一層,且不包含其它關鍵字資訊。
5. 每個非終端節點包含n個關鍵字資訊(P0,P1,…Pn, k1,…kn)
6. 關鍵字的個數n滿足:ceil(m/2)-1 <= n <= m-1
7. ki(i=1,…n)為關鍵字,且關鍵字升序排序。
8. Pi(i=1,…n)為指向子樹根節點的指標。P(i-1)指向的子樹的所有節點關鍵字均小於ki,但都大於k(i-1)。

B-Tree中的每個節點根據實際情況可以包含大量的關鍵字資訊和分支,如下圖所示為一個3階的B-Tree:

每個節點佔用一個盤塊的磁碟空間,一個節點上有兩個升序排序的關鍵字和三個指向子樹根節點的指標,指標儲存的是子節點所在磁碟塊的地址。兩個關鍵詞劃分成的三個範圍域對應三個指標指向的子樹的資料的範圍域。以根節點為例,關鍵字為17和35,P1指標指向的子樹的資料範圍為小於17,P2指標指向的子樹的資料範圍為17~35,P3指標指向的子樹的資料範圍為大於35。

模擬查詢關鍵字29的過程:

  1. 根據根節點找到磁碟塊1,讀入記憶體。【磁碟I/O操作第1次】
  2. 比較關鍵字29在區間(17,35),找到磁碟塊1的指標P2。
  3. 根據P2指標找到磁碟塊3,讀入記憶體。【磁碟I/O操作第2次】
  4. 比較關鍵字29在區間(26,30),找到磁碟塊3的指標P2。
  5. 根據P2指標找到磁碟塊8,讀入記憶體。【磁碟I/O操作第3次】
  6. 在磁碟塊8中的關鍵字列表中找到關鍵字29。

分析上面過程,發現需要3次磁碟I/O操作,和3次記憶體查詢操作。由於記憶體中的關鍵字是一個有序表結構,可以利用二分法查詢提高效率。而3次磁碟I/O操作是影響整個B-Tree查詢效率的決定因素。B-Tree相對於AVLTree縮減了節點個數,使每次磁碟I/O取到記憶體的資料都發揮了作用,從而提高了查詢效率。

三、B+Tree

B+Tree是在B-Tree基礎上的一種優化,使其更適合實現外儲存索引結構,InnoDB儲存引擎就是用B+Tree實現其索引結構。

從上一節中的B-Tree結構圖中可以看到每個節點中不僅包含資料的key值,還有data值。而每一個頁的儲存空間是有限的,如果data資料較大時將會導致每個節點(即一個頁)能儲存的key的數量很小,當儲存的資料量很大時同樣會導致B-Tree的深度較大,增大查詢時的磁碟I/O次數,進而影響查詢效率。在B+Tree中,所有資料記錄節點都是按照鍵值大小順序存放在同一層的葉子節點上,而非葉子節點上只儲存key值資訊,這樣可以大大加大每個節點儲存的key值數量,降低B+Tree的高度。

B+Tree相對於B-Tree有幾點不同:

  1. 非葉子節點只儲存鍵值資訊。
  2. 所有葉子節點之間都有一個鏈指標。
  3. 資料記錄都存放在葉子節點中。

將上一節中的B-Tree優化,由於B+Tree的非葉子節點只儲存鍵值資訊,假設每個磁碟塊能儲存4個鍵值及指標資訊,則變成B+Tree後其結構如下圖所示:

通常在B+Tree上有兩個頭指標,一個指向根節點,另一個指向關鍵字最小的葉子節點,而且所有葉子節點(即資料節點)之間是一種鏈式環結構。因此可以對B+Tree進行兩種查詢運算:一種是對於主鍵的範圍查詢和分頁查詢,另一種是從根節點開始,進行隨機查詢。

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

上文說過,紅黑樹等資料結構也可以用來實現索引,但是檔案系統及資料庫系統普遍採用B-/+Tree作為索引結構,這一節將結合計算機組成原理相關知識討論B-/+Tree作為索引的理論基礎。

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

主存存取原理

目前計算機使用的主存基本都是隨機讀寫儲存器(RAM),現代RAM的結構和存取原理比較複雜,這裡本文拋卻具體差別,抽象出一個十分簡單的存取模型來說明RAM的工作原理。

從抽象角度看,主存是一系列的儲存單元組成的矩陣,每個儲存單元儲存固定大小的資料。每個儲存單元有唯一的地址,現代主存的編址規則比較複雜,這裡將其簡化成一個二維地址:通過一個行地址和一個列地址可以唯一定位到一個儲存單元。圖5展示了一個4 x 4的主存模型。

主存的存取過程如下:

當系統需要讀取主存時,則將地址訊號放到地址總線上傳給主存,主存讀到地址訊號後,解析訊號並定位到指定儲存單元,然後將此儲存單元資料放到資料匯流排上,供其它部件讀取。

寫主存的過程類似,系統將要寫入單元地址和資料分別放在地址匯流排和資料匯流排上,主存讀取兩個匯流排的內容,做相應的寫操作。

這裡可以看出,主存存取的時間僅與存取次數呈線性關係,因為不存在機械操作,兩次存取的資料的“距離”不會對時間有任何影響,例如,先取A0再取A1和先取A0再取D3的時間消耗是一樣的。

磁碟存取原理

上面說過,索引一般以檔案形式儲存在磁碟上,索引檢索需要磁碟I/O操作。與主存不同,磁碟I/O存在機械運動耗費,因此磁碟I/O的時間消耗是巨大的。

下圖是磁碟的整體結構示意圖。

一個磁碟由大小相同且同軸的圓形碟片組成,磁碟可以轉動(各個磁碟必須同步轉動)。在磁碟的一側有磁頭支架,磁頭支架固定了一組磁頭,每個磁頭負責存取一個磁碟的內容。磁頭不能轉動,但是可以沿磁碟半徑方向運動(實際是斜切向運動),每個磁頭同一時刻也必須是同軸的,即從正上方向下看,所有磁頭任何時候都是重疊的(不過目前已經有多磁頭獨立技術,可不受此限制)。

下圖是磁碟結構的示意圖。

碟片被劃分成一系列同心環,圓心是碟片中心,每個同心環叫做一個磁軌,所有半徑相同的磁軌組成一個柱面。磁軌被沿半徑線劃分成一個個小的段,每個段叫做一個扇區,每個扇區是磁碟的最小儲存單元。為了簡單起見,我們下面假設磁碟只有一個碟片和一個磁頭。

當需要從磁碟讀取資料時,系統會將資料邏輯地址傳給磁碟,磁碟的控制電路按照定址邏輯將邏輯地址翻譯成實體地址,即確定要讀的資料在哪個磁軌,哪個扇區。為了讀取這個扇區的資料,需要將磁頭放到這個扇區上方,為了實現這一點,磁頭需要移動對準相應磁軌,這個過程叫做尋道,所耗費時間叫做尋道時間,然後磁碟旋轉將目標扇區旋轉到磁頭下,這個過程耗費的時間叫做旋轉時間。

區域性性原理與磁碟預讀

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

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

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

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

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

B-/+Tree索引的效能分析

到這裡終於可以分析B-/+Tree索引的效能了。

上文說過一般使用磁碟I/O次數評價索引結構的優劣。先從B-Tree分析,根據B-Tree的定義,可知檢索一次最多需要訪問h個節點。資料庫系統的設計者巧妙利用了磁碟預讀原理,將一個節點的大小設為等於一個頁,這樣每個節點只需要一次I/O就可以完全載入。為了達到這個目的,在實際實現B-Tree還需要使用如下技巧:

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

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

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

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

上文還說過,B+Tree更適合外存索引,原因和內節點出度d有關。從上面分析可以看到,d越大索引的效能越好,而出度的上限取決於節點內key和data的大小:

dmax=floor(pagesize/(keysize+datasize+pointsize))dmax=floor(pagesize/(keysize+datasize+pointsize))

floor表示向下取整。由於B+Tree內節點去掉了data域,因此可以擁有更大的出度,擁有更好的效能。

這一章從理論角度討論了與索引相關的資料結構與演算法問題,下一章將討論B+Tree是如何具體實現為MySQL中索引,同時將結合MyISAM和InnDB儲存引擎介紹非聚集索引和聚集索引兩種不同的索引實現形式。

五、聚簇索引與非聚簇索引

mysql中普遍使用B+Tree做索引,但在實現上又根據聚簇索引和非聚簇索引而不同。

1、聚簇索引

所謂聚簇索引,就是指主索引檔案和資料檔案為同一份檔案,聚簇索引主要用在Innodb儲存引擎中。在該索引實現方式中B+Tree的葉子節點上的data就是資料本身,key為主鍵,如果是一般索引的話,data便會指向對應的主索引,如下圖所示:

在B+Tree的每個葉子節點增加一個指向相鄰葉子節點的指標,就形成了帶有順序訪問指標的B+Tree。做這個優化的目的是為了提高區間訪問的效能,例如上圖中如果要查詢key為從18到49的所有資料記錄,當找到18後,只需順著節點和指標順序遍歷就可以一次性訪問到所有資料節點,極大提到了區間查詢效率。

2、非聚簇索引

非聚簇索引就是指B+Tree的葉子節點上的data,並不是資料本身,而是資料存放的地址。主索引和輔助索引沒啥區別,只是主索引中的key一定得是唯一的。主要用在MyISAM儲存引擎中,如下圖:

非聚簇索引比聚簇索引多了一次讀取資料的IO操作,所以查詢效能上會差。

六、MySQL索引實現

在MySQL中,索引屬於儲存引擎級別的概念,不同儲存引擎對索引的實現方式是不同的,下面主要討論MyISAM和InnoDB兩個儲存引擎的索引實現方式。

1、MyISAM索引實現

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

這裡設表一共有三列,假設我們以Col1為主鍵,則上圖是一個MyISAM表的主索引(Primary key)示意。可以看出MyISAM的索引檔案僅僅儲存資料記錄的地址。在MyISAM中,主索引和輔助索引(Secondary key)在結構上沒有任何區別,只是主索引要求key是唯一的,而輔助索引的key可以重複。如果我們在Col2上建立一個輔助索引,則此索引的結構如下圖所示:

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

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

2、InnoDB索引實現

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

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

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

第二個與MyISAM索引的不同是InnoDB的輔助索引data域儲存相應記錄主鍵的值而不是地址。換句話說,InnoDB的所有輔助索引都引用主鍵作為data域。例如,下圖為定義在Col3上的一個輔助索引:

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

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

對於InnoDB而言,因為節點下有資料檔案,因此節點的分裂將會比較慢。對於InnoDB的主鍵,儘量用整型,而且是遞增的整型。如果是無規律的資料,將會產生頁的分裂,影響速度。

InnoDB索引MyISAM索引的區別:

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

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

InnoDB的主索引檔案上,直接存放該行資料,稱為聚簇索引。次索引指向對主鍵的引用。

Myisam中,主索引和次索引都指向物理行。

補充:索引覆蓋

索引覆蓋是指如果查詢的列恰好是索引的一部分,那麼查詢只需要在索引檔案上進行,不需要回行到磁碟再找資料。這種查詢速度非常快,稱為“索引覆蓋”。