MySQL EXPLAIN 执行计划:慢 SQL 优化实战

当一条 SQL 在线上突然变慢,第一反应不应该是加内存或换机器,而是看它的执行计划。MySQL 的 EXPLAIN 能把优化器的决策过程完整展示出来:它到底有没有用上索引、走了哪一种访问路径、估算扫描了多少行、是否产生了临时表或文件排序。读懂 MySQL EXPLAIN 执行计划,是慢 SQL 优化的第一步,也是投入产出比最高的一步。理解索引的底层结构有助于看懂执行计划,建议先回顾《MySQL 索引底层原理》。

一、为什么必须看执行计划

很多人优化 SQL 靠”感觉”:给字段加个索引、把 SELECT * 改成具体列、顺手加个 LIMIT。但同一句写法在不同数据量、不同索引结构下表现可能天差地别。执行计划把优化器的选择可视化,让我们知道它是否用了索引、扫描成本有多高。没有这层信息,优化就是盲人摸象,往往加了索引反而更慢。

更重要的是,执行计划是”可复现、可对比”的。修改前截一张图,改完索引后再截一张,性能差异一目了然。它也是和同事、DBA 沟通的共同语言:一句”type 还是 ALL”比”这条 SQL 很慢”专业得多。

二、如何拿到执行计划

用法极其简单:在 SELECT 前加 EXPLAIN 即可。生产环境建议结合慢查询日志先定位到需要优化的语句,再针对它做分析,相关思路可参考《PostgreSQL 慢查询排查》。

-- 基础用法:在 SELECT 前加 EXPLAIN
EXPLAIN
SELECT id, name, age FROM user
WHERE age > 30 AND city = 'beijing'
ORDER BY create_time DESC
LIMIT 100;

-- 更详细:JSON 格式,能看到 cost、used_key_parts、filtered 等
EXPLAIN FORMAT=JSON
SELECT * FROM orders WHERE user_id = 10086;

传统表格格式已经够用,FORMAT=JSON 则适合深挖:它能看到优化器对每一种可能路径的成本估算,以及索引条件下推是否生效。日常排障先用表格,遇到疑难再用 JSON。

三、type 列:访问类型从好到坏

type 表示 MySQL 访问数据的方式,是判断 SQL 好坏最直观的一列。从左到右性能递减,我们的目标是让线上核心查询至少达到 range 及以上。

type 值含义性能
const / system主键或唯一索引等值匹配,最多命中一行最优
eq_ref联表时主键/唯一索引关联,每行只匹配一条极好
ref非唯一索引等值匹配,可能命中多行很好
range索引范围扫描,如 >、<、BETWEEN、IN良好
index全索引扫描,比全表略快但仍扫大量行一般
ALL全表扫描,逐行匹配,最需警惕最差

看到 ALL 不要慌,先确认表数据量:小表(几百行)全表扫描毫无压力;但大表出现 ALL 基本意味着缺索引或索引失效,必须处理。

理解 type 还有一个隐含价值:它能帮你判断”索引到底有没有生效”。当查询条件对索引列做了函数运算、或发生了隐式类型转换(比如用字符串去匹配整数字段),优化器就无法走 ref 或 range,只能退化成 index 甚至 ALL。养成写完 SQL 看一眼 type 的习惯,能挡掉大量低级慢查询。

四、key、rows 与 filtered:索引与扫描量

key 显示实际命中的索引,possible_keys 是候选列表。如果 possible_keys 有值而 key 为 NULL,说明优化器认为全表比走索引更划算(常见于回表成本过高或命中率太低)。rows 是估算扫描行数,越小越好;filtered 表示存储引擎返回后,在 Server 层被 WHERE 过滤掉的百分比,越高说明索引选择性越好。

一个常见误区是”有索引就一定快”。当查询返回列很多、索引选择性差时,优化器可能放弃索引改走全表。合理的数据库设计规范能从源头避免这类问题,详见《数据库设计规范实战》。

五、Extra 列:最容易被忽略的杀手

Extra 往往藏着性能隐患。下面两张情况一旦出现,基本就是要优化的信号。

Extra 值含义与对策
Using filesort无法利用索引完成排序,需额外排序;应建立”排序字段”参与的联合索引
Using temporary使用了临时表,常见于 GROUP BY / DISTINCT;优先用索引覆盖分组列
Using index覆盖索引,无需回表,是最理想的状态
Using where存储引擎返回后还需在 Server 层过滤,通常可接受
Using index condition索引条件下推生效,在引擎层提前过滤,是好事

需要特别强调的是,Using filesort 和 Using temporary 在数据量小的时候不一定致命,但当表增长到几十万、上百万行时,它们会迅速拖垮响应时间——因为内存排序/临时表空间不足时会落盘到磁盘文件,I/O 开销陡增。排序或分组字段若能进入联合索引,往往能同时消除这两个坏信号。

六、一个真实慢 SQL 的优化过程

假设一张 120 万行的订单明细表,下面这句查询在高峰期要跑 2 秒以上。先 EXPLAIN 看问题:

-- 优化前:type=ALL,rows 扫全表 120 万行,Extra 出现 Using filesort
EXPLAIN SELECT * FROM order_item
WHERE user_id = 10086 AND status = 1
ORDER BY pay_time DESC;

-- 建立覆盖索引:等值列在前(最左前缀),排序列在后
CREATE INDEX idx_user_status_pay
ON order_item (user_id, status, pay_time);

-- 优化后:type=ref,rows 降到几十行,Extra 出现 Using index

关键点在于索引列顺序:等值过滤的 user_id、status 放在前面,排序的 pay_time 放在最后,这样排序也能借助索引完成,顺带消除 Using filesort。如果只建 (user_id),过滤能走索引,但排序仍要 filesort。

七、覆盖索引与索引下推

覆盖索引指查询所需的列全部包含在索引中,无需回表取数据,Extra 会显示 Using index。它对性能提升极明显,尤其在大表上。索引条件下推(ICP)则让 WHERE 中索引列的过滤在存储引擎层就完成,减少回表次数。

-- 索引下推(Index Condition Pushdown):MySQL 5.6+ 默认开启
-- Extra 会显示 Using index condition,表示过滤在引擎内完成
SELECT * FROM user
WHERE city = 'beijing' AND name LIKE '张%';
-- 联合索引 (city, name) 可让 name 的 LIKE 前缀匹配在引擎内完成

八、用 Docker 快速搭建实验环境

想安全地练习执行计划分析,本地起一个 MySQL 容器最快。打开慢查询日志,把阈值设低一点,就能马上看到哪些语句被记录。

docker run -d --name mysql-lab \
  -e MYSQL_ROOT_PASSWORD=secret \
  -p 3306:3306 \
  mysql:8.0 \
  --slow_query_log=1 \
  --long_query_time=1

容器化部署也是现代数据库运维的常态,更多 Docker 实践见《Docker 实战》。

九、落地清单与验证方法

优化不是改完就结束,一定要用数据验证。推荐流程:① 用慢查询日志或监控定位慢 SQL;② EXPLAIN 看 type/rows/Extra;③ 按最左前缀建联合索引或改为覆盖索引;④ 再次 EXPLAIN 确认 type 提升、filesort 消失;⑤ 用数据库压测工具量化前后差异,方法见《数据库压测实战》。

最后提醒:索引不是越多越好。每个索引都会拖慢写入、占用空间,优化要以”读多写少的核心查询”为主。把执行计划当作日常体检工具,你的慢 SQL 问题会越来越少。

上一篇 Java 日期时间 API 实战:从 Date 到 java.time
下一篇 前端内存泄漏排查实战:从现象到根因定位
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]