B+Tree从原理到实战:手推插入分裂与删除,彻底搞懂数据库索引底层
每个认真学过数据结构的人,迟早都会在数据库索引、文件系统目录组织里撞上B+Tree这个名字。它不像哈希表那样一两句话就能讲完,也不像红黑树那样在教科书里风光无限,但MySQL InnoDB的聚簇索引、PostgreSQL的默认表索引、许多文件系统的元数据组织,底层核心都是它。这篇文章从“为什么需要B+Tree”开始,讲到节点结构和约束规则,再带你把一棵五阶B+Tree的插入、分裂、删除全流程用手算推演一遍,最后整理面试和考研里的高频考点与易错点。适合正在复习数据结构期末、准备考研复试的同学,也适合写了几年SQL但始终没搞懂索引底层是怎么回事的工程师——写过SQL不一定真懂索引,而索引的第一课绕不开B+Tree。
1. 为什么需要B+Tree:先想明白它解决了什么问题
1.1 哈希表、二叉搜索树、红黑树各自的短板
很多人初学数据结构时有个疑问:查找一个数据,哈希表不是O(1)吗?为什么数据库不用哈希表当默认索引?
哈希表的O(1)只针对单点精确查询。一旦你要查范围,比如“id大于1000且小于2000的所有记录”,哈希表的桶是按哈希值分散的,物理上没有顺序,只能全表扫描或采用其他维护有序结构的方式。对于OLTP场景,点查是主力,但OLAP和报表场景里范围查询一点都不少,一个不支持范围查询的索引结构肯定撑不了全场。
二叉搜索树倒是能维护顺序,但它的高度很容易失控。连续插入有序数据时,普通二叉搜索树会退化成一条链表,查询从O(log n)变成O(n)。针对退化问题,AVL树和红黑树通过旋转保证高度为O(log n)。红黑树在内存中非常优秀,比如C++的std::map、Java的TreeMap都用它。可问题在于:一个节点只存一个键,如果数据量是一亿行,树高度约为26层左右。在一亿行的表里做一次查询,意味着要经过二十多次节点跳转。
在内存里,二十多次比较是纳秒级的事,无所谓。但数据库的数据大部分在磁盘上,一次随机磁盘IO的成本是几毫秒。二十多次IO在纯机械硬盘时代就是几百毫秒,哪怕换到SSD,也是不可接受的延迟。所以问题的核心不是“比较次数”,而是“要访问多少个节点”。一个节点在数据库里对应一页数据,访问一个节点往往就是一次磁盘读。为了让树变得更矮,必须让每个节点保存更多的键、拥有更多的子节点,这就是B系列树最基本的设计动机。
1.2 B树已经很好,为什么还要改造成B+Tree
B树(Balance Tree)的核心思想是放弃二叉树限制,让每个节点存储多个键和多个子指针,整棵树保持严格平衡。一个m阶B树的节点最多有m个子树、m-1个键,所有叶子在同一层。因为每个节点能存几百个键,一棵三四层的B树就能支撑上亿条记录的索引。
B树本身已经很好用,但B+Tree在B树基础上做了两个关键改动,这两个改动直接决定了数据库会选它。
第一个改动:B树的所有节点都存数据,而B+Tree只有叶子节点存数据或指向数据的指针,内部节点只存“路由键”。这个改动带来的直接好处是内部节点变小了。同样一个16KB的页,如果存B树那种带完整记录的节点,放不了几百条记录;但只存键和子页号,放下上千个键毫无压力。内部节点能容纳的子树数量越大,树的扇出就越高,树就越矮,查询需要的磁盘IO就越少。
第二个改动:B+Tree的叶子节点通过指针串成一个有序链表。查找单点时B树的路径可能结束在第2层,B+Tree则必须一路走到叶子节点。看起来B+Tree更费,但正因为所有查询都走同样的路径,查询性能非常稳定,没有“某些数据运气好更快”的波动。做范围查询时,B+Tree找到第一个符合条件的叶子后,顺着链表往后扫就行,不需要频繁回溯父节点再去另一棵子树里找下一个节点。B树做范围查询必须做中序遍历,中间伴随大量回溯,效率低得多。
这两个改动放在一起,B+Tree就成了磁盘场景下的最优解之一。内部节点小而多子、叶子链表利于顺序扫描,这两个特征恰好对应数据库最典型的两种访问模式:随机点查和顺序范围扫。
2. B+Tree的节点结构与约束规则:一页能装多少关键看这里
2.1 一个节点到底长什么样
理解B+Tree最直观的方式是看节点定义。一个节点可以简单理解成三个部分:一组有序键、一组子指针、一个是否是叶子的标记。叶子节点额外多了前后兄弟指针,用于串成链表。
用近似伪代码表示节点结构就是: