当一条 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 问题会越来越少。




