第一章:B-Tree 與 B+Tree — 索引的數學骨架
1. 定義
B-Tree(平衡多路搜尋樹)是一種自平衡的樹狀資料結構,它維護有序數據並允許以對數時間進行搜尋、順序存取、插入和刪除。它由 Rudolf Bayer 和 Edward M. McCreight 於 1972 年定義。
B+Tree 是 B-Tree 的變體,其特點是:
- 非葉子節點不存儲數據:僅存儲鍵(Key)作為索引。
- 葉子節點鏈表化:所有數據均存儲在葉子節點,且葉子節點之間透過雙向指標連接,便於範圍掃描(Range Scan)。
2. 技術深度:為什麼是 B+Tree 而不是二元樹?
資料庫索引之所以不使用紅黑樹或 AVL 樹,核心原因在於減少磁碟 I/O 次數。
A. 扇出 (Fan-out) 與樹的高度
在磁碟中,數據是以 Page (頁) 為單位讀取的(InnoDB 預設為 16KB)。
- 二元樹:每個節點只有 2 個子節點,樹的高度 $H$ 約為 $\log_2 N$。對於 1000 萬筆數據,高度約 24 層,意味著最壞情況需要 24 次磁碟 I/O。
- B+Tree:透過增大扇出(例如每個節點存儲 100 個鍵),樹的高度 $H$ 大幅降低。同樣 1000 萬筆數據,高度僅需 3-4 層。
B. B+Tree 的磁碟預讀優勢
由於 B+Tree 的非葉子節點不存數據,一個 Page 可以容納更多的鍵(Key),從而進一步提高扇出。同時,葉子節點的鏈表結構使得**範圍查詢(Range Scan)**無需回溯父節點,直接在葉子層水平移動即可。
3. 可視化:Page 分裂 (Page Split) 過程
當一個 Page 已滿(達到填充因子限額)且有新數據插入時,資料庫會觸發一次「不可見」的底層操作:Page Split。
sequenceDiagram
participant App as 插入請求
participant Buffer as Buffer Pool (記憶體)
participant PageA as Page 101 (已滿)
participant PageB as Page 102 (新申請)
App->>Buffer: 插入鍵 50
Buffer->>PageA: 嘗試寫入
Note over PageA: 數據溢出 (Overflow)
Buffer->>PageB: 申請新空頁
PageA->>PageB: 遷移一部分記錄 (如鍵 30-49)
Note over PageA,PageB: 重新平衡節點
Buffer-->>Buffer: 更新父節點索引指標
Note over Buffer: 磁碟 I/O 放大開始4. 真實案例:Instagram 的 ID 生成策略 (2011)
背景:2011 年,Instagram 工程團隊在設計其媒體 ID 生成系統時,需要一種能夠跨分片(Shard)唯一且高效能的方案。
技術選型:他們最初考慮了 UUID (Universally Unique Identifier)。 拒絕理由:
- 索引大小:UUID 是 128 位元的,而
bigint只有 64 位元。對於擁有數十億筆記錄的資料表,索引會變得極其臃腫,難以全部裝入記憶體。 - B+Tree 效能噩夢:UUID 是完全隨機的。在 B+Tree 索引中,這會導致數據被隨機插入到不同的 Page 中。由於 Page 頻繁分裂(Page Split),磁碟物理存儲變得極其不連續,且每個 Page 的填充率(Fill Factor)極低。這引發了大量的隨機磁碟 I/O,嚴重拖慢了寫入速度並擠壓了 Buffer Pool 空間。
最終方案:Instagram 參考了 Twitter 的 Snowflake 演算法,設計了一種包含 時間戳前綴 的 64 位元邏輯 ID。
- 優勢:由於 ID 是趨勢遞增的,新記錄大多追加到 B+Tree 的右側葉子節點。這避免了中間節點的頻繁隨機分裂,通常能維持較高的 Page 填充率(取決於具體的分裂策略),並顯著提升了快取命中率與寫入局部性 (Write Locality)。
教訓:在高興能資料庫設計中,索引列的順序性往往比唯一性更具挑戰。高品質的架構師會透過犧牲一定的隨機性來換取 B+Tree 的物理連續性。
5. 深度最佳化與縱深防禦
A. 根本修復:選擇合適的叢集索引 (Clustered Index)
- 優先使用自增 ID 或趨勢遞增的鍵作為叢集索引。
- 對於非遞增鍵,考慮降低
FILLFACTOR(填充因子,Postgres 術語)或innodb_fill_factor(MySQL,依版本/場景生效),為隨機插入預留空間,減少分裂。
B. 縱深防禦:索引覆蓋 (Covering Index)
- 透過將常用查詢欄位放入二級索引,避免「回表查詢」,降低額外 I/O 成本。
C. 碎片清理
- 定期執行
OPTIMIZE TABLE(MySQL) 或REINDEX(Postgres) 以回收物理碎片,重新平衡樹狀結構。
6. 參考資料
- Knuth, D. E. - The Art of Computer Programming, Vol 3: Sorting and Searching
- MySQL InnoDB Disk Structure - B+Tree
- PostgreSQL Index Types - B-Tree Internals
