B+树:MySQL为什么选了它
先从一个让人头疼的问题说起
你有一张表,1000万行数据,要查 id=8765432 的那一条。
数据在磁盘上,磁盘随机读一次大概10毫秒。如果一条条扫,那你不如去喝杯咖啡再回来。所以问题的关键就在于:怎么用尽量少的磁盘读取,定位到目标数据?
这也是MySQL选择索引结构的唯一标准——IO次数越少越好。
先淘汰几个选手
最先想到的肯定是二叉搜索树。但顺序插入 10、20、30、40、50 之后,它就变成了这样:
10 -> 20 -> 30 -> 40 -> 50
跟链表没区别,找50要比较5次。1000万条数据最坏情况?别想了。
那就用红黑树吧,好歹能自平衡,不会退化。但问题是它再怎么平衡,每个节点最多就两个子节点。1000万条数据:
树高 = log₂(10,000,000) ≈ 24
24次磁盘IO,每次10ms,就是240ms。听着还行?别忘了这是单次查询,高并发场景下根本扛不住。
那Hash呢?Hash查找确实是O(1),但遇上这种SQL直接歇菜:
SELECT * FROM user WHERE id BETWEEN 100 AND 500;
SELECT * FROM user ORDER BY id LIMIT 10;
Hash只能精确匹配,范围查询、排序、模糊匹配通通不支持。数据库里范围查询太常见了,这条路走不通。
B+树到底长什么样
别被名字吓到,其实不难理解。我用一组具体数字来演示,假设我们要用 1~20 这些数字建一棵B+树:
[ 7 | 13 ]
/ | \
↓ ↓ ↓
[1|4|7] [7|10|13] [13|16|19]
/ | \ / | \ / | \
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
[1,2,3,4] [7,8,9,10] [13,14,15,16] [19,20]
←→ ←→ ←→ ←→
有两点很关键:
第一,上面那些非叶子节点只存键值,不存数据。它们纯粹是路牌,告诉你"往左走""往右走"。
第二,最底下的叶子节点通过双向指针串成链表。这个设计是B+树的灵魂,后面会说到为什么。
查找 id=10
从根节点开始。10在7和13之间,走中间那条路。到第二层,命中10对应的指针,继续往下。到叶子节点,找到10的数据。
3次IO搞定,不管你有多少条数据。
范围查询 id BETWEEN 7 AND 16
先用同样的方式定位到7(3次IO),然后沿着叶子链表一路往右扫就行了:7、8、9、10、13、14、15、16。不需要反复从根节点找,也不需要回溯。
这就是叶子链表的威力——把范围查询变成了一次定位 + 顺序扫描。
B树为什么不行
B树和B+树长得很像,最关键的区别是:B树的每一层节点都存数据,B+树只有叶子节点存数据。
这听起来是个小改动,但影响巨大。
InnoDB一个页是16KB。假设键值占8字节,一条数据记录占1KB。
B树的节点里既有键值又有数据,一个16KB的页大概只能放16个键值。三层下来:
16 × 16 × 16 = 4096 条
四千条,塞牙缝都不够。
B+树的非叶子节点只放键值,一个16KB的页能放大约1170个键值(算上子节点指针)。三层下来:
1170 × 1170 × 16 ≈ 2200万条
同样三层,B树存四千,B+树存两千万。 差了五千倍,就是因为非叶子节点不放数据,空间全拿来存路由信息了。
而且B树做范围查询也很别扭。因为数据分散在各层,查 id BETWEEN 7 AND 16 必须反复在树里上上下下做中序遍历,每次跳转都是一次IO。不像B+树那样沿着链表一路扫过去就行了。
InnoDB里的真实数字
InnoDB默认配置下,B+树的参数大概是这样的:
- 非叶子节点每页约1170个键值
- 叶子节点每页约16条记录
- 3层B+树能存约2200万行
- 4层能存约257亿行
也就是说,你有一张两千万行的表,通过主键精确查一条数据,最多3次磁盘IO。这个数字才是B+树被选中的根本原因。
总结
B+树被MySQL选中,靠的是两板斧:
矮 — 非叶子节点只存键值不存数据,同样一页16KB能塞下上千个路由键,树被压得很矮。3层就能装下2000万行数据,点查只需3次IO。
连 — 叶子节点用链表串起来,范围查询定位起点后顺着链表扫就行,不用在树里反复跳转。
| 对比项 | B树 | B+树 |
|---|---|---|
| 数据存放位置 | 每层都有 | 只在叶子节点 |
| 同样3层容量 | ~4000行 | ~2000万行 |
| 范围查询 | 中序遍历,反复跳转 | 链表顺序扫描 |
| 查询稳定性 | 路径长度不固定 | 都到叶子,路径固定 |
一句话记忆:非叶子只存键让树矮(3层装2000万),叶子串链表让范围查变顺序扫——矮而连,IO最少。