MySQL之B+树详解
理論是灰色的,實(shí)踐之樹長(zhǎng)青🌲 ——恩格斯
概述
MySql這樣的關(guān)系型數(shù)據(jù)庫(kù)在查詢方面有一些重要特性,是KV型的數(shù)據(jù)庫(kù)或者 緩存所不具備的,比如:
(1)范圍查詢。
(2)前綴匹配模糊查詢。
(3)排序和分頁(yè)。
這些特性的支持,要?dú)w功于B+樹這種數(shù)據(jù)結(jié)構(gòu)。下面我們來分析一下B+樹是如何支持這些特性的。
邏輯結(jié)構(gòu)
這里我們拿數(shù)據(jù)庫(kù)主鍵對(duì)應(yīng)的B+樹邏輯結(jié)構(gòu)來說明,這個(gè)結(jié)構(gòu)有幾個(gè)關(guān)鍵特性:
- 在葉子節(jié)點(diǎn)一層,所有記錄的主鍵按照從小到大的順序排 列,并且形成了一個(gè)雙向鏈表。葉子節(jié)點(diǎn)的每一個(gè)Key指向一條記錄。
- 非葉子節(jié)點(diǎn)取的是葉子節(jié)點(diǎn)里面Key的最小值。這意味著所有 非葉子節(jié)點(diǎn)的Key都是冗余的葉子節(jié)點(diǎn)。同一層的非葉子節(jié)點(diǎn)也互相串 聯(lián),形成了一個(gè)雙向鏈表。
下面的結(jié)構(gòu)圖可以更好的說明這兩個(gè)特性:
基于這樣一個(gè)數(shù)據(jù)結(jié)構(gòu)以上特性就更好說明了:
- 范圍查詢:比如要查主鍵在[1,17]之間的記錄。二次查詢,先查找 1所在的葉子節(jié)點(diǎn)的記錄位置,再查找17所在的葉子節(jié)點(diǎn)記錄的位置 (就是16所處的位置),然后順序地從1遍歷鏈表直到16所在的位置。
- 前綴匹配模糊查詢:假設(shè)主鍵是一個(gè)字符串類型,要查詢where Key like abc%,其實(shí)可以轉(zhuǎn)化成一個(gè)范圍查詢Key in [abc,abcz]。當(dāng)然,如果是后綴匹配模糊查詢,或者諸如where Key like %abc%這樣的中間 匹配,則沒有辦法轉(zhuǎn)化成范圍查詢,只能挨個(gè)遍歷。
- 排序與分頁(yè):葉子節(jié)點(diǎn)天然是排序好的,支持排序和分頁(yè)。
另外,基于B+樹的特性,會(huì)發(fā)現(xiàn)對(duì)于offset這種特性,其實(shí)是用不到索引的。比如每頁(yè)顯示10條數(shù)據(jù),要展示第101頁(yè),通常會(huì)寫成select xxx where xxx limit 1000, 10,從offset = 1000的位置開始取10條。 雖然只取了10條數(shù)據(jù),但實(shí)際上數(shù)據(jù)庫(kù)要把前面的1000條數(shù)據(jù)都遍 歷才能知道 offset =1000的位置在哪。對(duì)于這種情況,合理的辦法是不 要用offset,而是把offset = 1000的位置換算成某個(gè)max_id,然后用where 語(yǔ)句實(shí)現(xiàn),就變成了select xxx where xxx and id > max_id limit 10,這樣 就可以利用B+樹的特性,快速定位到max_id所在的位置,即是 offset=1000所在的位置。
物理結(jié)構(gòu)
上面的樹只是一個(gè)邏輯結(jié)構(gòu),最終要存儲(chǔ)到磁盤上。下面就以 MySQL中最常用的InnoDB引擎為例,看一下如何實(shí)現(xiàn)B+樹的存儲(chǔ)。
對(duì)于磁盤來說,不可能一條條地讀寫,而都是以“塊”為單位進(jìn)行讀 寫的。InnoDB默認(rèn)定義的塊大小是16KB,通過innodb_page_size參數(shù)指 定。這里所說的“塊”,是一個(gè)邏輯單位,而不是指磁盤扇區(qū)的物理塊。 塊是InnoDB讀寫磁盤的基本單位,InnoDB每一次磁盤I/O,讀取的都是 16KB的整數(shù)倍的數(shù)據(jù)。無論葉子節(jié)點(diǎn),還是非葉子節(jié)點(diǎn),都會(huì)裝在 Page里。InnoDB為每個(gè)Page賦予一個(gè)全局的32位的編號(hào),所以InnoDB 的存儲(chǔ)容量的上限是64TB(2^30×16KB)。
16KB是一個(gè)什么概念呢?如果用來裝非葉子節(jié)點(diǎn),一個(gè)Page大概 可以裝1000個(gè)Key(16K,假設(shè)Key是64位整數(shù),8個(gè)字節(jié),再加上各種 其他字段),意味著B+樹有1000個(gè)分叉;如果用來裝葉子節(jié)點(diǎn),一個(gè) Page大概可以裝200條記錄(記錄和索引放在一起存儲(chǔ),假設(shè)一條記錄大概100個(gè)字節(jié))?;谶@種估算,一個(gè)三層的B+樹可以存儲(chǔ)多少數(shù)據(jù) 量呢?如圖下圖所示:
- 第一層:一個(gè)節(jié)點(diǎn)是一個(gè)Page,里面存放了1000個(gè)Key,對(duì)應(yīng)1000 個(gè)分叉。
- 第二層:1000個(gè)節(jié)點(diǎn),1000個(gè)Page,每個(gè)Page里面裝1000個(gè)Key。
- 第三層:1000×1000個(gè)節(jié)點(diǎn)(Page),每個(gè)Page里面裝200條記錄, 即是1000×1000×200 =2億條記錄,總?cè)萘渴?6KB×1000×1000,約16GB。
把第一層和第二層的索引全裝入內(nèi)存里,即(1+1000)×16KB,也 即約16MB的內(nèi)存。三層B+樹就可以支撐2億條記錄,并且一次基于主 鍵的等值查詢,只需要一次I/O(讀取葉子節(jié)點(diǎn))。由此可見B+樹的強(qiáng) 大!
基于Page,最終整個(gè)B+樹的物理存儲(chǔ)類似下圖所示:
Page與Page之間組成雙向鏈表,每一個(gè)Page頭部有兩個(gè)關(guān)鍵字段: 前一個(gè)Page的編號(hào),后一個(gè) Page 的編號(hào)。Page 里面存儲(chǔ)一條條的記 錄,記錄之間用單向鏈表串聯(lián),最終所有的記錄形成上面所示的雙向 鏈表的邏輯結(jié)構(gòu)。對(duì)于記錄來說,定位到了Page,也就定位到了Page里 面的記錄。因?yàn)镻age會(huì)一次性讀入內(nèi)存,同一個(gè)Page里面的記錄可以在 內(nèi)存中順序查找。
在InnoDB的實(shí)踐里面
- 其中一個(gè)建議是按主鍵的自增順序插入記 錄,就是為了避免Page Split問題。比如一個(gè)Page里依次裝入了Key為(1,3,5,9)四條記錄,并且假設(shè)這個(gè)Page滿了。接下來如果插入一個(gè) Key =4的記錄,就不得不建一個(gè)新的Page,同時(shí)把(1,3,5,9)分成兩半,前一半(1,3,4)還在舊的Page中,后一半(5,9)拷貝到新的Page 里,并且要調(diào)整Page前后的雙向鏈表的指針關(guān)系,這顯然會(huì)影響插入速 度。但如果插入的是Key = 10的記錄,就不需要做Page Split,只需要建 一個(gè)新的Page,把Key = 10的記錄放進(jìn)去,然后讓整個(gè)鏈表的最后一個(gè) Page指向這個(gè)新的Page即可。
- 另外一個(gè)點(diǎn),如果只是插入而不硬刪除記錄(只是軟刪除),也會(huì) 避免某個(gè)Page的記錄數(shù)減少進(jìn)而發(fā)生相鄰的Page合并的問題。
非主鍵索引
對(duì)于非主鍵索引,同上面類似的結(jié)構(gòu),每一個(gè)非主鍵索引對(duì)應(yīng)一顆 B+樹。在InnoDB中,非主鍵索引的葉子節(jié)點(diǎn)存儲(chǔ)的不是記錄的指針, 而是主鍵的值。所以,對(duì)于非主鍵索引的查詢,會(huì)查詢兩棵B+樹,先 在非主鍵索引的B+樹上定位主鍵,再用主鍵去主鍵索引的B+樹上找到 最終記錄。
有一點(diǎn)需要特別說明:對(duì)于主鍵索引,一個(gè)Key只會(huì)對(duì)應(yīng)一條記 錄;但對(duì)于非主鍵索引,值可以重復(fù)。所以一個(gè)Key可能對(duì)應(yīng)多條記 錄,如下表所示。假設(shè)對(duì)于字段1建立索引(字段1是一個(gè)字符類 型),一個(gè)A會(huì)對(duì)應(yīng)1,5,7三條記錄,C對(duì)應(yīng)8、12兩條記錄。這反映在 B+樹的數(shù)據(jù)結(jié)構(gòu)上面就是其葉子節(jié)點(diǎn)、非葉子節(jié)點(diǎn)的存儲(chǔ)結(jié)構(gòu),會(huì)和 主鍵索引的存儲(chǔ)結(jié)構(gòu)稍有不同。
| 1 | A | |
| 5 | A | |
| 7 | A | |
| 8 | C | |
| 10 | B | |
| 12 | C |
非主鍵索引的B+樹結(jié)構(gòu)如下圖所示:
首先,每個(gè)葉子節(jié)點(diǎn)存儲(chǔ)了主鍵的值;對(duì)于非葉子 節(jié)點(diǎn),不僅存儲(chǔ)了索引字段的值,同時(shí)也存儲(chǔ)了對(duì)應(yīng)的主鍵的最小值。
參考書籍:《軟件架構(gòu)設(shè)計(jì)》
個(gè)人github賬號(hào):https://github.com/SpecialAll
歡迎一起交流學(xué)習(xí)!
總結(jié)
以上是生活随笔為你收集整理的MySQL之B+树详解的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux下木马程序病原体的制作和运行
- 下一篇: mysql端口establish_est