ElIauk

MySQL EXPLAIN 执行计划入门

EXPLAIN 不是"看一眼 type" 慢查询优化时,很多人打开 EXPLAIN,看到 type=ALL 就加索引,看到 type=ref 就认为没问题。结果索引加了一堆,查询还是慢;或者明明走了索引,扫描行数却大得离谱。 EXPLAIN 输出的是一份"优化器打算怎么执行"的报告,里面十来个字段要

评论
MySQL EXPLAIN 执行计划入门

EXPLAIN 不是"看一眼 type"

慢查询优化时,很多人打开 EXPLAIN,看到 type=ALL 就加索引,看到 type=ref 就认为没问题。结果索引加了一堆,查询还是慢;或者明明走了索引,扫描行数却大得离谱。

EXPLAIN 输出的是一份"优化器打算怎么执行"的报告,里面十来个字段要连起来看:用哪个索引、索引用了多长、预估扫多少行、过滤比例多少、有没有额外排序。只看 type 很容易误判。

关键列逐个说

列 含义 怎么看
id 查询序号 数字越大越先执行,相同则从上往下
select_type 查询类型 SIMPLE、PRIMARY、SUBQUERY、DERIVED、UNION
table 访问的表 可能是派生表名
partitions 命中的分区 分区表才关注
type 访问类型 从好到差:system > const > eq_ref > ref > range > index > ALL
possible_keys 可能用到的索引 只是候选,不代表实际用
key 实际使用的索引 为 NULL 表示没走索引
key_len 索引使用长度 判断联合索引用到了第几列
ref 索引比较的对象 常量、列名或函数结果
rows 预估扫描行数 估算值,不代表最终返回行数
filtered 过滤后剩余百分比 越小说明过滤效果越好
Extra 附加信息 排序、临时表、覆盖索引都在这里

key_len 是判断联合索引的利器。以 (user_id, status, create_time) 为例,如果 user_id 是 bigint(8 字节)、status 是 tinyint(1 字节)、create_time 是 datetime(5 字节),那么:

  • 只用到 user_id:key_len 约 8;
  • 用到 user_id + status:约 9;
  • 三列全用上:约 14。

再考虑列是否允许 NULL,key_len 会多 1 字节。对照表结构算一下,就能知道索引到底用到了哪一列。

type 与 Extra 怎么读

type 反映访问方式,常用的几档:

  • const:主键或唯一索引等值查询,最多一行;
  • eq_ref:联表时被驱动表用主键/唯一索引匹配,每行只取一条;
  • ref:普通索引等值查询,可能多行;
  • range:索引范围扫描,between、>、in 常见;
  • index:扫描整棵索引树,比 ALL 好一点,但仍然是全量;
  • ALL:全表扫描。

Extra 里几个高频词:

值 含义 是否要处理
Using index 覆盖索引,不回表 好事
Using where 存储引擎返回后在 Server 层过滤 正常,结合 rows 看
Using index condition 索引下推,在引擎层过滤 通常是好事
Using filesort 需要额外排序 数据量大时要优化
Using temporary 用了临时表 group by、distinct 常见,要关注
Using join buffer 联表没有合适索引 通常需要加索引

一个实际例子:

EXPLAIN
SELECT id, amount
FROM t_order
WHERE user_id = 100 AND status = 1
ORDER BY create_time DESC;

如果 key 是 idx_user_status_time、rows 只有几十、Extra 里没有 Using filesort,说明索引顺序和查询、排序都匹配,这条 SQL 基本不用再改。

MySQL 8.0.18 以后还可以用 EXPLAIN ANALYZE,它会真正执行语句并给出实际耗时和行数:

EXPLAIN ANALYZE
SELECT id, amount FROM t_order WHERE user_id = 100;

注意它是真的执行,不是估算。对 UPDATE、DELETE 用之前,先包在事务里并准备回滚。

常见坑

1. 只看 type 不看 rows

type=ref 不代表快。如果 rows 是几百万,照样慢。反过来,小表上 type=ALL 可能比走索引还快,优化器选择全表扫描是有成本依据的。

2. 把 possible_keys 当成实际索引

possible_keys 只是候选,key 才是实际用的。两个都有值时,要看 key 具体是哪一个,别优化错了对象。

3. 测试库数据量太小

开发库只有几千行,怎么查都是全表扫描最快,执行计划和生产完全不是一回事。优化要在接近真实数据量的环境里做,至少要看 rows 的数量级。

4. 统计信息过期

rows 是估算值,依赖统计信息。表数据变化很大时,先执行 ANALYZE TABLE t_order,再看执行计划。否则可能看到"明明有索引却不走"的假象。

5. 绑定变量导致计划不同

同样的 SQL,参数不同,优化器可能选择不同索引。用 EXPLAIN 时要带入真实参数,不要用 ? 占位后随便填一个值。

6. 对 DML 直接跑 EXPLAIN ANALYZE

EXPLAIN ANALYZE UPDATE ... 会真的改数据。要么在事务里执行后回滚,要么只对 SELECT 使用。生产环境更要谨慎。

7. 只优化一条 SQL

单条 SQL 从 2 秒降到 200 毫秒,但如果 QPS 很高、并发一上来,数据库 CPU 和连接数仍然是瓶颈。优化要结合整体并发和资源水位看。

排查步骤

比较稳妥的流程是:

  1. 从慢查询日志里捞出耗时最长的 SQL,确认执行频率;
  2. 用真实参数跑 EXPLAIN,记录 type、key、key_len、rows、Extra;
  3. 判断问题类型:没走索引、索引用了但回表多、排序产生 filesort、临时表过大;
  4. 针对性调整:加联合索引、改 SQL 写法、减少 select *、拆分大查询;
  5. 调整后再 EXPLAIN 对比,必要时用 EXPLAIN ANALYZE 看实际行数;
  6. 上线后观察慢查询数量和接口耗时,确认没有把压力转移到写入侧。

执行计划是手段,不是目的。最终要回答的问题是:这条 SQL 扫了多少行、回了多少次表、排序和临时表有多大。把这几个数字降下来,查询自然就快了。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者