Appearance
07|执行计划与慢查询
上一篇讲了索引原理。但一条查询到底走了没走索引、扫了多少行、有没有额外排序,光靠猜不准。
本篇讲两个工具:EXPLAIN 让 MySQL 说出这条查询的执行计划,慢查询日志帮你找出哪些查询慢。这两个是排查慢查询的核心。
一、为什么查询慢
一条查询慢,可能的原因:
- 没走索引,全表扫描几百万行
- 走了索引但回表太多
- 用了
filesort(额外排序)或temporary(临时表) - 扫的行数远大于实际返回的行数
要确认是哪种,用 EXPLAIN 看 MySQL 实际怎么执行这条查询。
二、EXPLAIN 看执行计划
在查询前面加 EXPLAIN:
sql
EXPLAIN
SELECT id, uri, created_at
FROM access_logs
WHERE user_id = 1001
AND created_at >= '2026-05-01'
AND created_at < '2026-06-01'
ORDER BY created_at DESC
LIMIT 20;返回一行(或多行)结果,关键看几个字段:
| 字段 | 含义 | 关注点 |
|---|---|---|
type | 访问类型 | const > ref > range > index > ALL,ALL 是全表扫 |
possible_keys | 优化器能用的索引 | 为空说明没可用索引 |
key | 实际选用的索引 | NULL = 没走索引,全表扫 |
key_len | 索引使用长度 | 看联合索引用了几列 |
rows | 预估扫描行数 | 大但实际只返回几行时要关注 |
Extra | 额外信息 | 看有没有 filesort、temporary、Using index |
type:访问类型好不好
type 列从好到差:
| type | 含义 |
|---|---|
const | 主键或唯一索引等值查询,最快 |
ref | 非唯一索引等值查询 |
range | 索引范围查询(>、<、BETWEEN、IN) |
index | 扫整棵索引树(比扫全表好,但也不理想) |
ALL | 全表扫描,最差 |
看到 ALL 基本就是没走索引或索引失效了,要查原因。
key:实际走了哪个索引
possible_keys 列出可用的索引,key 是优化器实际选的。有时有索引但优化器没选(统计信息过期、查询条件让索引不划算),这时要分析为什么。
key 是 NULL 表示全表扫——要么没建索引,要么索引失效了(参考上一篇的失效场景)。
rows:扫了多少行
rows 是优化器预估要扫描的行数。一条查询只返回 20 行,但 rows 显示 100 万——说明扫了 100 万行才筛出 20 行,效率很低。理想情况 rows 接近实际返回行数。
Extra:额外做了什么
Extra 列透露额外操作:
| Extra | 含义 | 好不好 |
|---|---|---|
Using index | 覆盖索引,不用回表 | 好 |
Using where | 用 WHERE 过滤 | 正常 |
Using filesort | 额外排序 | 差,要优化 |
Using temporary | 用了临时表 | 差,要优化 |
Using filesort 意味着 MySQL 没法用索引的顺序直接返回结果,得额外排一次序。数据量大时排序很耗资源。Using temporary 更糟——要建临时表,通常出现在 GROUP BY、DISTINCT、ORDER BY 不同列时。
看到这两个要想办法优化——让排序能吃到索引顺序,或调整查询。
三、一条慢查询的排查过程
实际走一遍。某接口反馈慢,对应查询:
sql
SELECT * FROM access_logs
WHERE app_name = 'api' AND status_code = 500
ORDER BY created_at DESC LIMIT 20;先 EXPLAIN:
sql
EXPLAIN SELECT * FROM access_logs
WHERE app_name = 'api' AND status_code = 500
ORDER BY created_at DESC LIMIT 20;看输出:
text
type: ALL -- 全表扫
key: NULL -- 没走索引
rows: 1000000 -- 扫 100 万行
Extra: Using filesort -- 还额外排序三个问题:没走索引、扫全表、额外排序。建联合索引解决:
sql
ALTER TABLE access_logs ADD KEY idx_app_status_time (app_name, status_code, created_at);(app_name 和 status_code 等值放前面,created_at 范围/排序放后面——上一篇讲的顺序原则)
再 EXPLAIN:
text
type: ref -- 走了索引
key: idx_app_status_time
rows: 50 -- 只扫 50 行
Extra: Using index -- 不用回表从扫 100 万行变成扫 50 行,毫秒级返回。这就是索引优化的效果。
四、慢查询日志
线上不可能每条查询都手动 EXPLAIN。开慢查询日志,让 MySQL 自动记录超过阈值的查询:
ini
# my.cnf
slow_query_log = 1
slow_query_log_file = /data/mysql/logs/slow.log
long_query_time = 1 -- 超过 1 秒的记录
log_queries_not_using_indexes = 1 -- 没走索引的也记录改完重启生效。之后所有超过 1 秒的查询会被记到 slow.log。
看慢查询日志:
bash
mysqldumpslow -s t -t 10 /data/mysql/logs/slow.logmysqldumpslow 把慢日志汇总统计——-s t 按总耗时排序,-t 10 取前 10 条。它会按查询模式合并(参数替换成 S、N),不会每条都列出来。
输出大致:
text
Count: 152 Time=3.21s (488s) Lock=0.00s (0s) Rows=1000.0 (152000)
SELECT * FROM access_logs WHERE app_name='S' AND status_code=N ORDER BY created_at DESC LIMIT NCount 是这条模式执行了多少次,Time 是平均和总耗时。看到某条查询执行 152 次总耗时 488 秒,就是重点优化目标。
五、定位到查询后的优化步骤
- EXPLAIN 看执行计划——type、key、rows、Extra 四个字段
- 判断问题——没走索引?扫太多行?filesort?
- 加或改索引——按最左前缀和列顺序原则
- 改查询——避免函数包裹索引列、避免 SELECT *、避免前通配 LIKE
- 验证——再 EXPLAIN 看 rows 降下来了、filesort 没了
优化是个循环——改完看效果,还不满意再调。重点是拿 EXPLAIN 和慢日志的数据说话,不靠猜。
执行计划之后
查询优化讲完了。但 MySQL 存数据、查数据的底层——InnoDB 存储引擎——还没讲。为什么能回滚、为什么崩溃不丢数据、为什么有行锁,都和 InnoDB 有关。下一篇讲 InnoDB 存储引擎:Buffer Pool、Redo/Undo Log。