如果你写过几年后端,多半听过这条经验:"查询慢?加个索引。"——然后你 CREATE INDEX,查询从 3 秒变成 3 毫秒,仿佛施了魔法。但很少有人追问:索引到底是什么?它为什么能让查找"瞬间"完成?而那些"加了索引反而更慢""索引没用上"的诡异 bug,又源自哪里?
Hussein Nasser(后端工程领域最有影响力的 YouTuber 之一,频道订阅近 50 万)这期 "B-tree vs B+ tree in Database Systems",没教你怎么写 SQL,而是掀开数据库引擎的引擎盖,给你看里面那棵"看不见的树"。他要回答的核心问题是:为什么 MySQL、PostgreSQL、SQL Server、Oracle——几乎所有主流关系型数据库——都把同一种数据结构(B+ 树)当作索引的底座,而把它的"前辈" B 树无情抛弃?
这期视频值得看,不是因为它讲了 B+ 树,而是因为它讲清了一个被大多数人误读的事实:索引之所以快,不是因为"树",而是因为"树很矮"——而树之所以矮,是因为它的每一层都被刻意设计成了不带货的"纯路牌"。 这篇文章会在视频的基础上,把这道"小学算术题"算给你看,并补上 Hussein 来不及展开的 InnoDB 实现细节、回表代价和写放大账单。
| 项目 | 内容 |
|---|---|
| 视频标题 | B-tree vs B+ tree in Database Systems |
| 频道 | Hussein Nasser(@hnasr) |
| 频道订阅 | 约 49.3 万 |
| 时长 | 约 15 分钟(聚焦型长讲解) |
| 播放量 | 频道为头部后端工程频道(精确播放量因限流未能核实) |
| 核心主题 | 为什么数据库用 B+ 树而非 B 树;B+ 树的内部节点是纯路由键,数据只存叶子 |
视频一开始,Hussein 先打掉一个误解:很多人以为索引是数据库"加速"原始表的方式,好像给表装了个涡轮增压。真相是——索引根本不是表的一部分,它是一份独立的、按某个字段排好序的"数据副本"。
想象一本 1000 页的字典,正文按"收录顺序"乱排,你想查"apple"只能从第 1 页翻到第 1000 页(这就是全表扫描,full table scan)。如果你在最后附一个按字母排序的目录,告诉你"apple 在第 837 页",你只需要先查目录、再翻一次页——目录就是索引。
这个比喻揭示了索引的三个本质代价,Hussein 没有直接说,但我想点破:
所以"加索引"本质上是一个用存储和写入成本,换取查询速度的交易。没有免费午餐——这是理解后续一切的底色。
既然索引要"有序",那为什么不直接用二叉搜索树、红黑树、跳表?Hussein 给出的关键答案是两个字:磁盘。
内存里,二叉树挺好的。但数据库的数据动辄几个 GB 到几 TB,不可能全放进内存,必须存在磁盘上。磁盘的致命弱点是:随机读一次(一次 I/O)非常慢(机械盘要移动磁头,SSD 也要跨页),慢到和读一个连续块差好几个数量级。所以最优策略是:每次 I/O 多读一点(一个"页"/page),把"树的层数"压到最少——层数越少,磁盘读的次数越少。
二叉树每个节点只有 2 个孩子,树太高,10 亿条数据要读约 30 层——也就是 30 次磁盘 I/O,不可接受。于是有了 B 树(B-tree):每个节点可以有很多个孩子(m 叉),把一棵又高又瘦的树压成又矮又胖。
但数据库最终普遍选择了 B+ 树(B-plus tree),而不是 B 树。Hussein 在视频里反复强调的关键差异是:
B 树:数据和键可以出现在任何节点(包括非叶子节点)。 B+ 树:数据只出现在叶子节点;非叶子(内部)节点里只有键和子节点指针,一个数据都不存。
这一句差异,决定了数据库的命运。
这是整期视频最反直觉、也最值钱的洞察,我把它单独拎出来讲透。
大多数人脑中的"树",每个节点都挂着真实的数据(值、记录)。但 B+ 树偏偏不让内部节点带货——内部节点里那些键,不是为了"被查到",而是为了"指路"。 它们是一块块"路牌":告诉你"你要找的值,往左走还是往右走"。真正的货物(行数据),全部压在最底层的叶子里。
为什么这个设计如此精妙?因为节点的容量是固定的(一个磁盘页,比如 InnoDB 默认 16KB)。一个内部节点如果不带货,只放"键 + 子页指针",每条记录就非常小——InnoDB 里子页指针 6 字节,加一个 Bigint 主键 8 字节,一条才 14 字节。于是:
一个 16KB 的页,能塞下约 16384 ÷ 14 ≈ 1170 个路牌。
也就是说,一个内部节点能分出 1170 个叉。这个数字叫 fanout(扇出)。Hussein 强调的核心就在这里:fanout 越大,树越矮。 因为每一层都把数据"摊平"成了 1170 份,三下五除二就能定位到任意一条记录。
如果是 B 树,内部节点还要塞数据,每个键后面跟着一大坨行记录,一个页只能放几十个键,fanout 骤降,树立刻变高——磁盘 I/O 翻倍。这就是 B 树"败给"B+ 树的根本原因:为了榨干每一个页的指路能力,必须让内部节点彻底卸货。
把上面的数字乘起来,就得到数据库圈那道著名的算术题,Hussein 在视频里算过,我用 InnoDB 的真实参数替你重算一遍:
结论震撼又朴素:一棵只有 3~4 层的 B+ 树,就能装下 2000 万到 256 亿行数据。 而每一次按主键查找,只需要从根走到叶——最多 3~4 次磁盘 I/O。对 256 亿行数据,你只需读 4 个页。
这就是为什么所有数据库都死守 B+ 树:它把"层数 = I/O 次数"这个最贵的指标,压缩到了一个近乎常数(3~4),与数据总量几乎无关。无论你的表是 10 万行还是 100 亿行,单条主键查询的成本都稳如老狗。红黑树要 30+ 次比较,B 树要 5~6 次 I/O,而 B+ 树永远只要 3~4 次。
如果 B+ 树只是"矮",那还不足以统治数据库世界。Hussein 在视频里点出的第二个杀手特性是:B+ 树的所有叶子节点,用链表串了起来(在 InnoDB 里是双向链表)。
为什么这个设计至关重要?想想这两类查询:
WHERE id = 837。从根走 3~4 层到叶子,一步到位。WHERE id BETWEEN 100 AND 500,或 ORDER BY created_at LIMIT 20。对范围查询,B 树会很尴尬:你要找 100~500 之间所有值,可能散落在树的各个角落,得反复从根往下走,I/O 次数爆炸。而 B+ 树优雅得多:先定位到 id=100 的那个叶子页,然后沿着叶子之间的链表,一路向右顺藤摸瓜,把 100→500 之间的所有叶子页顺序读出来。
更妙的是:叶子页在物理上往往被尽量分配在相邻的磁盘块(顺序 I/O 比随机 I/O 快 10~100 倍)。于是范围查询、ORDER BY、LIMIT 分页——这些互联网应用里 90% 的真实查询——都被这种"叶子手拉手 + 顺序读"吃得死死的。B 树做不到,因为它的数据散在每一层,没有这种"叶子连成一片"的结构。
这是 Hussein 视频里点到、但很多中文资料没讲透的进阶点,我帮你补全(这部分是我的延伸,视频未详述)。
在 InnoDB 里,表本身就是一棵 B+ 树,叫聚集索引(clustered index),按主键排序,叶子页里存的是完整的一行数据。也就是说,InnoDB 的表 = 主键索引,数据物理上就"长在"主键这棵树的叶子上(这种表叫 index-organized table)。
但你在其他列(比如 email、user_id)上加的索引,叫二级索引(secondary index / non-clustered index)。关键细节来了:二级索引的叶子页里,存的不是完整行数据,而是"被索引列的值 + 主键值"。
于是当你执行 SELECT * FROM users WHERE email = '[email protected]',且 email 上有二级索引时,发生的是两棵树的攀爬:
id = 837)。id = 837,回到聚集索引这棵树,再走 3~4 层,把完整行捞出来。这个"回主键树取数据"的动作,中文圈叫回表(bookmark lookup)。它意味着一次查询可能要 2 倍的 I/O。这就是为什么有"覆盖索引"(covering index)这种优化:如果你的查询只需要 email 和 id 两列,而二级索引叶子页里正好就有这两列,数据库就不用回表,直接返回——性能直接砍半。这是面试高频、生产高频的实战知识。
回到第一节埋下的伏笔。Hussein 在视频后半段会提醒:B+ 树给你的"3~4 次 I/O 查询",是有代价的——代价就是写放大(write amplification)。
每一次 INSERT/UPDATE/DELETE,数据库不仅要改聚集索引,还要维护每一棵相关的二级索引 B+ 树。因为索引必须时刻保持有序,一个新值插入,可能触发:
这就是为什么一个表上的索引越多,写入越慢。我见过一个表加了 8 个索引,写入吞吐直接掉到原来的 1/5。更隐蔽的是 UPDATE:哪怕你只改一个非索引列,如果这个列参与了某个索引,那棵索引树也要更新;而 UPDATE 一个被索引的列,等于"删旧值 + 插新值",在 B+ 树里是两次定位 + 可能的分裂。
所以 Hussein 真正想传递的元洞察是:索引是建立在你对这张表"读写比"判断之上的一笔投资。 读多写少的表(如配置表、字典表、日志查询表),多加索引划算;写多读少的表(如埋点、计数器、消息流水),每加一个索引都是给写入性能放血。盲目 CREATE INDEX,和盲目吃药一样危险。
把整期视频串起来,Hussein 想说的其实不是"B+ 树好",而是所有数据结构都是取舍,B+ 树是"读优化"路线的极致。与之相对的,是另一条"写优化"路线——LSM 树(Log-Structured Merge Tree),被 Cassandra、HBase、RocksDB、LevelDB 采用。LSM 树把写入先丢进内存的 memtable,再批量、顺序地刷成磁盘上的 SSTable,写入飞快,但读取时要查多层 SSTable,读放大严重。
这是一对镜像:
Hussein 没有下"B+ 树更好"的结论——一个合格的后端工程师,不该问"哪个更好",而该问"我的读写比是什么、我的查询模式是什么、我能容忍多大的写放大"。这才是这期视频的真正价值:它不是教你一个数据结构,而是教你用"读写比 + 放大代价"这把尺子,去丈量所有存储引擎的设计选择。
flowchart TD
R["🟦 根节点 Root(内部页)<br/>只存【键 + 子页指针】<br/>★纯路牌·不带货★"]
R -->|1170 叉 fanout| I1["内部节点<br/>键 + 指针<br/>无数据"]
R -->|1170 叉 fanout| I2["内部节点<br/>键 + 指针<br/>无数据"]
I1 --> L1["🍃 叶子页<br/>键 + 行数据"]
I1 --> L2["🍃 叶子页<br/>键 + 行数据"]
I2 --> L3["🍃 叶子页<br/>键 + 行数据"]
I2 --> L4["🍃 叶子页<br/>键 + 行数据"]
L1 <-. 双向链表 .-> L2
L2 <-. 双向链表 .-> L3
L3 <-. 双向链表 .-> L4
R -.- M1["每页 16KB ÷ 14B ≈ 1170 路牌<br/>→ 树极矮"]
L1 -.- M2["叶子手拉手<br/>→ 范围查询顺序读"]
L1 -.- M3["3~4 层 = 3~4 次磁盘 I/O<br/>装下 2000 万~256 亿行"]
classDef internal fill:#e8f0fe,stroke:#4285f4,stroke-width:2px;
classDef leaf fill:#e6f4ea,stroke:#34a853,stroke-width:2px;
classDef note fill:#fff8e1,stroke:#f9a825,stroke-dasharray:5 5;
class R,I1,I2 internal;
class L1,L2,L3,L4 leaf;
class M1,M2,M3 note;
1. 加索引前,先算三笔账。 别再无脑 CREATE INDEX。先问:这列的读写比是多少?查询是等值查询还是范围查询?这一列是否经常和其他列一起被查询(能不能组成联合索引 / 覆盖索引)?三笔账算清,再决定加不加、加几个。
2. 善用 EXPLAIN,别靠玄学。 执行计划里看两件事:type 字段是不是 ref/range/eq_ref(走了索引)还是 ALL(全表扫描);Extra 里有没有 Using index(覆盖索引,免回表)或 Using filesort(索引没帮上排序,要警惕)。一个 ALL 就意味着你加的索引没被用上——可能是查询里对列用了函数(WHERE YEAR(create_time)=...)、用了左模糊(LIKE '%abc')、或隐式类型转换,这些都会让 B+ 树"失效",必须重写查询。
3. 写多场景,慎用索引;考虑 LSM 引擎。 如果你的业务是写密集(IoT 埋点、订单流水、监控指标),与其堆索引,不如考虑换存储:时序数据库(InfluxDB、TDengine)或基于 LSM 的引擎(RocksDB、ScyllaDB)。理解了 B+ 树 vs LSM 的读写镜像,你才知道"为什么 TimescaleDB 和 InfluxDB 用不同的底层"。
4. 联合索引的"最左前缀"是 B+ 树的直接推论。 联合索引 (a, b, c) 在 B+ 树里是按 a→b→c 排序的,所以 WHERE a=? AND b=? 能用,WHERE b=? AND c=? 用不上(因为没有 a,无法在第一层路牌定位)。这不是死记硬背的规则,而是"树是按这个顺序排列的"的必然结果——理解了结构,规则自然成立。
5. 认清"回表"代价,优先设计覆盖索引。 高频查询语句,尽量让二级索引的叶子页就包含所需列,避免回表(省一半 I/O)。这对高并发接口是立竿见影的优化。
B+ 树之所以能让数据库"3~4 次磁盘 I/O 搞定任意一条记录",靠的不是树,而是把内部节点全部榨成不带货的"纯路牌"、把 fanout 撑到上千——理解了"读写比 + 写放大"这把尺子,你才算真正懂了索引。
本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
