MySQL 索引经常被一句话概括成“底层是 B+ 树”。这句话没错,但真正有用的是继续往下问:为什么是 B+ 树,树高通常为什么不高,主键索引和二级索引怎么取数据,最左匹配到底限制了什么。
这篇把源 note 里的几段材料收束成一条主线:从 InnoDB 的页开始,看 B+ 树如何减少磁盘 IO,再回到日常建索引和看执行计划时最容易用上的判断。
先从页理解索引
InnoDB 的数据不是一行一行散落在磁盘上,而是按页组织。默认情况下,一个 InnoDB 页是 16KB。页里可以存放真实数据,也可以存放索引键值和指针。
如果一行记录大约 1KB,一个叶子页粗略能放十几行数据。真实情况还会扣掉页头、槽目录、事务信息、填充因子等开销,但这个粗算足够帮我们理解 B+ 树的数量级。
索引要解决的问题是:不要为了找一行数据,把大量页都扫一遍。B+ 树的做法是把页组织成多层结构:
- 非叶子节点页保存索引键和指向下层页的指针。
- 叶子节点页保存最终数据,或者保存二级索引键和主键值。
- 每层节点内部有序,可以用比较和二分思想快速确定下一跳。
一次从根节点走到叶子节点的过程,本质上就是用少量页读取换掉大范围扫描。
为什么树高通常不高
假设主键是 bigint,占 8 字节;InnoDB 指针大约占 6 字节。那么一个非叶子节点里的一个索引单元大约是 14 字节。
按 16KB 页粗算,一个非叶子页能放:
16384 / 14 ~= 1170
也就是说,一个根页大约能指向一千多个下层页。如果树高为 2,也就是根节点加叶子节点,粗略能放:
1170 * 16 ~= 18720 行
如果树高为 3,根节点指向中间层,中间层再指向叶子页:
1170 * 1170 * 16 ~= 2190 万行
这就是为什么很多千万级表的 B+ 树高度仍然只有 3 层左右。按主键等值查找时,走索引通常只需要少量页访问,剩下的成本更多来自缓存命中、回表、过滤条件、排序和网络返回。
这个估算不是精确容量公式,但它能解释一个很重要的事实:B+ 树不是靠“树特别聪明”取胜,而是靠高扇出把树高压得很低。
为什么不是普通二叉树或 B 树
如果用普通二叉搜索树,每个节点只有两个方向。数据一多,树高就会明显增加,磁盘读取次数也会增加。数据库索引关心的不是内存里的几次指针跳转,而是磁盘和页缓存层面的读写成本。
B 树比二叉树更适合磁盘,因为一个节点可以有很多分支。但 B 树的非叶子节点也可能保存真实数据。真实数据一放进去,单个页能容纳的键和指针就会变少,扇出下降,树高更容易变高。
B+ 树把真实数据集中放在叶子节点,非叶子节点尽量只做导航。这样非叶子节点更“瘦”,一个页能放更多索引项,树高就更容易保持在很小的范围内。
B+ 树还有一个很适合范围查询的特性:叶子节点之间通常按顺序相连。查到范围起点以后,可以顺着叶子节点向后扫描,而不是频繁回到上层节点重新找。
主键索引和二级索引的区别
InnoDB 表通常是索引组织表。主键索引的叶子节点保存整行数据,所以主键索引也叫聚簇索引。
如果按主键查:
select * from user where id = 100;
索引从根节点一路定位到叶子节点,叶子节点里就有完整行。
二级索引则不同。二级索引叶子节点保存的是二级索引字段和主键值,不直接保存完整行。比如有索引 idx_name(name):
select * from user where name = 'zhao';
执行时可能先走 idx_name 找到对应主键,再拿主键回到聚簇索引查整行。这个过程就是回表。
所以二级索引不是越多越好。它能加速定位,但写入、更新和删除时要维护更多索引;如果查询字段不能被索引覆盖,还可能产生回表成本。
覆盖索引为什么快
覆盖索引指的是查询需要的字段都能从二级索引里拿到,不需要再回表。
例如有索引:
create index idx_name_age on user(name, age);
查询只取 name 和 age:
select name, age from user where name = 'zhao';
如果执行计划确认走了这个索引,叶子节点里的内容已经满足查询,就可以少一次回到主键索引的过程。
这也是为什么很多查询不建议随手 select *。你取的字段越多,越容易从“索引能直接回答”变成“索引只能帮忙找到主键,然后还要回表”。
联合索引和最左匹配
联合索引不是把多个单列索引简单叠加,而是按字段顺序构建一棵有序结构。比如索引:
create index idx_name_age_status on user(name, age, status);
这棵树先按 name 排序;name 相同的部分再按 age 排序;age 也相同的部分再按 status 排序。
因此下面的条件通常能用上索引的前缀:
where name = 'zhao'
where name = 'zhao' and age = 18
where name = 'zhao' and age = 18 and status = 1
但如果条件没有最左字段:
where age = 18
这棵索引就很难直接定位,因为全局并不是按 age 排序的。
还有一个常见细节:范围条件可能截断后续字段的有效利用。比如:
where name = 'zhao' and age > 18 and status = 1
索引可以用 name 和 age 缩小范围,但 status 往往不能再像等值条件那样继续精确定位,只能在范围里过滤。具体仍要以 MySQL 版本和执行计划为准,但建联合索引时要把高频等值过滤、排序和范围条件的顺序一起考虑。
建索引时先问三个问题
第一,查询是等值、范围、排序,还是分页?
等值查询适合把选择性较好的字段放在索引前面;范围查询要注意后续字段是否还能有效利用;排序字段如果能和过滤条件一起走同一个联合索引,可以减少额外排序。
第二,查询是否需要回表?
如果某个高频查询只需要少量字段,可以考虑覆盖索引。但不要为了覆盖每个查询都建很宽的索引,索引越宽,页里能放的索引项越少,维护成本也越高。
第三,写入成本能不能接受?
索引会提升读,但会拖慢写。每次插入、删除、更新索引字段,都要维护对应 B+ 树。对于写多读少、低选择性、很少作为过滤条件的字段,建索引前要更谨慎。
小结
理解 MySQL B+ 树索引,可以抓住这几句话:
- InnoDB 默认以
16KB页组织数据。 - B+ 树通过高扇出把树高压低,减少页读取次数。
- 主键索引叶子节点保存整行数据,二级索引叶子节点保存索引字段和主键值。
- 二级索引查完整行可能需要回表,覆盖索引能减少回表。
- 联合索引按字段顺序排序,最左匹配来自这套排序方式。
- 索引不是越多越好,读性能、写成本、回表和排序都要一起看。
以后再看到“千万级表为什么索引还能很快”,就可以从页、扇出、树高和回表这几个词往回推。它们比单纯背“B+ 树”更接近日常排查里的真实判断。




