回表是二级索引无法覆盖查询字段时,通过主键回聚簇索引获取数据的操作;其性能瓶颈在于乱序主键引发的随机io,而非单纯多一次查找。

回表是二级索引查不到字段时的“补救动作”
MySQL(InnoDB)执行 SELECT name FROM user WHERE city = '北京' 时,如果只建了 INDEX idx_city (city),就会回表——因为该索引叶子节点只存 city 和主键 id,不存 name。引擎只能先用索引找到匹配的 id 列表,再一个个拿这些 id 去聚簇索引里捞完整行。这不是设计缺陷,而是二级索引的物理结构决定的:它本就不存全量数据。
看懂 EXPLAIN 里的 Using index 和 NULL
这是判断是否回表最直接的信号:
-
Extra: Using index→ 索引覆盖,**不回表**(所有查询字段都在索引中) -
Extra: NULL(且Type不是ALL)→ 很可能**正在回表** -
Extra: Using index condition→ 索引下推(ICP),仍可能回表,但会先在索引层过滤掉部分记录,减少回表次数
例如:EXPLAIN SELECT id, city FROM user WHERE city = '北京' 显示 Using index;而 SELECT name, city 就大概率是 NULL。
回表真正慢在哪?不是“两次查找”,而是随机 IO
很多人以为回表慢是因为“多走一次 B+ 树”,其实核心瓶颈是磁盘访问模式:
- 二级索引查出的主键值通常是乱序的(比如
id IN (105, 28, 993, 41)) - 拿着这些乱序
id去聚簇索引查,就触发大量**随机 IO**:每次加载一个新页,缓存利用率极低 - 当回表行数超过几千,性能可能还不如直接扫聚簇索引(即全表扫描)
所以优化方向不是“避免用二级索引”,而是让回表更少、更有序——比如开启 mrr=on(Multi-Range Read),它会把一批要回表的 id 排序后再批量读取,把随机 IO 变成顺序 IO。
实战中怎么绕过回表?覆盖索引不是加字段那么简单
加联合索引看似简单,但容易踩坑:
- 字段顺序很重要:
INDEX idx_city_name (city, name)能覆盖WHERE city = ? AND SELECT name,但INDEX idx_name_city (name, city)就不能用于WHERE city = ? - 别盲目加太多字段:索引变宽 → 单页存的键值变少 → B+ 树层数增加 → 二级索引本身变慢
- 注意隐式类型转换:哪怕索引是
(city, age),如果写WHERE city = 123(传了数字),MySQL 可能放弃索引,导致全表扫描,根本轮不到回表
真正有效的做法是:先用 EXPLAIN 定位回表语句,再根据 SELECT 字段 + WHERE 条件 + ORDER BY 组合,设计最小必要联合索引,而不是堆字段。
回表本身不可怕,可怕的是没意识到它背后是随机 IO —— 很多“加了索引反而更慢”的 case,问题不在索引有没有,而在回表是不是成了 IO 放大器。











