前言
🍊 缘由
写 SQL 时,最让人踏实的画面,莫过于 EXPLAIN 里出现了期待已久的索引名。
结果一点运行,速度还是不对劲。索引明明用上了,咋还慢?
先别急着给索引判死刑。它可能已经帮你找到了主键,只是查询需要的字段不在里面,MySQL 还得拿着主键再找一次。
索引是用了,数据没拿全。多出来的这一趟,就是回表。
👽 人话解释
二级索引先帮你找到主键。要查的字段不在索引里,MySQL 就拿着主键回去取整行。
狗哥今天就拿一张用户表,把这趟路捋明白。为了不把大家带沟里,先说范围:InnoDB、显式主键、普通 B+ 树索引。实测环境是 MySQL 8.0.11,原理按 MySQL 8.4 官方手册核对。
🎯 主要目标
这篇只解决三个问题:
- 主键索引和二级索引,叶子节点里分别放了什么?
- 回表是怎么发生的,覆盖索引又省掉了哪一步?
- 怎么从
EXPLAIN里看出端倪?
正文
🍪 一、主键索引和二级索引,家底不一样
先来张简单的用户表:
CREATE TABLE user_demo (
id BIGINT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
age INT NOT NULL,
KEY idx_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
拿一条数据举例:id=7,name=狗哥,age=18。
年龄只是示例,别问,问就是永远十八。
在 InnoDB 里,主键索引就是聚簇索引,叶子节点放着行数据。idx_name 属于二级索引,叶子记录里有索引列 name,还会带上主键 id。MySQL 官方手册:聚簇索引与二级索引

先把两边的家底亮出来:
- 主键索引:有
id、name、age。 idx_name:有name、id,没有age。
图里只展开了一条叶子记录,其他节点和内部字段都省略了。别被 B+ 树吓住,盯住叶子节点就行。
🍪 二、查个年龄,MySQL 为啥又跑了一趟
现在按名字查年龄:
SELECT age
FROM user_demo
WHERE name = '狗哥';
假设优化器选择 idx_name,MySQL 会先找到狗哥,拿到 id=7。
可 SQL 要的是 age,idx_name 里偏偏没有。MySQL 只能拿着 id=7 去主键索引找出这行,读取 age=18。

从二级索引取得主键,再去聚簇索引取行数据,这个过程就叫回表。
它发生在 MySQL 内部,应用并没有多发一条 SQL。
把它想成取快递就顺了:二级索引像取件码,先帮你找到货架位置;想拿到包裹,还得去货架跑一趟。
同名用户只有几个,问题通常不大;如果一次匹配几万条记录,后面要取的行自然也多。这里说的是访问行数据,并不等于每条记录都会单独读一次磁盘,相关页可能已经在 Buffer Pool 中。
直接按 id=7 查询,则会沿主键索引找到行数据,一般不叫回表。
🍪 三、只换两个字段,这趟路省了
WHERE 条件不动,只把返回字段换一下:
SELECT id, name
FROM user_demo
WHERE name = '狗哥';
这次要的 id 和 name,idx_name 里都有,通常可以直接返回。

这种情况叫覆盖索引:一个索引已经包含这条查询需要的全部列。MySQL 官方手册:索引如何用于查询
别再找什么“覆盖索引创建语法”了。它描述的是索引和查询的关系:同一个 idx_name,查 id、name 时能够覆盖,查 age 时就不行。
这也是不建议随手写 SELECT * 的原因。页面只展示名字,却把年龄、邮箱等字段全查出来,本来能省掉的回表又加回来了。
不过,显式写字段也不保证不回表。前面的 SELECT age 只查一列,照样要回。关键得看选中的索引里有没有查询需要的字段。
再抠个细节:碰上某些并发更新,InnoDB 可能还要访问主键索引检查数据版本。覆盖索引通常能省掉取行步骤,别背成“绝对不碰主键索引”。MySQL 官方手册:多版本机制
🔍 四、本狗实测:EXPLAIN 到底长啥样
狗哥用 MySQL 8.0.11 建了 10000 行测试数据,执行 ANALYZE TABLE 后跑了两条 SQL,没有强制指定索引:
EXPLAIN SELECT age
FROM user_demo WHERE name = '狗哥';
EXPLAIN SELECT id, name
FROM user_demo WHERE name = '狗哥';
结果不长,挑几个关键字段看:
| 返回字段 | key | rows | Extra |
|---|---|---|---|
age | idx_name | 1 | NULL |
id, name | idx_name | 1 | Using index |
两条 SQL 都用了 idx_name。第二条的 Extra 出现 Using index,说明查询需要的列可以从索引里取得。
第一条的 Extra 是 NULL,可别看到空值就懵。它要查 age,idx_name 里没有,所以还得读取行数据。
Using index condition 也别和 Using index 混在一起。多了一个 condition,意思就变了。它表示索引条件下推:先在索引里判断能处理的条件,再按需读取行,仍然可能回表。MySQL 官方手册:EXPLAIN 输出
完整的复现 SQL和原始输出都留在文末附件中。数据量、版本和统计信息不同,你本地跑出的执行计划也可能不同。
🍯 五、看到回表,别激动
先看查询慢不慢,再决定要不要动。
只查几条记录,数据又在缓存里,正常回表没什么问题。真正值得检查的,是调用频繁、一次匹配很多行的 SQL。
优化时可以从两处下手:
- 去掉业务不需要的返回字段,别顺手
SELECT *。 - 高频查询确实需要其他字段,再评估联合索引。比如经常按姓名查年龄,可以考虑
(name, age)。
联合索引也不能使劲堆字段。索引越宽,占用的空间越大,写入时还得跟着维护。MySQL 官方手册:优化 InnoDB 查询
用户详情页本来就需要完整数据,该回表就回表。为了消灭回表,把一条 SQL 拆成几次请求,反倒更折腾。
总结
下次排查慢 SQL,除了看 key,再多瞅两眼:索引里有没有需要的字段,预计要读多少行。
真改了索引,记得用相同条件对比执行计划和耗时。别凭感觉优化,数据库不吃玄学这一套。
我是狗哥。下次有人说“这条 SQL 明明走索引了”,你可以顺手再问一句:要的字段,索引里都有吗?
🍈猜你想问
如何与狗哥联系进行探讨?

关注公众号【JavaDog程序狗】,回复【入群】或【加入】,一起聊技术、聊踩坑。