JavaDog程序狗
发布于 2026-09-13 / 2 阅读
0
0

【MySQL】索引明明用上了,SQL 咋还慢?原来还在回表

前言

🍊 缘由

写 SQL 时,最让人踏实的画面,莫过于 EXPLAIN 里出现了期待已久的索引名。

结果一点运行,速度还是不对劲。索引明明用上了,咋还慢?

先别急着给索引判死刑。它可能已经帮你找到了主键,只是查询需要的字段不在里面,MySQL 还得拿着主键再找一次。

索引是用了,数据没拿全。多出来的这一趟,就是回表

👽 人话解释
二级索引先帮你找到主键。要查的字段不在索引里,MySQL 就拿着主键回去取整行。

狗哥今天就拿一张用户表,把这趟路捋明白。为了不把大家带沟里,先说范围:InnoDB、显式主键、普通 B+ 树索引。实测环境是 MySQL 8.0.11,原理按 MySQL 8.4 官方手册核对。

🎯 主要目标

这篇只解决三个问题:

  1. 主键索引和二级索引,叶子节点里分别放了什么?
  2. 回表是怎么发生的,覆盖索引又省掉了哪一步?
  3. 怎么从 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,还会带上主键 idMySQL 官方手册:聚簇索引与二级索引

聚簇索引与二级索引解释图:聚簇索引叶子节点保存行数据,二级索引叶子节点保存索引列和主键

先把两边的家底亮出来:

  • 主键索引:有 id、name、age
  • idx_name:有 name、id,没有 age

图里只展开了一条叶子记录,其他节点和内部字段都省略了。别被 B+ 树吓住,盯住叶子节点就行。

🍪 二、查个年龄,MySQL 为啥又跑了一趟

现在按名字查年龄:

SELECT age
FROM user_demo
WHERE name = '狗哥';

假设优化器选择 idx_name,MySQL 会先找到狗哥,拿到 id=7

可 SQL 要的是 ageidx_name 里偏偏没有。MySQL 只能拿着 id=7 去主键索引找出这行,读取 age=18

MySQL 回表:从 idx_name 取得 id=7,再访问主键索引读取 age=18

从二级索引取得主键,再去聚簇索引取行数据,这个过程就叫回表。

它发生在 MySQL 内部,应用并没有多发一条 SQL。

把它想成取快递就顺了:二级索引像取件码,先帮你找到货架位置;想拿到包裹,还得去货架跑一趟。

同名用户只有几个,问题通常不大;如果一次匹配几万条记录,后面要取的行自然也多。这里说的是访问行数据,并不等于每条记录都会单独读一次磁盘,相关页可能已经在 Buffer Pool 中。

直接按 id=7 查询,则会沿主键索引找到行数据,一般不叫回表。

🍪 三、只换两个字段,这趟路省了

WHERE 条件不动,只把返回字段换一下:

SELECT id, name
FROM user_demo
WHERE name = '狗哥';

这次要的 idnameidx_name 里都有,通常可以直接返回。

同一个 idx_name 索引,查询 age 需要回表,查询 id 和 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 = '狗哥';

结果不长,挑几个关键字段看:

返回字段keyrowsExtra
ageidx_name1NULL
id, nameidx_name1Using index

两条 SQL 都用了 idx_name。第二条的 Extra 出现 Using index,说明查询需要的列可以从索引里取得。

第一条的 ExtraNULL,可别看到空值就懵。它要查 ageidx_name 里没有,所以还得读取行数据。

Using index condition 也别和 Using index 混在一起。多了一个 condition,意思就变了。它表示索引条件下推:先在索引里判断能处理的条件,再按需读取行,仍然可能回表。MySQL 官方手册:EXPLAIN 输出

完整的复现 SQL原始输出都留在文末附件中。数据量、版本和统计信息不同,你本地跑出的执行计划也可能不同。

🍯 五、看到回表,别激动

先看查询慢不慢,再决定要不要动。

只查几条记录,数据又在缓存里,正常回表没什么问题。真正值得检查的,是调用频繁、一次匹配很多行的 SQL。

优化时可以从两处下手:

  1. 去掉业务不需要的返回字段,别顺手 SELECT *
  2. 高频查询确实需要其他字段,再评估联合索引。比如经常按姓名查年龄,可以考虑 (name, age)

联合索引也不能使劲堆字段。索引越宽,占用的空间越大,写入时还得跟着维护。MySQL 官方手册:优化 InnoDB 查询

用户详情页本来就需要完整数据,该回表就回表。为了消灭回表,把一条 SQL 拆成几次请求,反倒更折腾。


总结

下次排查慢 SQL,除了看 key,再多瞅两眼:索引里有没有需要的字段,预计要读多少行。

真改了索引,记得用相同条件对比执行计划和耗时。别凭感觉优化,数据库不吃玄学这一套。

我是狗哥。下次有人说“这条 SQL 明明走索引了”,你可以顺手再问一句:要的字段,索引里都有吗?


🍈猜你想问

如何与狗哥联系进行探讨?

加瓦狗联系方式

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


评论