一、什么是 EXPLAIN?
EXPLAIN 是 MySQL 自带的 SQL 体检工具,也是所有 SQL 优化的第一步。只需记住一句话:
在 SELECT 语句前面加
EXPLAIN,就能看到 MySQL 打算怎么执行这条 SQL。
sql
EXPLAIN SELECT * FROM zzw_juanzhu_xuesheng WHERE id = 1;
执行后会输出一张表格,包含这些列:
| 列名 | 作用 |
|---|---|
| id | 查询的执行顺序编号 |
| select_type | 查询类型(简单查询/子查询/UNION等) |
| table | 正在访问的表名 |
| partitions | 匹配的分区(分区表才显示) |
| type | ⭐ 访问类型,最核心的性能指标 |
| possible_keys | 可能用到的索引 |
| key | 实际使用的索引 |
| key_len | 使用到的索引长度(字节) |
| ref | 与索引比较的列或常量 |
| rows | MySQL 预估需要扫描的行数 |
| filtered | 按条件过滤后剩余行数的百分比 |
| Extra | 额外信息(是否回表、是否排序等) |
核心要点:
- EXPLAIN 不会真正执行 SQL,不会查询数据,不会修改数据
- 它只是提前告诉我们:MySQL 打算怎么执行这条 SQL
- 通过体检结果,能精准发现:有没有走索引、是不是全表扫描、索引有没有失效、SQL 为什么卡顿
🔥 面试高频:所有 MySQL 性能优化,第一步一定是 EXPLAIN 分析执行计划。
二、select_type:查询类型(理解子查询必看)
select_type 表示查询的类型,看懂它才能分析复杂 SQL(子查询、UNION)。
| select_type | 含义 | 场景 |
|---|---|---|
| SIMPLE | 简单查询,不包含子查询和 UNION | 最常见的单表查询 |
| PRIMARY | 最外层的主查询 | 有子查询时,最外层就是 PRIMARY |
| SUBQUERY | 子查询中的第一个 SELECT | SELECT 或 WHERE 中的子查询 |
| DERIVED | 派生表(FROM 子句中的子查询) | FROM (SELECT ...) AS t |
| UNION | UNION 中第二个及之后的 SELECT | UNION 联合查询 |
| UNION RESULT | UNION 合并后的结果集 | 从临时表中读取合并结果 |
| DEPENDENT SUBQUERY | 依赖外部查询的子查询 | 子查询引用了外部表的字段 |
| MATERIALIZED | 物化子查询(MySQL 5.6+) | 子查询结果先存入临时表 |
理解要点:简单查询全部显示 SIMPLE,出现 DERIVED 和 DEPENDENT SUBQUERY 就要警惕性能问题。
三、核心字段:type(看懂它,就懂 90% 优化)
type 是执行计划中最重要的字段,代表 SQL 的查询效率等级。
性能排序(必背):
sql
system → const → eq_ref → ref → range → index → ALL
└─────────────── 越往左性能越好 ──────────
下面从最优到最差,逐一讲解:
1. system(最优,但极少见)
适用场景:表只有一行数据(系统配置表等)
通俗理解:整张表就一条记录,直接拿就行了,比 const 还快。
📌 实际开发中几乎遇不到,了解即可。
2. const(最优级别,性能天花板)
适用场景:单表查询 + 主键/唯一索引 + 精准等值匹配
sql
EXPLAIN SELECT * FROM zzw_juanzhu_xuesheng WHERE id = 1;
-- type = const(id 是主键)
通俗理解:主键是唯一的,MySQL 不需要遍历任何数据,直接通过索引定位到唯一一行,速度最快。
限制条件:只能用于单表查询,多表联表不会出现该级别。
✅ 评价:最优 SQL,线上满分级别。
3. eq_ref(联表查询最优)
适用场景:多表 JOIN 联表查询,被关联的表通过主键/唯一索引精准匹配
sql
EXPLAIN SELECT * FROM orders o
JOIN users u ON o.user_id = u.id;
-- users 表的 type = eq_ref(u.id 是主键)
通俗理解:两张表关联时,一对一精准匹配数据,没有多余扫描,是联表查询的最好状态。
✅ 评价:联表场景满分级别。
4. ref(线上最标准、最常用)
适用场景:普通索引 + 等值查询,可匹配多条数据
sql
-- xuesheng_name 建有普通索引
EXPLAIN SELECT * FROM zzw_juanzhu_xuesheng WHERE xuesheng_name = '张三';
-- type = ref
通俗理解:通过普通索引快速筛选数据,虽然可能匹配多行,但完全不用全表扫描,性能稳定可控。
注意 :此时 EXPLAIN 的 ref 列会显示 const,表示用常量值来匹配索引。如果是联表查询,ref 列会显示关联表的字段名。
✅ 评价:线上业务推荐的标准写法,性能合格。
5. range(范围查询,可控安全)
适用场景:索引字段做范围匹配
支持所有语法 :>、<、>=、<=、BETWEEN、IN、LIKE '前缀%'
sql
EXPLAIN SELECT * FROM zzw_juanzhu_xuesheng WHERE xuesheng_name LIKE '王%';
-- type = range
重点避坑:很多人以为 range 是慢查询,这是错的!小范围查询完全没问题,只有一次性查询数万行大数据,才会变慢。
✅ 评价:可控级别,日常业务完全可用。
6. index(全索引扫描,高危!)
适用场景:无需过滤数据,只需遍历索引
两种触发情况:
情况一:只查询索引字段,不需要回表
sql
-- xuesheng_name 有索引
EXPLAIN SELECT xuesheng_name FROM zzw_juanzhu_xuesheng;
-- type = index,Extra = Using index(覆盖索引)
情况二:ORDER BY 索引字段
sql
EXPLAIN SELECT * FROM zzw_juanzhu_xuesheng ORDER BY xuesheng_name;
-- 也可能触发 type = index
通俗解释:MySQL 不会扫描整张数据表,但会从头到尾遍历整棵二级索引树,属于全量遍历,不是精准查询。
核心特点:
- 比全表扫描快(索引文件体积远小于数据表)
- 本质还是全量扫描,数据量大了一定会卡顿
- Extra 显示
Using index,代表无需回表,是覆盖索引
⚠️ 评价:小表可用,大表绝对禁止。
7. ALL(全表扫描,线上红线禁止)
通俗解释:从头到尾遍历整张数据表,不使用任何二级索引过滤,性能最差。
高频出现场景(必记):
| 场景 | 示例 |
|---|---|
| 查询字段无索引 | WHERE xuesheng_yizizhu = 'xx'(字段无索引) |
| 索引失效:左模糊 | LIKE '%王' |
| 索引失效:全模糊 | LIKE '%王%' |
| 索引失效:函数运算 | WHERE YEAR(create_time) = 2024 |
| 索引失效:隐式转换 | WHERE phone = 13800138000(phone 是 varchar) |
| 无条件全表查询 | SELECT * FROM 表 |
❌ 评价:线上绝对禁止,属于不合格 SQL。
四、key vs possible_keys:可能 vs 实际(关键对比)
| 字段 | 含义 |
|---|---|
| possible_keys | MySQL 认为可能用到的索引列表 |
| key | MySQL 实际选择使用的索引 |
当两者不一致时,说明:
- 优化器在多个索引中做了选择
- 可能选错了索引(统计信息不准)
- 可以用
FORCE INDEX强制指定索引来对比验证
sql
-- 对比测试
EXPLAIN SELECT * FROM table WHERE a = 1 AND b = 2; -- 看优化器选哪个
EXPLAIN SELECT * FROM table FORCE INDEX(idx_a) WHERE a = 1; -- 强制用索引a
五、key_len:联合索引用了几个字段(面试必考)
key_len 表示 MySQL 实际使用的索引长度(字节数),是判断联合索引用了几个字段的关键指标。
示例 :假设联合索引 idx_name_age(name, age)
- name 是 varchar(50),utf8mb4 编码,允许 NULL → 长度 = 50 × 4 + 2 + 1 = 203
- age 是 int,允许 NULL → 长度 = 4 + 1 = 5
sql
EXPLAIN SELECT * FROM users WHERE name = '张三' AND age = 25;
-- key_len = 208(203 + 5),说明联合索引两个字段都用上了
sql
EXPLAIN SELECT * FROM users WHERE name = '张三';
-- key_len = 203,说明只用了 name 一个字段
面试要点:key_len 越大,说明用到的索引字段越多,过滤效果越好。
六、Extra:额外状态标记(完整版)
| 标记 | 含义 | 评价 |
|---|---|---|
| Using index | 覆盖索引,不需要回表查数据 | ✅ 优秀 |
| Using index condition | 使用了索引下推(ICP,MySQL 5.6+) | ✅ 优秀 |
| Using where | 使用了 WHERE 过滤 | 正常(但配合 type=ALL 就是坏消息) |
| Using filesort | 需要额外排序(没走索引排序) | ⚠️ 需要优化 |
| Using temporary | 需要创建临时表(GROUP BY/DISTINCT) | ⚠️ 需要优化 |
| Using join buffer | 联表用了 Join Buffer(被驱动表没走索引) | ⚠️ 需要优化 |
| Impossible WHERE | WHERE 条件永远为假,不会返回任何数据 | 检查 SQL 逻辑 |
| Select tables optimized away | 查询被优化器直接优化掉了 | ✅ 最优(如 SELECT MAX(id) FROM t) |
🔥 面试高频 :
Using index(覆盖索引)和Using index condition(索引下推)的区别。
七、rows:预估扫描行数(重要但别全信)
rows是 MySQL 基于统计信息估算的值,不是精确值- 数值越小性能越好,越大越需要优化
- 如果统计信息过期,rows 可能严重偏离实际值
更新统计信息:
sql
ANALYZE TABLE zzw_juanzhu_xuesheng;
八、EXPLAIN 进阶用法
| 命令 | 作用 | 版本要求 |
|---|---|---|
EXPLAIN FORMAT=JSON |
输出 JSON 格式,信息比表格更详细 | MySQL 5.6+ |
EXPLAIN FORMAT=TREE |
树形输出,直观显示执行路径 | MySQL 8.0.16+ |
EXPLAIN ANALYZE |
真正执行 SQL 并输出每步实际耗时 | MySQL 8.0.18+ |
sql
-- 最推荐:MySQL 8.0.18+ 用这个,能看到实际耗时
EXPLAIN ANALYZE SELECT * FROM users WHERE name = '张三';
⚠️ 注意:
EXPLAIN ANALYZE会真实执行 SQL,生产环境慎用!
九、四大终极误区(解决 99% 的疑问 + 面试必考)
误区 1:EXPLAIN 中 key=NULL,代表完全没用到索引?(面试高频坑)
标准答案:错误(仅限 InnoDB 引擎)
InnoDB 是聚簇索引引擎,整张表的所有数据都存在主键(聚簇索引)中。我们看到的 type=ALL 全表扫描,底层本质是遍历主键聚簇索引树,不可能脱离索引。
正确定义 :key=NULL 仅仅代表本次查询没有使用二级索引做过滤和定位,不是没有用到任何索引。
误区 2:InnoDB 和 MyISAM 全表扫描有什么区别?(面试对比考点)
两个引擎核心差异,直接记住结论:
| 引擎 | 全表扫描本质 |
|---|---|
| InnoDB | 数据和索引一体,全表扫描 = 遍历主键聚簇索引 |
| MyISAM | 数据和索引文件完全分离,全表扫描直接读原始数据文件,完全不碰任何索引 |
误区 3:字段有索引,无条件查询就一定很快?
完全错误。
不管字段有没有索引,无 WHERE 条件的查询都是全量扫描:
sql
SELECT name FROM 表; -- type=index 全索引遍历(扫描二级索引树)
SELECT * FROM 表; -- type=ALL 全表遍历(扫描聚簇索引/数据文件)
区别的本质:
type=index:扫描二级索引树(只含索引字段+主键,体积小)type=ALL:扫描聚簇索引/数据文件(包含全部字段,体积大)
两者只是扫描文件大小不同,数据过万后都会变慢,线上一律禁止无条件全表查询。
误区 4:无索引的 LIKE '王%' 不算慢查询?
小表(4000 行)场景下,这条 SQL 是百分百全表扫描,但因为耗时极低,没触发慢查询阈值,所以不记录。
核心结论:小表无感知,上万数据量必然卡顿,变成标准慢 SQL。
十、索引失效的完整场景(面试必考 100%)
| 失效场景 | 示例 | 原因 |
|---|---|---|
| 左模糊/全模糊 LIKE | LIKE '%王'、LIKE '%王%' |
无法利用索引有序性 |
| 函数运算 | WHERE YEAR(create_time) = 2024 |
对字段做了函数运算 |
| 运算表达式 | WHERE id + 1 = 10 |
字段参与了运算 |
| 隐式类型转换 | WHERE phone = 13800138000(phone 是 varchar) |
字符串字段用数字查询 |
| OR 条件有非索引字段 | WHERE name = '王' OR age = 20(age 无索引) |
OR 两边都需要索引 |
| 违反最左前缀 | 联合索引 (a,b,c),查询 WHERE b=1 AND c=2 |
跳过了 a 字段 |
| 范围查询右边失效 | 联合索引 (a,b,c),查询 WHERE a=1 AND b>10 AND c=3 |
b 用了范围,c 失效 |
| != 或 <> | WHERE status != 1 |
不等于不走索引 |
| IS NULL / IS NOT NULL | 视数据分布而定,可能不走索引 | 优化器判断 |
| NOT IN / NOT EXISTS | 通常不走索引 | 无法利用索引 |
十一、实战排查流程(直接套用)
遇到慢 SQL,按以下步骤排查:
sql
1. EXPLAIN 看 type
├─ type=ALL/index → 全量扫描,进入第 2 步
├─ type=range/ref → 走索引,检查扫描行数
└─ type=const/eq_ref → 最优,无需优化
2. 检查 WHERE 条件字段
├─ 无索引 → 加索引
└─ 有索引但没用上 → 进入第 3 步
3. 检查是否索引失效
├─ 函数运算/隐式转换 → 修改 SQL 写法
├─ 左模糊 LIKE → 考虑全文索引或 ES
└─ 联合索引未满足最左前缀 → 调整索引或 SQL
4. 检查 key 和 possible_keys 是否一致
└─ 不一致 → 统计信息可能过期,ANALYZE TABLE
5. 检查 Extra
├─ Using filesort → ORDER BY 字段加索引
├─ Using temporary → GROUP BY/DISTINCT 字段加索引
└─ Using join buffer → 被驱动表关联字段加索引
6. rows 过大
└─ 考虑覆盖索引、分页、数据归档
十二、优化口诀(直接套用)
| 级别 | 目标 | 说明 |
|---|---|---|
| ✅ 优先追求 | const / eq_ref / ref / 小范围 range |
线上标准 |
| ⚠️ 尽量避免 | index 全索引遍历 |
大表禁止 |
| ❌ 绝对禁止 | ALL 全表扫描 |
线上红线 |
| ⚠️ 需要优化 | Using filesort、Using temporary |
影响性能 |
十三、复盘 + 面试速记(必背)
- 核心本质:EXPLAIN 看的是 MySQL 预执行方案,不运行 SQL
- type 优先级必背 :
system → const → eq_ref → ref → range → index → ALL - 最大易错点 :InnoDB 全表扫描是遍历主键索引,
key=NULL只是不用二级索引 - key_len 面试题:联合索引用了几个字段,看 key_len 的值
- select_type:出现 DERIVED、DEPENDENT SUBQUERY 要警惕
- 禁用场景 :无条件全表查询、左模糊
%xxx、无索引查询、索引字段做函数运算 - 面试加分项 :
type=index和type=ALL都是全量扫描,大表都不允许 - 覆盖索引 :Extra 显示
Using index,不需要回表,性能最优 - 索引下推 :Extra 显示
Using index condition,MySQL 5.6+ 优化特性 - EXPLAIN ANALYZE:MySQL 8.0.18+ 可以看真实执行耗时