MySQL `EXPLAIN`:看懂执行计划、判断索引是否生效与排错套路

目标:你不仅能背出 type/key/rows/Extra 的含义,还能在「明明有索引却很慢」时,用执行计划把问题定位到:索引没用上 / 用错索引 / 回表过多 / 排序与临时表

1. 为什么要看执行计划:优化器的选择不等于你的直觉

SQL 慢通常不是 MySQL "算不动",而是:

  • 走错了访问路径(扫太多行/页)
  • 排序/分组触发 filesort 或临时表
  • 回表过多导致大量随机 IO

执行计划就是你拿到的"证据"。

2. EXPLAIN 最重要的 6 个字段(先记这 6 个就够用)

2.1 type:访问类型(从好到坏的直觉顺序)

常见从优到劣(不追求全背,理解含义):

  • const / system:按主键/唯一键等值命中一行
  • ref:非唯一索引等值匹配
  • range:范围扫描(>, between, like 'xxx%'
  • index:全索引扫描(扫索引树)
  • ALL:全表扫描

经验:

  • ALL 大概率有问题(除非表很小/统计类查询)
  • index 也不一定好,它可能在"扫完整个索引"

2.2 key & possible_keys

  • possible_keys:优化器认为可能用的索引
  • key:最终选择的索引

排错关键:

  • possible_keys 有但 key 为空:索引可能被写法破坏(函数、类型转换、OR 等)
  • key 选了但仍慢:可能回表大、选择性差、排序分组没被覆盖

2.3 rows:预计扫描行数(不是返回行数)

  • rows 越大,越可能慢
  • 观察 rows 与实际返回行数的倍率:
    • 例如返回 20 行,却扫描 20 万行,说明过滤不够"靠前"

2.4 filtered:过滤比例(估计值)

  • 低意味着:即使走索引也过滤不掉多少
  • 常见于:索引选择性差、条件不够精确

2.5 Extra:优化器额外动作(面试/排错高频)

重点识别:

  • Using index:覆盖索引(不回表)
  • Using where:还需要 server 层过滤
  • Using temporary:用了临时表(group by / order by 可能触发)
  • Using filesort:需要额外排序(不一定是文件,表示额外排序过程)

经验:

  • Using temporary + Using filesort 是性能红灯

2.6 一个可复现的最小例子:用同一张表看清 type/key/rows/Extra

先准备一张"很像业务表"的表:既有等值查询、也有范围、也有分页与排序。

sql 复制代码
create table t_order (
  id bigint primary key,
  user_id bigint not null,
  status tinyint not null,
  create_time datetime not null,
  amount int not null,
  title varchar(64) not null,
  key idx_user_time (user_id, create_time, id),
  key idx_status_time (status, create_time)
);

接下来用同一张表的不同查询,观察执行计划的关键差异。

例 1:等值命中(最理想,const/ref

sql 复制代码
explain select * from t_order where id = 123;

你期望看到:

  • type 接近 const
  • key = PRIMARY
  • rows 很小(≈1)

例 2:范围扫描(range

sql 复制代码
explain
select id, user_id, create_time
from t_order
where user_id = 1
  and create_time >= '2026-04-01 00:00:00'
  and create_time <  '2026-04-02 00:00:00'
order by create_time desc, id desc
limit 20;

你期望看到:

  • key 命中 idx_user_time
  • type 至少 range(或 ref + 范围下探,视数据分布与优化器而定)
  • Extra 尽量不要出现 Using filesort

如果这里出现 Using filesort,通常说明:

  • 排序字段与索引顺序不一致
  • 或优化器无法利用索引完成排序

例 3:回表放大(key 选对了但仍然慢)

sql 复制代码
explain
select *
from t_order
where user_id = 1
order by create_time desc
limit 10000, 20;

即使 key 命中 idx_user_time,它仍可能很慢,因为:

  • offset 本质是"扫描并丢弃"
  • select * 会导致大量回表

此时你应该结合:

  • rows 是否随页码变大
  • Extra 是否缺少 Using index(无法覆盖)

来判断瓶颈是否是"回表与大分页"。

3. 判断"索引是否生效"的快速规则

  • key 是否不为空
  • type 是否至少到 range/ref/const
  • rows 是否明显下降
  • Extra 是否出现 Using filesort/temporary

索引"生效"不是看 key 有值就结束,而是看它有没有把 rows 降到可接受范围,并避免额外排序/临时表。

4. 常见"索引没用上"的 7 种原因(面试高频)

4.1 对索引列做函数/表达式

  • where date(create_time) = '2026-04-03'
  • 破坏有序性,无法范围定位

修复:改成范围条件:

  • create_time >= '2026-04-03 00:00:00' and create_time < '2026-04-04 00:00:00'
对照组(建议面试这样讲)

错误写法(破坏索引有序性):

sql 复制代码
where date(create_time) = '2026-04-03'

正确写法(可范围定位):

sql 复制代码
where create_time >= '2026-04-03 00:00:00'
  and create_time <  '2026-04-04 00:00:00'

4.2 隐式类型转换

  • varchar_col = 123 可能触发转换导致索引失效

对照组:

  • 错:where phone = 13800138000(phone 是 varchar)
  • 对:where phone = '13800138000'

4.3 OR 条件

  • 可能导致优化器放弃索引(或改成 union 才能用)

对照组:

  • 错:where a = 1 or b = 2(容易走全表或选择性差的路径)
  • 对:拆成 union all(前提:两段条件互斥或你能接受去重逻辑)

4.4 like '%xxx'

  • 前缀 % 无法利用 B+ 树前缀有序

对照组:

  • 错:like '%abc'
  • 对:like 'abc%'(可以范围扫描)

4.5 联合索引未满足最左前缀

  • 索引 (a,b,c),只用 b 条件通常不走

对照组:

  • 错:where b = 2(缺少 a)
  • 对:where a = 1 and b = 2

4.6 返回列过多导致回表巨大

  • select * + 二级索引过滤,最终回表 N 次

典型修复方向:

  • 列表页只取必要列,尽量覆盖索引
  • 必须返回全字段:用"延迟关联"(先查主键 limit,再回表取整行)

4.7 统计信息不准/数据倾斜

  • 优化器误判成本,选择了全表或错误索引

处理:

  • analyze table(谨慎,低峰)
  • 重建索引/优化 SQL

补充说明:统计信息不准最容易出现的现象是:

  • 同一条 SQL,不同参数走不同计划(参数敏感)
  • 你在测试环境很快,线上慢(因为线上数据分布不同)

5. 排序/分组为什么慢:filesorttemporary 的根因

常见触发:

  • order by 字段与 where 使用的索引不一致
  • order by 多列顺序与联合索引不一致
  • group by 需要额外聚合

优化方向:

  • where + order by 尽可能使用同一个联合索引
  • 让查询走覆盖索引,减少回表

5.1 一个可解释的 filesort 例子:索引能过滤但无法按要求有序输出

sql 复制代码
-- 假设有索引 (user_id, create_time)
select *
from t_order
where user_id = 1
order by amount desc
limit 20;

这类查询可能:

  • where 能用索引定位 user_id
  • 但 order by 不是索引顺序,仍需要额外排序(Using filesort

优化手段:

  • 调整索引使其覆盖排序(例如把 amount 纳入联合索引,且满足 where + order)
  • 或接受排序成本但缩小参与排序的数据量(更强过滤)

5.2 一个高频 temporary 场景:group by 与 order by 不一致

sql 复制代码
select status, count(*)
from t_order
where user_id = 1
group by status
order by create_time desc;

如果聚合维度与排序维度无法用同一个索引顺序完成,可能出现临时表。通常建议:

  • 先把查询目标说清:到底要按 status 聚合还是要按时间排序
  • 对报表类查询考虑离线聚合或预聚合表

6. 线上排查手册:从现象到动作

  • 现象:偶发慢
    • 看是否锁等待(SHOW ENGINE INNODB STATUS / performance_schema)
  • 现象:一直慢
    • EXPLAIN,确认 rowsExtra
    • 抓慢日志 SQL + 参数,确认是否存在"参数导致走不同计划"(参数敏感)
  • 现象:扫描行巨大但索引存在
    • 查索引选择性、是否最左前缀、是否被函数/转换破坏

6.1 一套"先证据、再动作"的 checklist(更像线上排障)

  1. 先确认是不是"DB 慢"还是"等锁慢"
    • RT 高但 CPU 不高、连接堆积:优先怀疑锁等待
  2. 固定证据(同一条 SQL + 同一组参数)
    • 慢日志/链路追踪拿到 SQL 与参数
  3. EXPLAIN 只回答 4 个问题
    • 用了哪个索引(key
    • 扫了多少(rows
    • 是哪种访问(type
    • 有没有额外代价(Extra:filesort/temporary/Using index)
  4. 结合现象选择动作
    • ALL / rows 巨大:先修 where 与索引
    • Using filesort/temporary:修 where+order/group 的联合索引与写法
    • key 正确但仍慢:优先排查回表、offset、大结果集
    • 偶发慢:排查锁等待、热点更新、长事务

7. 面试背诵稿(60 秒)

我会用 EXPLAIN 看执行计划来判断 SQL 慢的根因。重点看 type(访问类型)、key/possible_keys(是否用到正确索引)、rows(预计扫描行数)和 Extra(是否出现 Using temporaryUsing filesort、是否覆盖索引 Using index)。

索引不生效常见原因是对索引列做函数、隐式类型转换、like '%xxx'OR、联合索引没走最左前缀,以及回表过多导致随机 IO。优化时我会尽量让 where + order by 走同一个联合索引并用覆盖索引减少回表,同时注意统计信息不准导致的计划误选。

相关推荐
小红的布丁2 小时前
Redis 持久化详解:AOF、RDB 与混合持久化如何平衡性能和可靠性
数据库·redis·缓存
牧魂.2 小时前
MySQL 主从延迟根因诊断法
mysql·高并发·主从复制·主从延迟·数据库调优
NPE~2 小时前
[App逆向]环境搭建下篇 — — 逆向源码+hook实战
android·javascript·python·教程·逆向·hook·逆向分析
qqxhb2 小时前
23|工具生态全景:本地文件、网络、数据库、浏览器自动化
网络·数据库·自动化·ai编程·最小权限·人工确认
Meme Buoy2 小时前
10.2需求分析-获取-定义-验证-管理
数据库·需求分析
东北甜妹2 小时前
MySQL数据库高级特性
mysql
Trouvaille ~2 小时前
【MySQL篇】从零开始:安装与基础概念
linux·数据库·mysql·ubuntu·c·教程·基础入门
周末也要写八哥2 小时前
追求性能极致为何不用Redis?
数据库·redis·缓存