雖然上一章節介紹的二叉搜索樹在查詢指定值時表現很好,但是當查詢兩個值之間的多個節點時,就會遇到很大的問題。因爲需要遍歷整個樹的節點,並檢查每個節點是否在指定的區間內。而且遍歷整顆樹是隨機磁盤IO(譯者注:隨機IO會導致頻繁的磁頭換道,所以相比順序IO來說非常耗時),所以我們需要找到一種更有效做範圍查詢的方法。爲了解決這個難題,現代數據庫修正了之前介紹的二叉搜索樹,我們稱修正後的數據結構爲B+Tree:
- 只有葉子節點(樹最底層的節點,圖中橘黃色的節點)存儲信息,即:行在表中精確的位置,也就是rowid;
- 其他的節點僅僅是在搜索時用於路由到正確的節點。
就像上圖中所示,有了更多的節點,實際上這些額外的節點就是決策節點,它會幫助我們找到正確的節點(實際存儲行精確位置),但是它的搜索複雜度卻依然是O(log(N)),和搜索二叉樹最大的不同就是葉子節點都持有下一個節點的指針(譯者注:可以看做一個有序的單向鏈表)。
使用B+Tree,如果我們需要查詢40到100之間的值:
- 你僅僅需要找到40節點,或者如果40節點不存在則找到大於40的最近節點;
- 根據上一步找到的節點持有的指針,樹藤摸瓜找到下一個節點,直到找到100節點截止。
譯者注:上圖其實也間接的解釋了數據庫是怎麼構建B+Tree索引的,應該是自底向上的來構建。
假設實際需要在一棵有N個節點的樹中範圍查找到M個節點,那麼其時間複雜度爲M+log(N)。因爲找到開始節點需要log(N),接着就可以根據指針按順序找到M個節點,實際耗費M。M+log(N)與之前二叉搜索書的N相比,你不需要搜索整顆樹,僅僅搜索M+log(N)個節點,這意味着更少的磁盤IO消耗;如果M很小(比如:200行),而N很大(比如:1 000 000行),那這兩者的差距就非常巨大了。
可是我們又發現了一個問題,如果數據庫使用B+Tree索引,而你刪除一張表中的某一行,那麼:
- 你必須保持整棵樹中節點的順序,否則在亂序的樹種你無法找到正確的節點。
- 你必須保證樹的高度儘可能低,否則無論單值查詢還是範圍查詢的複雜度就會從O(log(N))無限的接近O(N)。(譯者注:當樹的高度就是節點的數量時,那麼樹的外形就像一條豎線,這時要找到一個節點實際上需要遍歷整顆樹)
簡而言之,B+Tree需要自排序和自平衡。雖然我們可以高效快速的刪除和插入,但這是有代價的:在B+Tree數據庫中代價是O(log(N))。這就是爲什麼你經常看到創建過多的索引不是一個好主意,這樣會降低在表中插入/刪除/更新行的速度。究其原因,是每一次插入/刪除/更新,都會導致數據庫更新(譯者注:指自排序和自平衡)表的索引樹,並且這種更新每個索引耗費是O(log(N))(譯者注:上文指的代價)。而且,增加索引會導致事務管理器的負載增大(後續篇章會介紹到)。
關於B+Tree的更多細節,可以查看維基百科中的B+Tree說明。如果你想找B+Tree在數據庫中的實現例子,可以查看來自MySQL核心開發者貢獻的這兩篇關於InnoDB如何處理索引的文章:The physical structure of InnoDB index pages、B+Tree index structures in InnoDB。