Skip to content

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额外信息看有没有 filesorttemporaryUsing index

type:访问类型好不好

type 列从好到差:

type含义
const主键或唯一索引等值查询,最快
ref非唯一索引等值查询
range索引范围查询(>、<、BETWEEN、IN)
index扫整棵索引树(比扫全表好,但也不理想)
ALL全表扫描,最差

看到 ALL 基本就是没走索引或索引失效了,要查原因。

key:实际走了哪个索引

possible_keys 列出可用的索引,key 是优化器实际选的。有时有索引但优化器没选(统计信息过期、查询条件让索引不划算),这时要分析为什么。

keyNULL 表示全表扫——要么没建索引,要么索引失效了(参考上一篇的失效场景)。

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.log

mysqldumpslow 把慢日志汇总统计——-s t 按总耗时排序,-t 10 取前 10 条。它会按查询模式合并(参数替换成 SN),不会每条都列出来。

输出大致:

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 N

Count 是这条模式执行了多少次,Time 是平均和总耗时。看到某条查询执行 152 次总耗时 488 秒,就是重点优化目标。

五、定位到查询后的优化步骤

  1. EXPLAIN 看执行计划——type、key、rows、Extra 四个字段
  2. 判断问题——没走索引?扫太多行?filesort?
  3. 加或改索引——按最左前缀和列顺序原则
  4. 改查询——避免函数包裹索引列、避免 SELECT *、避免前通配 LIKE
  5. 验证——再 EXPLAIN 看 rows 降下来了、filesort 没了

优化是个循环——改完看效果,还不满意再调。重点是拿 EXPLAIN 和慢日志的数据说话,不靠猜。

执行计划之后

查询优化讲完了。但 MySQL 存数据、查数据的底层——InnoDB 存储引擎——还没讲。为什么能回滚、为什么崩溃不丢数据、为什么有行锁,都和 InnoDB 有关。下一篇讲 InnoDB 存储引擎:Buffer Pool、Redo/Undo Log。