1. 程式人生 > >從二分查詢到B+樹索引原理

從二分查詢到B+樹索引原理

如果現在有一張表t,id為主鍵,有以下SQL語句:
--設在a列上建立了索引
select a from t where a >= 80;
select id, a from t where a >= 80;
select * from t where a >= 80;
--設建立了(a,b,c)聯合索引
select * from t where a = 4 and b = 5 and c > 6;
select * from t where a = 4 and b > 5 and c = 6;
select * from t where a > 4 and b = 5 and c = 6;
select
* from t where a = 4 and b != 5 and c = 6;
select * from t where a != 4 and b = 5 and c = 6;
--設在d列上建立了索引
select * from t where d like 'what%';
select * from t where d like '%what%';
--設在sex列上建立了索引,sex為性別,0表示男生,1表示女生
select * from t where sex = 1;
以上SQL能使用索引嗎,它們會如何使用索引呢?讓我們一步步從原理分析: 資料庫索引說白了就是加快查詢速度,當資料庫儲存的資料量比較大時,經常會遇到查詢時間特別長的問題,如何短時間內找到我們想要的資料呢? 不考慮資料的儲存形式,首選想到的可能是順序查詢,但是這種查詢方式實在太慢了!在最壞的情況下我們需要遍歷所有資料才能找到我們想要的資料,查詢的複雜度為O(N)! 進一步可以優化為二分查詢,二分查詢相信大家都會了,查詢的平均時間複雜度為O(logN),但它要求待查詢的資料必須是有序的。 還可以想到二叉查詢樹,二叉查詢樹的特點是對於每一個節點都滿足它的左子樹的節點的值都小於該節點的值,右子樹的節點的值都大於該節點的值。對於這樣一顆二叉樹,如果運氣好,查詢的平均時間複雜度為O(logN)。 二叉查詢樹
圖1 二叉查詢樹

可是,如果是下面這樣一顆查詢二叉樹呢?查詢的時間複雜度又退化到線性了。

查詢效率低下的一棵二叉查詢樹
圖2 查詢效率低下的一棵二叉查詢樹

怎麼優化呢?這時AVL樹(即自平衡二叉查詢樹)出場了,AVL樹不僅是一顆二叉查詢樹,它的每一個節點還滿足左子樹和右子樹的高度之差不超過1。這樣AVL樹就能保證良好的查詢效能,在最壞的情況下仍為O(logN)。但是這是有代價的,在插入和刪除節點時需要一次或多次旋轉以保持平衡,時間複雜度為O(logN)。

需要一次旋轉的情況
圖3 需要一次旋轉的情況(插入9)
需要兩次旋轉的情況
圖4 需要兩次旋轉的情況(插入3)

從AVL樹就能看到B+樹索引的一些“影子”了,從一定程度上就可以感受到為什麼索引會加快查詢速度但又會影響資料插入效能。

從二分查詢到二叉查詢樹,再到AVL樹,那麼資料庫可以用以上的資料結構來實現索引嗎?由於資料庫儲存的資料量實在太大,記憶體一次並不能完全讀入資料,所以資料庫採用了更適合的資料結構—B+樹來實現索引以加快查詢速度。(索引的實現非常多樣,本文僅以MySQL資料庫Innodb引擎B+樹索引為例)

直接上圖,按圖說原理:

B+樹
圖5 B+樹

B+樹可以理解為是為方便從磁碟存取資料而設計的平衡樹,如圖示是一棵高度為4的B+樹。所有記錄都在B+樹的葉子節點上,並按順序存放,

這裡順帶提一下B+樹和B樹的區別,需要指出的是網上許多部落格提出了B減樹的概念,這是錯誤的,B減樹是不存在的,只是好事者把B-Tree即B樹翻譯成了B減樹而已,他們的區別如下:

1.B+樹只有葉子節點會帶有指向記錄的指標,而B樹則是所有節點都有,在內部節點出現的索引項不會再出現在葉子節點中。
2.B+樹中所有葉子節點都是通過指標連線在一起,而B樹不會。

那麼B+樹為什麼要這麼做呢?
1.非葉子節點不帶有指向記錄的指標,則一個塊可以儲存更多的索引項,如此可以降低樹的高度。
2.葉子節點之間有指標連線,則在範圍掃描時避免了在內部節點來回移動。

B+樹
圖6 B+樹
B樹
圖7 B樹

那麼B+樹是如何加快查詢速度的呢?很明顯可以想到類似於二叉查詢樹查詢資料的方式,自頂向下遍歷樹,根據子指標指向的節點即可確定一個數據範圍,在節點內部則使用二分查詢來確定位置。

B+樹的層數一般在3-4層,這能保證良好的查詢效能,但是這也是有代價的,在插入和刪除操作時必須進行調整以維持B+樹的平衡。所以索引並不是建的越多越好,更不是每列都加上索引查詢就會變快,這是許多人會犯的一個錯誤,在討論群裡曾親眼看見開發人員這樣做,然後質問問什麼查詢這麼慢,後文將說明其中的原理。

B+樹的最下面一層稱為Leaf Page,非最下面一層稱為Index Page,B+樹在插入時,會區分Leaf Page是否滿和Index Page是否滿來做相應的調整。這裡為了說明索引是如何影響插入效能的,舉一個Leaf Page和Index Page都滿的情況:

插入鍵值100,Leaf Page和Index Page都需要做拆分:

Leaf Page和Index Page都需要拆分的情況
圖8 Leaf Page和Index Page都需要拆分的情況

為了降低拆頁頻率(拆頁意味著磁碟操作),B+樹也有旋轉操作,B+樹的刪除操作同理,也需要區分多種情況,可以認為是插入的逆操作,只不過是否觸發合併操作取決於填充因子(有資料值的鍵/該節點內所有鍵)大小,這裡都不再詳細說明了。

回到文章開頭提出的問題:

select a from t where a >= 80;
select id, a from t where a >= 80;
select * from t where a >= 80;
哪一句SQL會執行的更快呢?這時就要介紹聚集索引(或稱聚簇索引)和輔助索引(或稱二級索引、非聚集索引)了,還是直接上圖說明(以Innodb為例): 聚集索引和輔助索引
圖9 聚集索引和輔助索引

聚集索引的節點儲存的是主鍵的值,葉子節點還儲存有指向資料頁的偏移量,資料頁儲存完整的行記錄。輔助索引的所有節點儲存對應的列的值,但是在葉子節點還儲存了主鍵的值。如此當通過輔助索引來查詢資料時會在葉子節點獲得指向聚集索引的主鍵,然後再通過聚集索引來查詢完整的行記錄。

所以之前提到的”select a from t where a >= 80” 會比”select * from t where a >= 80”要快,同理”select id, a from t where a >= 80”由於包含了主鍵,不需要去聚集索引中查詢完整的行記錄,在輔助索引的葉子節點即可找到滿足要求的資料(包括主鍵的值)。需要指出的是,這裡討論的都是在Innodb引擎中,在MyISAM引擎中,這個規則不再適用,聚集索引和非聚集索引除了名字不同,沒有其它任何區別。

文章開頭提出的五句SQL:

select * from t where a = 4 and b = 5 and c > 6;
select * from t where a = 4 and b > 5 and c = 6;
select * from t where a > 4 and b = 5 and c = 6;
select * from t where a = 4 and b != 5 and c = 6;
select * from t where a != 4 and b = 5 and c = 6;

想要分析這五句SQL,又需要介紹一下聯合索引(多列索引)了,其實可以簡單的認為就是在上文介紹的輔助索引的基礎之上,每個節點儲存有多列的值,可以想象成長度相同的單詞按字典序進行排列,每個字母都表示一列。那麼聯合索引中列的順序就至關重要了,我們希望用盡量少的運算元篩選出更小的結果集合,所以需要將選擇度高的列放在前面。所謂選擇度,即COUNT(DISTINCT(列名))/COUNT(列名)。

那麼這時可以發現,上文提到的”select * from t where a = 4 and b = 5 and c > 6”將能很好的滿足聯合索引的順序,能很快篩選出結果集。

而”select * from t where a = 4 and b > 5 and c = 6”由於在索引的第二列使用了範圍查詢,使得該條SQL只能利用聯合索引(a, b, c)的前兩列。(想象查字典的過程,即可很好理解)

同理,”select * from t where a > 4 and b = 5 and c = 6”只可用到聯合索引的第一列,”select * from t where a = 4 and b != 5 and c = 6”由於在第二列上使用了”!=”,只能用到聯合索引(a, b, c)的第一列,”select * from t where a != 4 and b = 5 and c = 6”則不能用到聯合索引(a, b, c);

再來看前文提到的另一句SQL:

select * from t where sex = 1;

這條SQL會利用到在sex列上建立的索引嗎?答案是不會,這就要回到剛提到的選擇度的問題了,這裡的選擇度為COUNT(DISTINCT(sex))/COUNT(sex),分子不會超過2,而分母可能很大,選擇度將趨近於0,這時資料庫將放棄使用索引,而使用全表掃描。所以索引應當建立在選擇度較高的列,否則建了也白建。

再看剩下的兩條SQL:

select * from t where d like 'what%';
select * from t where d like '%what%';

如果在d列上建立了單列索引,這兩條SQL能利用到所建立的索引嗎?答案是第一條SQL會,而第二條SQL不會,這是因為索引的最左字首匹配原理。第一條SQL能通過前面已經限定的字串”what”在所建立的索引上按字典序篩選出結果集,而第二條需要掃描全部資料才能篩選出結果集。

類似的索引規則還有很多很多,下面摘出了一些:
* 較頻繁的作為查詢條件的欄位應該建立索引;唯一性太差的欄位不適合單獨建立索引,即使頻繁作為查詢條件;更新非常頻繁的欄位不適合建立索引;不會出現在 WHERE 子句中的欄位不該建立索引。

  • 使用短索引,如果對字串列進行索引,應該指定一個字首長度,可節省大量索引空間,提升查詢速度;

  • 應儘量避免在 where 子句中使用!=或\<>操作符,否則引擎將放棄使用索引而進行全表掃描。

  • 在使用索引欄位作為條件時,如果該索引是聯合索引,那麼必須使用到該索引中的第一個欄位作為條件時才能保證系統使用該索引,否則該索引將不會被使用,並且應儘可能的讓欄位順序與索引順序相一致。

  • 索引並不是越多越好,索引固然可 以提高相應的 select 的效率,但同時也降低了 insert 及 update 的效率,因為insert 或 update 時有可能會重建索引,所以怎樣建索引需要慎重考慮,視具體情況而定。一個表的索引數最好不要超過6個,若太多則應考慮一些不常使用到的列上建的索引是否有必要。儘量使用數字型欄位,若只含數值資訊的欄位儘量不要設計為字元型,這會降低查詢和連線的效能,並會增加儲存開銷。這是因為引擎在處理查詢和連線時會逐個比較字串中每一個字元,而對於數字型而言只需要比較一次就夠了。

  • 儘可能的使用 varchar/nvarchar 代替 char/nchar ,因為首先變長欄位儲存空間小,可以節省儲存空間,其次對於查詢來說,在一個相對較小的欄位內搜尋效率顯然要高些。

  • 儘量不要使用 select * from t ,用具體的欄位列表代替“*”,不要返回用不到的任何欄位。

  • ……

這樣的規則還有很多,在腦海裡記住索引的結構和原理,將非常有助於理解這些規則,或自己總結出一些規則。

除了B+樹索引,還有hash索引,空間索引等等,本文就不再討論了。因為我本人對MySQL的使用還處於入門階段,本文在一定程度上有紙上談兵之疑,算是一篇學習筆記吧,寫得不當之處歡迎拍磚~