原文對B+樹的解釋是很詳細的,看的好文章記錄轉載一下。
原文地址:https://www.toutiao.com/i6947997799594164748/
很多互聯網應用都離不開數據庫的增刪改查(CRUD),實際開發過程中經常因為數據庫索引沒有建好,導致系統性能問題。了解數據庫索引查詢數據的底層原理,有利於我們更好地優化系統的查詢性能。本文主要以Mysql數據庫InnoDB引擎來介紹,關於InnoDB引擎的數據存儲格式可以參考前文《Mysql引擎InnoDB數據存儲的基本單位是什么? 》。
B+樹索引
使用B+樹存儲數據,磁盤IO更少。B+樹中,非葉子節點是不存儲數據的,只存儲key,這樣每個節點能夠存儲更多的key,使得樹的高度更矮,進而讀取磁盤次數更少。
假如B+樹的一個節點可以存儲 1000 個鍵值,那么3層B+樹最多可以存儲 1000×1000×1000=10 億條數據。根節點一般是常駐內存的,如果要在10 億條數據進行一次單條數據記錄的查詢,只需要 2 次磁盤 IO。

InnoDB的數據頁以B+樹結構組織
B+樹有利於范圍查找和排序。B+樹的所有數據都存在葉子節點,而且數據是按順序存儲的,這樣使得范圍查找、排序查找更加方便。關於B+樹的特性,可以參考前文《Java面試常見問題:B-樹和B+樹 》。
正是因為B+樹的這些優點,包括InnoDB在內的很多數據庫存儲引擎都采用B+樹來建立索引。在InnoDB中,數據頁之間用雙向鏈表連接,數據記錄之間用單向鏈表連接。由於頁之間有雙向鏈表鏈接,使得掃描數據更加快捷。
B+樹的索引
主鍵構成聚集索引
B+樹在葉子節點存儲數據,這樣的索引是聚集索引。MySQL里默認根據主鍵創建的索引就是聚集索引,根據主鍵構建一棵B+樹,主鍵所對應的值直接存在葉子節點中。
由頁組成的鏈表,頁之間是雙向列表,頁里面的數據是單向鏈表,這種結構組成了主鍵索引B+樹。

主鍵索引(聚集索引)的查詢過程
對於以下查詢語句,上圖展示了主鍵索引的查詢過程。
select * from user where id>=18 and id <40
根據主鍵id的最小值,首先找到起始的頁面和數據,之后利用數據記錄的單向鏈表向后搜索,直到主鍵值達到搜索范圍的最大值結束。在搜索范圍內的Page頁都被載入內存中。
非主鍵構成非聚集索引
根據主鍵以外的字段創建的索引一般是非聚集索引,非聚集索引也是用B+樹構建的。非聚集索引和聚集索引的唯一不同就是葉子節點中保存的值不是實際的值,而是主鍵值。
當基於主鍵以外的字段來查詢數據時,引擎先通過 非聚集索引 找到對應的主鍵值后,再去聚集索引中查找需要的數據。

非聚集索引的搜索過程
假設數據庫基於非主鍵字段 num 建立了非聚集索引。對於以下查詢語句的搜索過程如上圖所示。
select * from user where num=33
首先在非聚集索引的非葉子節點中定位到對應的頁,在頁中利用單向鏈表定位到對應的記錄。在記錄中有對應的主鍵值,接下來轉到聚集索引中查詢最終的數據。
總結
最后我們總結一下InnoDB引擎查詢指定數據的過程:
- 數據庫系統經過解析SQL語句;
- 讀取裝有非葉子節點的Page頁,遍歷非葉子節點;
- 隨着節點的遍歷,會將一個或多個Page頁加載到內存,直到定位到這條記錄所在的葉子節點;
- 在數據頁中遍歷單向鏈表找出該條記錄。
在InnoDB 存儲引擎,頁(Page)是用於磁盤和內存進行數據交換的基本單位,即最小磁盤單位。常見的頁類型有數據頁、Undo 頁、系統頁、事務數據頁等,本文主要分析的是數據頁。
InnoDB的邏輯存儲結構
下圖為InnoDB引擎的邏輯存儲結構圖,從上往下依次為:表空間(Tablespace)、段(Segment)、區(Extent)、頁(Page) 以及行(Row)。表空間是由各個段組成的,段一般分為數據段、索引段和回滾段等。區是表空間的單元結構,每個區的大小為1MB。

InnoDB邏輯存儲結構圖
頁是組成區的最小單元。頁的本質就是一塊16KB大小的存儲空間,InnoDB把頁分為不同的類型,其中用於存放記錄的頁也稱為數據頁。

- File Header:文件頭部,記錄頁的通用信息,比如:頁的校驗和、編號、上一頁(FIL_PAGE_PREV )、下一頁(FIL_PAGE_NEXT)等;
- Page Header:頁面頭部,記錄頁的狀態信息,比如:本頁有多少條記錄、第一條記錄的地址、頁目錄中有多少槽(slot)、最后插入記錄的位置等;
- Infimum + supremum:兩個虛擬的行記錄,Infimum(下確界)記錄比該頁中任何主鍵值都要小的值,Supremum (上確界)記錄比該頁中任何主鍵值都要大的值;
- User Records:實際存儲數據的行記錄;
- Free Space:頁中尚未使用的空間;
- Page Directory:頁面目錄,記錄各個槽在頁面中的地址偏移量;
- File Trailer:文件尾部,校驗頁的完整性。
將頁連成雙向鏈表
一張表中有成千上萬條記錄,一個頁只有16KB,所以需要好多頁來存放數據。不同頁之間構成了一條雙向鏈表,File Header 的 FIL_PAGE_PREV 和 FIL_PAGE_NEXT 分別代表本頁的上一個和下一個頁的頁號,即鏈表的上一個以及下一個節點指針。

數據頁構成雙向鏈表
最大記錄和最小記錄
Infimum 和 supremum 是兩個偽行記錄,不記錄實際數據,因此也不放在User Records區域中。這兩條記錄的作用就是確定了當前數據頁記錄主鍵的最小值和最大值,這兩個值在頁創建時被建立,並且在任何情況下不會被刪除。

最大記錄和最小記錄頁中的數據記錄形成了一個單鏈表。鏈表順序是按照主鍵的值從小到大的順序。最小記錄的NEXT_RECORD 就是頁中的第一條數據記錄,頁中的最后一條數據記錄的 NEXT_RECORD 是最大記錄。

頁中的數據記錄單鏈表結構
頁中插入記錄
在一開始生成頁的時候,頁中並沒有User Records這個部分。每當插入一條記錄,都會從Free Space部分中申請一個記錄大小的空間划分到User Records部分。

在頁中插入記錄的過程
當Free Space部分的空間全部被 User Records 部分替代掉之后,也就意味着這個頁使用完了,如果還有新的記錄插入的話,就需要去申請新的頁了。總之,User Records 和 Free Space 之間是此消彼長的關系。
除了Java語法和並發編程方面這些必考內容,Java面試還經常會問到關於數據結構方面的問題。本文就來介紹兩個在數據庫和文件系統中常用的數據結構:B-樹和B+樹。需要注意的是:“B-樹”中的短橫不是減號,B表示平衡(Balance)的意思。
B-樹
B-樹是為了磁盤或其它存儲設備而設計的一種平衡多叉搜索樹。B-樹與平衡二叉搜索樹最大的不同在於,B-樹的結點可以有許多孩子。B-樹可以在O(logn)的時間復雜度內,實現插入、刪除等動態操作,相比平衡二叉樹,大大減少了IO的次數。

B-樹(M=3)
M階B-樹(M>2),代表一個樹結點最多有M個子結點(查找路徑),上圖為3階B-樹。
B-樹需要滿足以下規則:
- 排序方式:每個結點中的關鍵字都按照從小到大的順序排列,每個關鍵字的左子樹中的所有關鍵字都小於它,而右子樹中的所有關鍵字都大於它;
- 子結點數:根結點的子結點數為[2, M],其他非葉結點的子結點數為[M/2, M],向上取整;
- 關鍵字數:非葉結點的關鍵字數量為子結點數-1,如上圖每個非葉結點有2個關鍵字,分成3個區間,分別指向3個子樹;
- 葉子結點:所有葉子結點均在同一層,或者說根節點到每個葉子節點的長度都相同;
另外B-樹的每個結點,除了有關鍵字key以外,還有value,也就是關鍵字記錄的指針(地址)。
蘋果電腦的HFS+文件系統,就采用了B樹作為文件系統的索引。
B+樹
B+樹是B-樹的一種變形,其與B-樹的區別主要在於:
- 非葉子結點的子樹指針與關鍵字個數相同;
- 非葉子結點的子樹指針 P[i],指向關鍵字值屬於 [K[i], K[i+1]) 的子樹,而B-樹的子樹結點關鍵字不包括 K[i];
- 所有葉子結點增加了一個鏈指針;
- 所有關鍵字都在葉子結點出現。

B+樹(M=3)
上圖是一個 M=3 的 B+樹,非葉子結點的關鍵字個數和子樹指針都是3個。第一個非葉子結點(5,10,20)的第一個指針 P1 指向的子樹,只有一個葉子結點,P1對應的關鍵字5又出現在了葉子結點的關鍵字中。
B+樹的應用
B+樹適合應用於操作系統的文件索引和數據庫索引。B+樹相比於B樹,在文件系統和數據庫系統當中更有優勢,原因如下:
- IO讀寫代價低
B+樹的非葉結點不保存關鍵字記錄的指針,只進行數據索引,B+樹每個非葉節點所能保存的關鍵字大大增加。一次性讀入內存中的關鍵字越多,也就意味着I/O讀寫次數越少。 - 查詢效率穩定
B+樹只有葉子結點保存了關鍵字記錄的指針,任何關鍵字記錄的查找都必須走一條從根結點到葉子結點的路。所有關鍵字查詢的路徑長度相同,數據查詢效率相比B-樹更加穩定。 - 有利於數據庫掃描
B+樹只需要遍歷葉子結點就可以解決對全部關鍵字信息的掃描,所以對於數據庫中頻繁使用的區間查詢,B+樹有着更高的性能。

基於B+樹的Mysql數據庫索引
Mysql的InnoDB和MylSAM存儲引擎都是用B+樹實現索引結構。