覆盖索引为什么能减少 B+ 树回表?它和普通二级索引有什么区别?
简化版
覆盖索引指查询需要的列都能从某个索引里拿到,不必再根据二级索引叶子中的主键去聚簇索引查整行。它减少了一次 B+ 树查找和随机 I/O,尤其适合读多、返回列少的查询。
详细版
以 InnoDB 为例,二级索引叶子节点通常保存「二级索引 key + 主键值」。如果查询列不在二级索引中,流程是:
- 先走二级索引找到主键;
- 再用主键回到聚簇索引查整行;
- 从整行里取需要的列。
如果索引已经包含查询所需列,例如 (user_id, status, created_at) 能满足:
select status, created_at from t where user_id = ?
那么数据库扫描二级索引叶子就能返回结果,不需要回表。这就是覆盖索引。它的代价是索引变宽,占更多磁盘和内存,写入更新成本更高。
完整版教学
一、为什么会有回表
在聚簇索引组织的存储引擎里,表数据本身按主键 B+ 树组织,叶子页保存完整行。二级索引也是 B+ 树,但它的叶子页通常不保存整行,而是保存二级索引列和主键。这样可以避免每个二级索引都复制完整行,节省空间。
回表就是二级索引查到主键后,再去主键聚簇索引查一次完整行。它本质上是两次树查找:一次从二级索引树到主键,一次从聚簇索引树到行数据。
记忆钩子:二级索引像通讯录里的「姓名 -> 身份证号」,回表就是拿身份证号再去档案柜取完整档案。
二、覆盖索引怎样避免回表
如果查询需要的列已经全部存在于二级索引叶子里,数据库就不必去聚簇索引拿整行。例如索引 (user_id, status, created_at),查询只需要 status 和 created_at,并且条件是 user_id=?,那么扫描这个索引就够了。
普通二级索引:
secondary B+Tree -> primary key -> clustered B+Tree -> row
覆盖索引:
secondary B+Tree -> needed columns -> return
少一次 B+ 树查找,尤其在结果很多、主键分散时,能明显减少随机 I/O。
三、覆盖索引和联合索引是什么关系
覆盖索引不一定是单独的索引类型,它通常由联合索引实现。一个索引是否覆盖,取决于它是否包含这条 SQL 所需的列。对另一条 SQL 来说,同一个索引可能就不覆盖。
| SQL | 索引 (user_id,status,created_at) 是否覆盖 |
|---|---|
select status from t where user_id=? | 覆盖 |
select created_at,status from t where user_id=? | 覆盖 |
select name from t where user_id=? | 不覆盖 |
select * from t where user_id=? | 通常不覆盖 |
所以覆盖索引是「查询视角」的概念,不是建索引语句里某个特殊关键字。
四、为什么覆盖索引不等于一定更快
覆盖索引减少读路径回表,但会让索引更宽。索引越宽,一个页能放下的索引项越少,B+ 树扇出下降,缓存命中也可能变差。写入、更新、删除时还要维护更多索引内容。
举个数字例子:16KB 页里,如果索引项平均 32B,大约能放 500 条;如果为了覆盖把索引项扩到 128B,只能放约 125 条。叶子页更多,扫描和缓存压力都会变大。
五、如何判断一条查询是否真正覆盖
判断覆盖要看 select 列、where 列、order by 列、group by 列,以及存储引擎二级索引叶子天然包含的主键。只要执行这条查询不需要读取索引之外的列,就可以认为覆盖。
在 MySQL 执行计划里,常见标志是 Using index,表示可以只通过索引返回数据。但执行计划还要结合 type、rows、过滤条件等一起看,不能只盯一个标志。
六、覆盖索引的设计取舍
覆盖索引适合高频、读多、返回列少、延迟敏感的查询。不要为了覆盖所有 SQL 把索引建得特别宽,那会让写入成本和存储成本变高。更好的做法是针对核心查询设计少量高价值联合索引。
| 适合覆盖 | 不适合盲目覆盖 |
|---|---|
| 高频列表页查询 | 很少执行的后台查询 |
| 只返回少数列 | select * |
| 回表代价高、结果多 | 写入非常频繁的宽表 |
| 排序分页稳定 | 列经常更新 |
索引设计本质是读写空间三方权衡,不是越多越宽越好。
七、常见误区与追问
- 误区:覆盖索引是一种新的树结构。 它仍是 B+ 树索引,只是查询列都被索引覆盖。
- 追问:为什么二级索引要回表? 二级索引叶子通常只保存索引列和主键,不保存完整行。
- 误区:只要用了索引就没有回表。 用二级索引定位后仍可能需要回聚簇索引取其他列。
- 追问:覆盖索引有什么代价? 索引变宽,占空间、降扇出、增加写维护成本。
- 误区:
select *很容易覆盖。 除非索引包含所有列,否则通常无法覆盖。 - 追问:主键列是否也算被二级索引覆盖? 在 InnoDB 二级索引叶子里通常包含主键值,所以只查二级索引列和主键时经常可以覆盖。
八、加强记忆
覆盖索引的核心是「索引页里就有答案」。普通二级索引常是先查索引再按主键回表;覆盖索引让查询需要的列都在二级索引叶子中,省掉第二次 B+ 树查找。它提升读性能,但会让索引更宽、更贵。面试里要把覆盖索引、联合索引、回表三者串起来讲,答案就很稳。