B+树:MySQL为什么选了它

6 阅读5分钟

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最少。