淺談MySQL的B樹索引與索引優化

MySQL的MyISAM、InnoDB引擎默認均使用B+樹索引(查詢時都顯示爲「BTREE」),本文討論兩個問題:html

  • 爲何MySQL等主流數據庫選擇B+樹的索引結構?
  • 如何基於索引結構,理解常見的MySQL索引優化思路?

爲何索引沒法所有裝入內存

索引結構的選擇基於這樣一個性質:大數據量時,索引沒法所有裝入內存node

爲何索引沒法所有裝入內存?假設使用樹結構組織索引,簡單估算一下:mysql

  • 假設單個索引節點12B,1000w個數據行,unique索引,則葉子節點共佔約100MB,整棵樹最多200MB。
  • 假設一行數據佔用200B,則數據共佔約2G。

假設索引存儲在內存中。也就是說,每在物理盤上保存2G的數據,就要佔用200MB的內存,索引:數據的佔用比約爲1/10。1/10的佔用比算不算大呢?物理盤比內存廉價的多,以一臺內存16G硬盤1T的服務器爲例,若是要存滿1T的硬盤,至少須要100G的內存,遠大於16G。git

考慮到一個表上可能有多個索引、聯合索引、數據行佔用更小等狀況,實際的佔用比一般大於1/10,某些時候能達到1/3。在基於索引的存儲架構中,索引:數據的佔用比太高,所以,索引沒法所有裝入內存github

其餘結構的問題

因爲沒法裝入內存,則必然依賴磁盤(或SSD)存儲。而內存的讀寫速度是磁盤的成千上萬倍(與具體實現有關),所以,核心問題是「如何減小磁盤讀寫次數」。算法

首先不考慮頁表機制,假設每次讀、寫都直接穿透到磁盤,那麼:sql

  • 線性結構:讀/寫平均O(n)次
  • 二叉搜索樹(BST):讀/寫平均O(log2(n))次;若是樹不平衡,則最差讀/寫O(n)次
  • 自平衡二叉搜索樹(AVL):在BST的基礎上加入了自平衡算法,讀/寫最大O(log2(n))次
  • 紅黑樹(RBT):另外一種自平衡的查找樹,讀/寫最大O(log2(n))次

BST、AVL、RBT很好的將讀寫次數從O(n)優化到O(log2(n));其中,AVL和RBT都比BST多了自平衡的功能,將讀寫次數降到最大O(log2(n))。數據庫

假設使用自增主鍵,則主鍵自己是有序的,樹結構的讀寫次數可以優化到樹高,樹高越低讀寫次數越少;自平衡保證了樹結構的穩定。若是想進一步優化,能夠引入B樹和B+樹。緩存

B樹解決了什麼問題

不少文章將B樹誤稱爲B-(減)樹,這多是對其英文名「B-Tree」的誤解(更有甚者,將B樹稱爲二叉樹或二叉搜索樹)。特別是與B+樹一塊兒講的時候。想固然的認爲有B+(加)樹就有B-(減)樹,實際上B+樹的英文名是「B+-Tree」。服務器

若是拋開維護操做,那麼B樹就像一棵「m叉搜索樹」(m是子樹的最大個數),時間複雜度爲O(logm(n))。然而,B樹設計了一種高效簡單的維護操做,使B樹的深度維持在約log(ceil(m/2))(n)~logm(n)之間,大大下降樹高

image.png

再次強調:

不要糾結於時間複雜度,與單純的算法不一樣,磁盤IO次數纔是更大的影響因素。讀者能夠推導看看,B樹與AVL的時間複雜度是相同的,但因爲B樹的層數少,磁盤IO次數少,實踐中B樹的性能要優於AVL等二叉樹。

同二叉搜索樹相似,每一個節點存儲了多個key和子樹,子樹與key按順序排列。

頁表的目的是擴展內存+加速磁盤讀寫。一個頁(Page)一般4K(等於磁盤數據塊block的大小,見inode與block的分析),從磁盤讀寫的角度出發,操做系統每次以頁爲單位將內容從磁盤加載到內存(以攤分尋道成本),修改頁後,再擇期將該頁寫回磁盤。考慮到頁表的良好性質,可使每一個節點的大小約等於一個頁(使m很是大),這每次加載的一個頁就能完整覆蓋一個節點,以便選擇下一層子樹;對子樹同理。對於頁表來講,AVL(或RBT)至關於1個key+2個子樹的B樹,因爲邏輯上相鄰的節點,物理上一般不相鄰,所以,讀入一個4k頁,頁面內絕大部分空間都將是無效數據。

假設key、子樹節點指針均佔用4B,則B樹節點最大m * (4 + 4) = 8m B;頁面大小4KB。則m = 4 * 1024 / 8 = 512,一個512叉的B樹,1000w的數據,深度最大 log(512/2)(10^7) = 3.02 ~= 4。對比二叉樹如AVL的深度爲log(2)(10^7) = 23.25 ~= 24,相差了5倍以上。震驚!B樹索引深度居然如此!

另外,B樹對局部性原理很是友好。若是key比較小(好比上面4B的自增key),則除了頁表的加成,緩存還能進一步預讀加速。美滋滋~

B+樹解決了什麼問題

B樹的剩餘問題

然而,若是要實際應用到數據庫的索引中,B樹還有一些問題:

  1. 未定位數據行
  2. 沒法處理範圍查詢

問題1

數據表的記錄有多個字段,僅僅定位到主鍵是不夠的,還須要定位到數據行。有3個方案解決:

  1. 直接將key對應的數據行(可能對應多行)存儲在節點中。
  2. 數據行單獨存儲;節點中增長一個字段,定位key對應數據行的位置。
  3. 修改key與子樹的判斷邏輯,使子樹大於等於上一key小於下一key,最終全部訪問都將落於葉子節點;葉子節點中直接存儲數據行或數據行的位置。

方案1中,數據行一般很是大,存儲數據行將減小頁面中的子樹個數,m減少樹高增大。假設數據行佔用200B,可忽略組織B樹的指針,則新的m = 4 * 1024 / 200 = 20.48 ~= 21,深度最大 log(21/2)(10^7) ~= 7。增長了一倍以上的IO,不考慮。

方案2中,節點增長了一個字段。假設是4B的指針,則新的m = 4 * 1024 / 12 = 341.33 ~= 341,深度最大 log(341/2)(10^7) = 3.14 ~= 4。與3差異不大,能夠考慮。

方案3的節點m與深度不變,但時間複雜度變爲穩定的O(logm(n))。考慮。

問題2

實際業務中,範圍查詢的頻率很是高,B樹只能定位到一個索引位置(可能對應多行),很難處理範圍查詢。給出2種方案:

  1. 不改動:查詢的時候先查到左界,再查到右界,而後DFS(或BFS)遍歷左界、右界之間的節點。
  2. 在「問題1-方案3」的基礎上,因爲全部數據行都存儲在葉子節點,B樹的葉子節點自己也是有序的,能夠增長一個指針,指向當前葉子節點按主鍵順序的下一葉子節點;查詢時先查到左界,再查到右界,而後從左界到有界線性遍歷。

乍一看感受方案1比方案2好——時間複雜度和常數項都同樣,方案1還不須要改動。可是別忘了局部性原理,無論節點中存儲的是數據行仍是數據行位置,方案2的好處在於,葉子節點連續存儲,對頁表和緩存友好。而方案1則面臨節點邏輯相鄰、物理分離的缺點。

引出B+樹

綜上,問題1的方案2與問題2的方案1可整合爲一種方案(基於B樹的索引),問題1的方案3與問題2的方案2可整合爲一種(基於B+樹的索引)。實際上,數據庫、文件系統有些採用了B樹,有些採用B+樹。

因爲某些猴子暫未明白的緣由,包括MySQL在內的主流數據庫多選擇了B+樹。即:

image.png

主要變更如上所述:

  • 修改key與子樹的組織邏輯,將索引訪問都落到葉子節點
  • 按順序將葉子節點串起來(方便範圍查詢)

B樹和B+樹的增、刪、查過程

B樹的增刪過程暫時可參考從B樹、B+樹、B*樹談到R 樹的「六、B樹的插入、刪除操做」小節,B+樹的增刪同理。此處暫不贅述。

Mysql索引優化

根據B+樹的性質,很容易理解各類常見的MySQL索引優化思路。

暫不考慮不一樣引擎之間的區別。

優先使用自增key做爲主鍵

前面的分析中,假設用4B的自增key做爲索引,則m可達到512,層高僅有3。使用自增的key有兩個好處:

  1. 自增key通常爲int等整數型,key比較緊湊,這樣m能夠很是大,並且索引佔用空間小。最極端的例子,若是使用50B的varchar(包括長度),那麼m = 4 * 1024 / 54m = 75.85 ~= 76,深度最大 log(76/2)(10^7) = 4.43 ~= 5,再加上cache缺失、字符串比較的成本,時間成本增長較大。同時,key由4B增加到50B,整棵索引樹的空間佔用增加也是極爲恐怖的(若是二級索引使用主鍵定位數據行,則空間增加更加嚴重)。
  2. 自增的性質使得新數據行的插入請求必然落到索引樹的最右側,發生節點分裂的頻率較低,理想狀況下,索引樹能夠達到「滿」的狀態。索引樹滿,一方面層高更低,一方面刪除節點時發生節點合併的頻率也較低。

優化經歷:

猴子曾使用varchar(100)的列作過主鍵,存儲containerId,過了三、4天100G的數據庫就滿了,DBA小姐姐郵件裏委婉表示了對個人鄙視。。。以後增長了自增列做爲主鍵,containerId做爲unique的二級索引,時間、空間優化效果至關顯著。

最左前綴匹配

索引能夠簡單如一個列(a),也能夠複雜如多個列(a, b, c, d),即聯合索引。若是是聯合索引,那麼key也由多個列組成,同時,索引只能用於查找key是否存在(相等),遇到範圍查詢(>、<、between、like左匹配)等就不能進一步匹配了,後續退化爲線性查找。所以,列的排列順序決定了可命中索引的列數。

若有索引(a, b, c, d),查詢條件a = 1 and b = 2 and c > 3 and d = 4,則會在每一個節點依次命中a、b、c,沒法命中d。也就是最左前綴匹配原則。

=、in自動優化順序

不須要考慮=、in等的順序,mysql會自動優化這些條件的順序,以匹配儘量多的索引列。

若有索引(a, b, c, d),查詢條件c > 3 and b = 2 and a = 1 and d < 4a = 1 and c > 3 and b = 2 and d < 4等順序都是能夠的,MySQL會自動優化爲a = 1 and b = 2 and c > 3 and d < 4,依次命中a、b、c。

索引列不能參與計算

有索引列參與計算的查詢條件對索引不友好(甚至沒法使用索引),如from_unixtime(create_time) = '2014-05-29'

緣由很簡單,如何在節點中查找到對應key?若是線性掃描,則每次都須要從新計算,成本過高;若是二分查找,則須要針對from_unixtime方法肯定大小關係。

所以,索引列不能參與計算。上述from_unixtime(create_time) = '2014-05-29'語句應該寫成create_time = unix_timestamp('2014-05-29')

能擴展就不要新建索引

若是已有索引(a),想創建索引(a, b),儘可能選擇修改索引(a)爲索引(a, b)。

新建索引的成本很容易理解。而基於索引(a)修改成索引(a, b)的話,MySQL能夠直接在索引a的B+樹上,通過分裂、合併等修改成索引(a, b)。

不須要創建前綴有包含關係的索引

若是已有索引(a, b),則不須要再創建索引(a),可是若是有必要,則仍然需考慮創建索引(b)。

選擇區分度高的列做索引

很容易理解。如,用性別做索引,那麼索引僅能將1000w行數據劃分爲兩部分(如500w男,500w女),索引幾乎無效。

區分度的公式是count(distinct <col>) / count(*),表示字段不重複的比例,比例越大區分度越好。惟一鍵的區分度是1,而一些狀態、性別字段可能在大數據面前的區分度趨近於0。

這個值很難肯定,通常須要join的字段要求是0.1以上,即平均1條掃描10條記錄。


參考:


本文連接:淺談MySQL的B樹索引與索引優化
做者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議發佈,歡迎轉載,演繹或用於商業目的,可是必須保留本文的署名及連接。

相關文章
相關標籤/搜索