大綱
1.索引原理
2.二叉查詢樹
3.平衡二叉樹(AVL樹)
4.紅黑樹
5.B-Tree
6.B+Tree
7.Hash索引
8.聚簇索引與非聚簇索引
1.索引原理
索引會在資料檔案中(ibd檔案),透過資料頁(Page)進行儲存。索引可以加快檢索速度,但也會降低增刪改速度,索引維護需要代價。
MySQL預設使用B+樹結構管理索引。B+樹中的B代表平衡,B+樹是由二叉樹、平衡二叉樹(AVL)和B-Tree逐步最佳化而來的。
2.二叉樹
(1)二叉樹的定義
(2)特殊的二叉樹
(3)二叉樹的儲存
(4)二叉樹的遍歷
(1)二叉樹的定義
二叉樹是n個結點的有限集合:或者為空二叉樹,即n=0。或者由一個根結點和兩個互不相交的被稱為根的左子樹和右子樹組成,左子樹和右子樹又分別是一棵二叉樹。
二叉樹的特點是每個結點至多有兩棵子樹,子樹分左右且次序不能顛倒。
二叉樹是有序樹,若將其左右子樹顛倒,則成為另一棵不同的二叉樹。
(2)特殊的二叉樹
一.滿二叉樹
一棵高度為h,且含有2^h - 1個結點的二叉樹稱為滿二叉樹。
二.完全二叉樹
高度為h、有n個結點的二叉樹,當且僅當其每個結點都與高度為h的滿二叉樹中編號為1~n的結點一一對應時,稱為完全二叉樹。
三.二叉排序樹
左子樹上所有結點的值均小於根結點的值。
右子樹上所有結點的值均大於根結點的值。
左子樹和右子樹又各是一棵二叉排序樹。
四.平衡二叉樹
樹上任一結點的左子樹和右子樹的高度差不超過1。
(3)二叉樹的儲存
一.完全二叉樹和滿二叉樹採用順序儲存比較合適
順序儲存指用一組地址連續的儲存單元依次儲存完全二叉樹上的元素,也就是將完全二叉樹上編號為i的元素儲存在一維陣列的下標為i-1的位置中。
二.普通二叉樹一般採用鏈式儲存比較合適
由於順序儲存的空間利用率較低,因此二叉樹一般採用鏈式儲存結構,鏈式儲存就是用連結串列結點來儲存二叉樹中的每個結點。
在二叉樹中,結點結構通常包括若干資料域和若干指標域。二叉連結串列至少包含3個域:資料域data、左指標域lchild、右指標域rchild。
(4)二叉樹的遍歷
一.先序遍歷
步驟1:訪問根結點;
步驟2:先序遍歷左子樹;
步驟3:先序遍歷右子樹;
public void preOrder(BiTree t) {
if (t != null) {
visit(t);//訪問根結點
preOrder(t.getLchild());//遞迴遍歷左子樹
preOrder(t.getRchild());//遞迴遍歷右子樹
}
}
二.中序遍歷
步驟1:中序遍歷左子樹;
步驟2:訪問根結點;
步驟3:中序遍歷右子樹;
public void inOrder(BiTree t) {
if (t != null) {
inOrder(t.getLchild());//遞迴遍歷左子樹
visit(t);//訪問根結點
inOrder(t.getRchild());//遞迴遍歷右子樹
}
}
三.後序遍歷
步驟1:後序遍歷左子樹;
步驟2:後序遍歷右子樹;
步驟3:訪問根結點;
public void postOrder(BiTree t) {
if (t != null) {
postOrder(t.getLchild());//遞迴遍歷左子樹
postOrder(t.getRchild());//遞迴遍歷右子樹
visit(t);//訪問根結點
}
}
例子如下所示:
3.二叉查詢樹(二叉排序樹BST)
(1)沒有索引的查詢
(2)使用二叉查詢樹進行最佳化
(3)二叉查詢樹的定義
(4)二叉查詢樹的查詢
(5)二叉查詢樹的插入
(6)二叉查詢樹的刪除
(7)二叉查詢樹的實現
(8)二叉查詢樹的缺點
(1)沒有索引的查詢
如下資料庫的一張表有兩列:分別是Col1和Col2。
查詢col2=87的這行資料,SQL語句如下:
mysql> select * from a where col2 = 87;
如果執行上面的查詢時沒有使用索引,那麼會從磁碟中掃描一條一條的資料進行對比來找到結果。當資料表很大且資料又在表尾的時候,需要花費很多時間進行檢索,效率比較低下。
(2)使用二叉查詢樹進行最佳化
為了加快查詢,可以維護一個二叉樹。
二叉樹具有的性質:
左子樹的鍵值小於根的鍵值,右子樹的鍵值大於根的鍵值(左小右大);
每個結點分別儲存欄位資料和一個指向對應資料記錄實體地址的指標;
這樣查詢時,就可以使用二叉樹查詢獲取相應的資料,從而快速檢索出符合條件的記錄。
對該二叉樹的結點進行查詢發現:深度為1的結點的查詢次數為1,深度為2的查詢次數為2,深度為n的結點的查詢次數為n。因此其平均查詢次數為:(1+2+2+3+3+3+3) / 6 = 2.8次。
(3)二叉查詢樹的定義
二叉排序樹(二叉查詢樹)或者是一棵空樹,或者是具有以下特性的二叉樹:
一.若左子樹非空,則左子樹上所有結點的值均小於根結點的值;
二.若右子樹非空,則右子樹上所有結點的值均大於根結點的值;
三.左右子樹也分別是一棵二叉排序樹;
根據二叉查詢樹的定義:左子樹結點值 < 根結點值 < 右子樹結點值,所以對二叉查詢樹進行中序遍歷,可以得到一個遞增的有序序列。
(4)二叉查詢樹的查詢
二叉排序樹的查詢是從根結點開始,沿某個分支逐層向下比較的過程。若二叉排序樹非空,先將給定值與根結點的值比較,如果相等,則查詢成功。
如果查詢結點值key小於根結點的值,則在根結點的左子樹上查詢。如果查詢結點值key大於根結點的值,則在根結點的右子樹上查詢。
public BSTNode bstSearch(BiTree t, ElemType key) {
while (t != null && key != t.getData()) {//若二叉查詢樹為空或等於根結點值,則結束迴圈
if (key < t.getData()) {
t = t.getLchild();//小於,則在左子樹上查詢
} else {
t = t.getRchild();//大於,則在右子樹上查詢
}
return T;
}
}
(5)二叉查詢樹的插入
二叉排序樹的特點是樹的結構通常不是一次生成的,而是在查詢過程中,發現樹中不存在結點值等於插入值的時候,再進行插入的。
插入的過程如下:
若原二叉排序樹為空,則直接插入結點,否則:
若插入結點值小於根結點值,則插入到左子樹;
若插入結點值大於根結點值,則插入到右子樹;
插入的結點一定是一個新新增的葉結點,而且是查詢失敗時的查詢路徑上訪問的最後一個結點的左孩子或右孩子。
public int bstInsert(BiTree t, KeyType key) {
if (t == null) {//原二叉查詢樹為空,新插入的記錄為根結點
t.data = key;
t.lchild = null;
t.rchild = null;
return 1;//返回1,插入成功
} else if (key == t.getData()) {//樹中存在相同值的結點,插入失敗
return 0;
} else if (key < t.getData()) {//插入到t的左子樹
return bstInsert(t.getLchild(), key);
} else {//插入到t的右子樹
return bstInsert(t.getRchild(), key);
}
}
(6)二叉查詢樹的刪除
一.若刪除結點沒有左右子樹,則直接刪除;
二.若刪除結點右子樹為空,則用左子女填補;
三.若刪除結點左子樹為空,則用右子女填補;
四.若刪除結點左右子樹均不空,則在右子樹上找中序第一個子女填補;
(7)二叉查詢樹的實現
public class BST<Key extends Comparable<Key>, Value> {
public static void main(String[] args) {
BST<Integer, Integer> bst = new BST<Integer, Integer>();
bst.put(10, 100);
bst.put(8, 80);
bst.put(12, 120);
bst.put(6, 60);
bst.put(9, 90);
bst.put(5, 50);
}
private Node root;//二叉查詢樹的根結點
private class Node {
private Key key;//鍵
private Value val;//值
private Node left, right;//左右子樹
private int n;//以該結點為根的子樹中的結點總數
public Node(Key key, Value val, int n) {
this.key = key;
this.val = val;
this.n = n;
}
}
public int size() {
return size(root);
}
private int size(Node x) {
if (x == null) {
return 0;
} else {
return x.n;
}
}
//查詢二叉排序樹的結點
public Value get(Key key) {
return get(root, key);
}
//在以x為根結點的子樹中查詢並返回key所對應的值,如果找不到則返回null
private Value get(Node x, Key key) {
if (x == null) {
return null;
}
int cmp = key.compareTo(x.key);
if (cmp < 0) {
return get(x.left, key);
} else if (cmp > 0) {
return get(x.right, key);
} else {
return x.val;
}
}
//查詢key,找到則更新它的值,否則為它建立一個新的結點
public void put(Key key, Value val) {
root = put(root, key, val);
}
//如果key存在於以x為根結點的子樹中則更新它的值
//否則將以key和val為鍵值對的新結點插入到該子樹中
private Node put(Node x, Key key, Value val) {
if (x == null) {
//新新增的結點,以該結點為根的子樹的結點個數為1
return new Node(key, val, 1);
}
int cmp = key.compareTo(x.key);
if (cmp < 0) {
x.left = put(x.left, key, val);
} else if (cmp > 0) {
x.right = put(x.right, key, val);
} else {
x.val = val;
}
x.n = size(x.left) + size(x.right) + 1;
return x;
}
}
(8)二叉查詢樹的缺點
MySQL索引底層使用的並不是二叉樹,因為二叉樹有個不平衡的缺點。在儲存有序的資料時,最終的排列結構會形成一個單向連結串列,如下圖示:樹的高度過高,這時候讀取某個指定結點的效率會很低。
測試連結:
https://www.cs.usfca.edu/~galles/visualization/BST.html
4.平衡二叉樹(AVL樹)
(1)平衡二叉樹的定義
(2)AVL樹與非AVL樹對比
(3)AVL樹的插入
(4)AVL樹的刪除
(5)AVL樹的優缺點
(1)平衡二叉樹的定義
一.定義
為了避免二叉排序樹的高度增長過快,降低二叉排序樹的效能。規定在插入和刪除二叉樹結點時,要保證任意結點左右子樹高度差不超1,這樣的二叉排序樹就稱為平衡二叉樹。
平衡二叉樹可定義為:
或者是一棵空樹,或者是具有下列性質的二叉樹,它的左子樹和右子樹都是平衡二叉樹,且左右子樹的高度差絕對值不超1。
二.總結
由於二叉查詢樹存在不平衡的問題,所以可以透過葉子結點自旋和調整,讓二叉樹始終保持基本平衡的狀態,這樣就能夠保持二叉查詢樹的最佳效能。
AVL樹就是基於以上思路的自動調整平衡的二叉樹。平衡二叉樹(AVL樹)在符合二叉查詢樹的條件下,還滿足任何結點的左右子樹的高度差最大為1。
(2)AVL樹與非AVL樹對比
左邊是AVL樹,它的任何結點的兩個子樹的高度差 <= 1。右邊不是AVL樹,其根結點的左子樹高度為3,而右子樹高度為1。
(3)AVL樹的插入
二叉排序樹保證平衡的基本思想如下:
每當在二叉排序樹中插入(或刪除)一個結點時,首先檢查其插入路徑上的結點是否因為此次操作而導致了不平衡。若導致不平衡,則先找到插入路徑上離插入結點最近的最小不平衡子樹。再調整最小不平衡子樹的各結點的位置關係,使之重新達到平衡。
AVL樹失去平衡後,可以透過旋轉使其恢復平衡。接下來介紹四種失去平衡的情況下對應的旋轉方式。下面所說的根結點,指的是最小不平衡子樹的根結點。
一.LL旋轉(右單旋轉)—根結點的左子樹高度比右子樹高度高2
插入的具體情況是:在根結點的左孩子(L)的左子樹(L)上插入新結點。
步驟1:原來根結點的左孩子作為新的根結點;
步驟2:原來根結點作為新根結點的右孩子;
步驟3:原來根結點的左孩子的右孩子作為新根結點的右孩子的左孩子;
注意:旋轉時是按插入結點的爺結點進行右單上旋;
二.RR旋轉(左單旋轉)—根結點的右子樹高度比左子樹高度高2
插入的具體情況是:在根結點的右孩子(R)的右子樹(R)上插入新結點。
步驟1:原根結點的右孩子作為新根結點;
步驟2:原根結點作為新根結點的左孩子;
步驟3:原來根結點的右孩子的左孩子作為新根結點的左孩子的右孩子;
注意:旋轉時是按插入結點的爺結點進行左單上旋;
三.LR旋轉(先左旋後右旋)
插入的具體情況是:在根結點的左孩子(L)的右子樹(R)上插入新結點。如果還是按插入結點的爺結點使用右單上旋,那麼旋轉後的高度差還是2。
所以,這種情況需要先左旋再右旋。左旋時是按插入結點的爺結點進行左下旋,右旋時是按插入結點的爺(父)結點進行右上旋。
情況一:
在根結點的左孩子(L)的右子樹(R)上插入左結點
情況二:
在根結點的左孩子(L)的右子樹(R)上插入右結點
四.RL旋轉(先右旋後左旋)
插入的具體情況是:在根結點的右孩子(R)的左子樹(L)上插入新結點。如果還是按插入結點的爺結點使用左上旋轉,那麼旋轉後的高度差還是2。
所以,這種情況需要先右旋再左旋。右旋時是按插入結點的爺結點進行右下旋,左旋時是按插入結點的爺(父)結點進行左上旋。
情況一:
在根結點的右孩子(R)的左子樹(L)上插入右結點
情況二:
在根結點的右孩子(R)的左子樹(L)上插入左結點
(4)AVL樹的刪除
以刪除結點w為例說明AVL樹刪除操作的步驟:
一.用二叉排序樹的方法對結點w執行刪除操作
二.從結點w開始向上回溯,找到第一個不平衡的結點z(最小不平衡子樹)
最小不平衡子樹的根結點為結點z,結點z的高度最高的孩子結點為y,結點y的高度最高的孩子結點為x。
三.對以結點z為根結點的最小不平衡子樹進行調整
其中結點x、結點y、結點z的位置情況有四種:
一.y在z的左,x在y的左(LL,右單旋轉)
二.y在z的右,x在y的右(RR,左單旋轉)
三.y在z的左,x在y的右(LR,先左旋後右旋)
四.y在z的右,x在y的左(RL,先右旋後左旋)
(5)AVL樹的優缺點
AVL樹的優點:
一.葉子結點的層級減少;
二.形態上能夠保持平衡;
三.查詢效率提升,大量的順序插入也不會導致查詢效能的降低;
AVL樹的缺點:
一.一個結點最多分裂出兩個子結點,樹的高度太高,導致IO次數過多;
二.結點裡面只儲存著一個關鍵字,每次操作獲取的目標資料太少;
4.紅黑樹
(1)紅黑樹的定義和性質
(2)紅黑樹的推論
(3)紅黑樹的插入
(4)紅黑樹的刪除
(1)紅黑樹的定義和性質
為了保持平衡二叉樹的平衡性,插入和刪除都要頻繁調整結點的位置。為此在平衡二叉樹的平衡標準上進一步放寬條件,引入紅黑樹的結構。
為了理解紅黑樹,對於n個結點的紅黑樹,會引入n+1個外部葉結點,以保證紅黑樹中每個結點(內部結點)的左右孩子均不為空,其中紅黑樹的葉結點是虛構的外部結點、是一個null結點。
一棵紅黑樹是滿足如下性質的二叉排序樹:
性質一:每個結點或是紅色或是黑色
性質二:根結點是黑色
性質三:葉結點都是黑色
性質四:紅結點的父結點和孩子結點都是黑色
性質五:每個結點到任一葉結點的簡單路徑上所含黑結點數量相同
從紅黑樹的某個結點出發(不含該結點),到達一個葉結點的任一簡單路徑上的黑結點總數稱為該結點的黑高(bh),紅黑樹的根結點的黑高稱為紅黑樹的黑高。
(2)紅黑樹的推論
推論一:從根結點到葉結點的最長路徑不大於最短路徑的2倍;
推論二:有n個內部結點的紅黑樹的高度h <= 2log(n+1);
推論三:新插入紅黑樹中的結點初始著色為紅色;
可見,紅黑樹的適度平衡,由平衡二叉樹的高度平衡,降低到任一結點左右子樹的高度,相差不超過2倍,從而降低了動態操作時,調整結點位置的頻率。
對於一棵二叉查詢樹,如果插入和刪除比較少,查詢比較多,可用AVL樹。如果插入和刪除比較多,那麼使用紅黑樹會更加合適。但由於維護平衡二叉樹的高度平衡所付出的代價比收益大,一般用紅黑樹。
假設新插入的結點初始著色為黑色,那麼這個結點所在的路徑就會比其他路徑多出一個黑結點,這樣就破壞了性質五,而且調整起來也比較麻煩。
如果新插入的結點初始著色為紅色,此時所有路徑上的黑結點數量不變,僅出現連續兩個紅結點才需要調整,這樣調整起來就比較簡單了。
(3)紅黑樹的插入
用二叉查詢樹的插入法插入結點z,並著色為紅色。
若插入結點z的父結點是黑色,則無須做任何調整,此時就是一棵標準紅黑樹;
若插入結點z的父結點是根結點,則將結點z著色為黑色(樹的黑高增加1);
若插入結點z不是根結點 + 插入結點z的父結點是紅色,則分如下三種情況處理:
情況一:插入結點z的叔結點y是黑色的,且插入結點z是一個左孩子
A.如果結點z是爺結點的左孩子(L)的左孩子(L)
首先對結點z的父結點做一次右上旋轉(LL,右單旋轉),然後交換結點z的原父結點和原爺結點的顏色。這樣就可以保持性質五,也不會改變紅黑樹的黑高,且紅黑樹也不會再有連續的兩個紅結點。
B.如果結點z是爺結點的右孩子(R)的左孩子(L)
首先對結點z的父結點做一次右下旋轉,這樣就會轉變為情況二B。然後再對結點z的新父結點做一次左下旋轉(RL,先右旋再左旋),最後交換結點z和右旋後結點z的父結點的顏色。
情況二:插入結點z的叔結點y是黑色的,且插入結點z是一個右孩子
A.如果結點z是爺結點的左孩子(L)的右孩子(R)
首先對結點z的父結點做一次左下旋轉,這樣就會轉變為情況一A。然後再對結點z的新父結點做一次右下旋轉(LR,先左旋再右旋),最後交換結點z和左旋後結點z的父結點的顏色。
B.如果結點z是爺結點的右孩子(R)的右孩子(R)
首先對結點z的父結點做一次左上旋轉(RR,左單旋轉),然後交換結點z的原父結點和原爺結點的顏色。這樣就可以保持性質五,也不會改變紅黑樹的黑高,且紅黑樹也不會再有連續的兩個紅結點。
情況三:插入結點z的叔結點y是紅色(插入結點z是左孩子還是右孩子不影響)
此時,結點z的父結點和叔結點y都是紅色,結點z的爺結點是黑色。所以將結點z的父結點和叔結點y著為黑色,將結點z的爺結點著為紅色。如果爺結點是根結點,那麼繼續著為黑色,這樣也能保持性質五。
插入結點的情況總結:
一.插入結點為根結點
直接將插入的結點變成黑色。
二.父結點為黑色結點
此時不需要任何操作。
三.父結點為紅色,叔結點為紅色
將叔結點和父結點改為黑色,爺結點改為紅色。然後又將爺結點當作插入結點看待,一直進行上面操作。直到當前結點為根結點,然後將根結點變成黑色。
四.父結點為紅色,叔結點為黑色
情況一:父結點為左,插入結點為左 || 父結點為右,插入結點為右
將父結點和爺結點的顏色互換,然後對爺結點進行一次右旋(左旋)。
情況二:父結點為左,插入結點為右 || 父結點為右,插入結點為左
首先對父結點進行左旋(右旋),左旋(右旋)後的情況必定是情況一,於是便可以按照情況一來進行處理。
(4)紅黑樹的刪除
一.刪除結點的過程原理
首先按二叉查詢樹的刪除方法進行刪除。
假設刪除結點有兩個孩子:
那麼根據二叉查詢樹的刪除結點的方法,要找刪除結點的中序後繼填補,也就是需要找刪除結點的右子樹中最小的結點和刪除結點進行位置交換,然後刪除交換位置後,在原來中序後繼結點位置的刪除結點。由於原來的中序後繼結點(因為是最小的)最多隻有一個孩子,於是就將刪除結點有兩孩子轉換為沒有孩子或者只有一個孩子的情況了。
假設刪除結點只有一個孩子:
由於刪除結點還有一個空的黑色葉結點,其唯一孩子結點也會有一個空的黑色葉結點,所以其唯一孩子結點必然是紅色。於是按照二叉查詢樹的刪除方法,用紅色的孩子結點替換刪除結點即可。
假設刪除結點沒有孩子:
如果刪除結點是紅色,可以直接刪除,無須調整;如果刪除結點是黑色,那麼有4種不同的情況;
二.刪除結點沒有孩子且是黑色的4種情況
假設刪除結點為y,經過二叉查詢樹的刪除方法刪除結點之後,會使用結點x來替換結點y(如果y是葉結點,那麼x是黑色的空葉結點)。刪除y結點後將導致先前包含結點y的任何路徑上的黑結點數量減1,因此結點y的任何祖先都不再滿足紅黑樹的性質五。
修正辦法就是將替換y的結點x視為還有額外一重黑色,定義為雙黑結點。也就是說,如果將任何包含結點x的路徑上的黑結點數量加1。在此假設下,性質五得到了滿足,但多出了一個雙黑結點,破壞了性質一。於是,刪除操作便可以轉化為將雙黑結點恢復為普通結點的操作。
情況一:x的兄弟結點w是紅色的
由於結點w是紅色,所以結點w的父結點和孩子結點必然是黑色的。於是交換結點w和結點x的父結點x.p的顏色,然後對x.p做一次左旋,這次左旋不會破壞紅黑樹的任何規則。
如下圖示,x的新兄弟結點是旋轉之前w的某個孩子結點,其顏色為黑色。這樣就將情況一轉換為情況二、情況三或情況四了。
關於上圖的補充說明:
假設從結點A出發到葉結點的普通路徑有n個黑色結點(包含出發結點),那麼從黑色結點B出發到葉結點的普通路徑就應該有n + 2個黑色結點,從紅色結點D出發到葉結點的普通路徑就應該有n + 1個黑色結點,從黑色結點C出發到葉結點的普通路徑就應該有n + 1個黑色結點,從黑色結點E出發到葉結點的普通路徑就應該有n + 1個黑色結點。
情況二:x的兄弟結點w是黑色的,w的右孩子是紅色,w的左孩子可紅可黑
由於x的兄弟結點w的右孩子是紅色,即紅結點是其爺結點的右孩子的右孩子。所以交換x的兄弟結點w和x的父結點x.p的顏色,然後把w的右孩子著為黑色,並對x的父結點x.p做一次左旋,最後就可以將x恢復為普通的黑色結點。此時紅黑樹的性質不會再收到破壞了,其中x的父結點x.p是黑還是紅不影響。
情況三:x的兄弟結點w是黑色的,w的右孩子是黑色,w的左孩子是紅色
由於x的兄弟結點w的左孩子是紅色,即紅結點是其爺結點的右孩子的左孩子,所以交換x的兄弟結點w和其左孩子的顏色,然後對x的兄弟結點w做一次右旋。這樣情況三就轉換為情況二了,此時x的父結點x.p是黑還是紅不影響。
情況四:x的兄弟結點w是黑色的,w的右孩子是黑色,w的左孩子也是黑色
因為w也是黑色的,所以可以將x和其兄弟結點w上去掉一重黑色,從而使得x只有一重黑色,而其兄弟結點w則變成紅色。
為了補償從x和w中去掉的一重黑色,可以把x的父結點x.p額外著一層黑色,從而保持區域性的黑高不變。
如果x.p是紅色,此時將x.p著為黑色即可;
如果x.p是黑色,則將x.p作為新結點x(x上升一層)來繼續迴圈處理;
總結:
情況一:x的兄弟結點w是紅色的
情況二:x的兄弟結點w是黑色的,w的右孩子是紅色,w的左孩子可紅可黑
情況三:x的兄弟結點w是黑色的,w的右孩子是黑色,w的左孩子是紅色
情況四:x的兄弟結點w是黑色的,w的右孩子是黑色,w的左孩子也是黑色
5.B-Tree
(1)B-Tree介紹
(2)B-Tree結構儲存索引的特點
(3)B-Tree的查詢操作
(4)B-Tree總結
因為AVL樹存在的缺陷,MySQL並沒有把它作為索引儲存的資料結構,如何透過降低樹的高度減少磁碟IO是資料庫提升效能的關鍵。如果每個結點多儲存一些資料,每次磁碟IO就能多載入一些資料到記憶體,B-Tree就是基於這樣的思想設計的。
(1)B-Tree介紹
B-Tree是一種平衡的多路查詢樹,B樹允許一個結點存放多個資料。這樣在儘可能減少樹的深度的同時,存放更多的資料,也就是把瘦高的樹變得矮胖。
B-Tree中所有結點的子樹個數的最大值稱為B-Tree的階,用m表示。一棵m階的B樹,如果不為空,就必須滿足以下條件。
m階的B-Tree要滿足以下條件:
一.每個結點最多擁有m個子樹
二.每個結點最多擁有m-1個資料
三.根結點至少有兩個子樹
四.分支結點至少有(m/2)棵子樹,防止變成二叉樹
五.所有葉子結點都在同一層,並且以升序排序
什麼是B-Tree的階?
所有結點中,結點[60, 70, 90]擁有的子結點數目最多。由於結點[60, 70, 90]擁有四個子結點(灰色結點),所以上面的B-Tree為4階B樹。
(2)B-Tree結構儲存索引的特點
為了描述B-Tree首先定義一條記錄為一個鍵值對[key, data],key為記錄的鍵值,對應表中的主鍵值(聚簇索引),data為一行記錄中除主鍵外的資料。對於不同的記錄,key值互不相同。
B-Tree結構儲存索引的特點如下:
一.索引值和data資料分佈在整棵樹的各個結點中
二.白色塊部分是指標,儲存著子結點的地址資訊
三.每個結點可以存放多個索引值及對應的data資料
四.樹結點中的多個索引值從左到右升序排列
(3)B-Tree的查詢操作
B-Tree的每個結點的元素可以視為一次IO讀取,樹的高度表示最多的IO次數。
在相同數量的總元素個數下:每個結點的元素個數越多,高度越低,查詢所需的IO次數越少。
B-Tree的查詢可以分為3步:
步驟1:首先要查詢結點
因為B-Tree通常是在磁碟上儲存的,所以這步需要進行磁碟IO操作。
步驟2:然後查詢關鍵字
當找到某個結點後將該結點讀入記憶體,然後透過順序或折半查詢來查詢關鍵字,如果命中就結束查詢。
步驟3:若沒有找到關鍵字,則需要判斷大小來找到合適的分支繼續查詢。如果已經找到了葉子結點,就結束查詢。
下面是在一個3階B-Tree中查詢元素21的過程:
演示地址: https://www.cs.usfca.edu/~galles/visualization/BTree.html
插入順序: 32 3 12 54 1 9 14 21 54 65 66
(4)B-Tree總結
優點:B樹可以在內部結點儲存鍵值和相關記錄資料,把頻繁訪問的資料放在靠近根結點位置,可提高熱點資料的查詢效率。
缺點:B樹中每個結點不僅包含資料的key值,還有data資料。所以當data資料較大時,會導致每個結點儲存的key值減少,並且導致B樹的層數變高,增加查詢時的IO次數。
使用場景:B樹主要應用於檔案系統以及部分資料庫索引,如MongoDB。大部分關係型資料庫索引則是使用B+樹實現。
6.B+Tree
(1)B+Tree的特徵
(2)B+Tree的優勢
(3)一棵B+Tree可以存放多少資料
B+Tree是在B-Tree基礎上的一種最佳化,使其更適合實現儲存索引結構。InnoDB儲存引擎就是用B+Tree實現其索引結構。
(1)B+Tree的特徵
特徵一:非葉子結點只儲存鍵值資訊
特徵二:所有葉子結點之間都有一個鏈指標
特徵三:資料記錄都存放在葉子結點中
(2)B+Tree的優點
優點一:B+Tree降低樹高和增大結點儲存資料量
B+Tree是B-Tree的變種,B-Tree能解決的問題,B+Tree也能夠解決。
優點二:B+Tree掃庫和掃表能力更強
對B-Tree的資料表的所有資料進行掃描,需要遍歷整棵樹。對B+Tree的資料表的所有資料進行掃描,只需遍歷所有葉子結點。因為B+Tree的葉子結點才存放完整的資料,以及之間有指標進行連結。
優點三:B+Tree磁碟讀寫能力更強,因根結點和分支結點不儲存資料
如果所有根結點和分支結點同樣大小,B+Tree儲存的key要比B-Tree多。所以B+Tree讀寫一次磁碟載入的關鍵字key要比B-Tree更多。
優點四:B+Tree排序能力更強
B+Tree天然具有排序功能。
優點五:B+Tree查詢效率更加穩定,每次查詢資料的IO次數相對穩定
當然如果直接看B-Tree可以根據根結點命中而直接返回,則效率更高。
(3)一棵B+Tree可以存放多少資料
一.B+樹是如何存放的
B+Tree的結點的大小等於一個資料頁的大小(16K)。B+Tree的根結點儲存在記憶體中,子結點才儲存在磁碟上。
設計一個結點等於一個頁的目的是每個結點只需一次IO就可完全載入。InnoDB的一個資料頁的大小是16K,所以每個結點的大小也是16K。
假設一個B+樹高為2,即存在一個根結點和若干個葉子結點。那麼這棵B+樹的存放總記錄數為:根結點指標數 * 單個葉子結點記錄行數。
二.計算根結點指標數
假設表的主鍵為int型別佔用4位元組,指標大小為6位元組。那麼一個頁即B+Tree中的一個結點,大概可以儲存的指標數量為:16384B / (4B + 6B) = 1638,一個結點最多可儲存1638個索引指標。
三.計算每個葉子結點的記錄數
假設一行記錄的資料大小為1K,那麼一個葉子結點就可以儲存16行資料:16K / 1K = 16。
四.一棵高度為2的B+Tree可以存放多少記錄數
一棵高度為2的B+Tree可以存放:1638 * 16 = 2.6萬條資料記錄,同理一棵高度為3的B+Tree可存放:1638 * 1638 * 16 = 4千萬條記錄。所以一個高度為3的B+Tree就可以滿足千萬級別的資料儲存。
InnoDB中的B+Tree的高度一般就是1~3層,由於查詢一次資料頁代表一次IO,所以透過主鍵索引查詢通常只需要1~3次IO即可查到資料。
7.Hash索引
(1)Hash索引
(2)Hsah索引的優點
(3)Hash索引的缺點
(1)Hash索引
MySQL中索引的資料結構有兩種:一種是B+Tree,另一種是Hash。Hash底層是由Hash表來實現的,根據鍵值儲存資料,非常適合根據key查詢value值,也就是單個key查詢,或者等值查詢。
對於每一行資料,儲存引擎都會對所有的索引列計算一個雜湊碼。雜湊碼是一個較小的值,如果出現雜湊碼值相同的情況會拉出一條連結串列。
(2)Hsah索引的優點
一.因為索引自身只需要儲存對應的Hash值,所以索引結構非常緊湊。只做等值查詢而不包含排序或範圍查詢的需求,都適合使用雜湊索引。
二.沒有雜湊衝突的情況下,等值查詢訪問雜湊索引的資料非常快。如果發生Hash衝突,必須遍歷連結串列中的所有行指標,逐行進行比較。
(3)Hash索引的缺點
缺點一:雜湊索引只包含雜湊值和行指標,不儲存欄位值,所以不能使用索引中的值來避免讀取行。
缺點二:雜湊索引只支援等值比較查詢,不支援任何範圍查詢和部分索引列匹配查詢。
缺點三:雜湊索引資料並不是按照索引值順序儲存的,所以也就無法用於排序。
缺點四:如果發生雜湊衝突,儲存引擎就必須遍歷連結串列,來逐行比較。
8.聚簇索引與非聚簇索引
(1)聚簇索引(主鍵索引)
(2)非聚簇索引(非主鍵索引或二級索引)
(3)使用聚簇索引時要注意的問題
(4)使用非聚簇索引要注意的問題
假設有一個表中的記錄如下,表中id是聚集索引,name是普通索引。
(1)聚簇索引(主鍵索引)
聚簇索引將資料儲存與索引放到了一塊,索引結構的葉子結點儲存了行資料。
聚簇索引是一種資料儲存方式,InnoDB的聚簇索引就是按照主鍵順序構建B+Tree結構。
B+Tree的葉子結點就是行記錄,行記錄和主鍵值緊湊地儲存在一起,這也意味著InnoDB的主鍵索引就是資料表本身。主鍵索引按主鍵順序存放了整張表的資料,佔用的空間是整個表的大小,通常說的主鍵索引就是聚簇索引。
InnoDB的表要求必須要有聚簇索引:
一.如果表定義了主鍵,則主鍵索引就是聚簇索引
二.如果表沒有定義主鍵,則第一個非空的唯一列作為聚簇索引
三.如果表沒有定義主鍵 + 也沒有非空的唯一列,則會建立一個隱藏的row-id作為聚簇索引
(2)非聚簇索引(非主鍵索引或二級索引)
非聚簇索引將資料與索引分開儲存,索引結構的葉子結點只儲存索引列和主鍵資訊。
InnoDB的二級索引,是根據索引列構建B+Tree結構,但在B+Tree的葉子結點中只儲存索引列和主鍵的資訊,所以二級索引佔用的空間會比聚簇索引小很多。通常建立二級索引就是為了提升查詢效率,一個表InnoDB只能建立一個聚簇索引,但可以建立多個二級索引。
(3)使用聚簇索引時要注意的問題
一.主鍵最好不要使用UUID
因為UUID的值太過離散,不適合排序且可能新增的UUID會插入在索引樹中間的位置,導致索引樹調整複雜度變大,消耗更多的時間和資源。
二.建議使用int型別的自增
方便排序且會在索引樹的末尾增加主鍵值,對索引樹的結構影響最小。主鍵值佔用的儲存空間越大,二級索引中儲存的主鍵值也會跟著變大,佔用的儲存空間會影響到IO操作讀取到的資料量。
(4)使用非聚簇索引要注意的問題
一.什麼是回表
下面來執行這樣一條SQL語句:
mysql> select * from A where name = 'ls';
在透過name進行查詢時,需要掃描兩遍索引樹:
一.先透過非聚簇索引定位到主鍵值 = 1
二.根據主鍵值在聚簇索引中定位到具體記錄
回表總結:先根據普通索引查詢主鍵值,再根據主鍵值在聚集索引中獲取行記錄。回表查詢,相對於只掃描一遍聚集索引樹的效能要低一些。
二.使用覆蓋索引來避免回表
什麼是覆蓋索引:
如果一個索引包含了所有需要查詢的欄位值,那麼該索引就是覆蓋索引。覆蓋索引是一種避免回表查詢的最佳化策略,只需在一棵索引樹上就能獲取所需的所有列資料,無需回表,速度更快。
覆蓋索引的實現方式:
對被查詢的欄位建立普通索引或聯合索引,查詢時直接返回索引資料。不需要再透過聚集索引去定位行記錄,避免了回表。