在日常数据库开发与维护中,我们经常会遇到 SQL 执行效率低下的问题 ------ 明明数据量不大,查询却耗时数十秒;明明建了索引,查询却依然全表扫描。想要精准定位并解决这些问题,EXPLAIN(执行计划)是我们最核心的工具。本文将全面讲解EXPLAIN每个字段的含义,以及如何通过执行计划排查、定位、解决 SQL 性能问题,包括如何验证索引是否生效、判断是否使用了最优索引。
一、EXPLAIN 基础:是什么?怎么用?
EXPLAIN是 MySQL 提供的关键字,用于查看 SQL 语句的执行计划------ 即数据库优化器决定的 SQL 执行方式。通过执行计划,我们能清晰看到:
- SQL 是否走了索引、走了哪个索引
- 数据扫描的行数(rows)
- 表连接的方式(join_type)
- 数据读取的方式(访问类型)
使用方式
在需要分析的 SQL 语句前加上EXPLAIN即可,例如:
java
-- 分析单表查询
EXPLAIN SELECT id, name FROM user WHERE age = 25;
-- 分析关联查询
EXPLAIN SELECT u.name, o.order_no FROM user u
LEFT JOIN order o ON u.id = o.user_id
WHERE u.create_time > '2026-01-01';
执行后会返回一行或多行结果(多表关联时每行对应一个表),每行包含 12 个核心字段,接下来逐一解析。
二、EXPLAIN 核心字段详解
1. id:查询执行顺序标识
- 含义 :表示查询中执行子查询 / 操作的顺序,核心规则:
- id 相同:执行顺序由上至下
- id 不同:id 值越大,执行优先级越高
- id 为 NULL:表示结果集(如临时表、聚合操作)
- 示例:子查询的 id 会大于主查询,先执行子查询再执行主查询。
- 优化关注点:id 层级过多(如多层子查询)可能导致执行计划复杂,可考虑改写为 JOIN 优化。
2. select_type:查询类型
标识当前行是简单查询还是复杂查询,核心取值及含义:
| 取值 | 含义 | 优化关注点 |
|---|---|---|
| SIMPLE | 简单查询(无子查询、无 UNION) | 基础查询,重点看访问类型和索引 |
| PRIMARY | 复杂查询中的主查询 | 主查询的执行效率决定整体性能 |
| SUBQUERY | 子查询中的内层查询 | 子查询可能被重复执行,建议改写为 JOIN |
| DERIVED | 衍生表(FROM 后的子查询) | 衍生表会生成临时表,无索引,数据量大时性能差 |
| UNION | UNION 中的第二个及以后的查询 | UNION 会去重,可考虑 UNION ALL(无需去重) |
3. table:当前行对应的表
- 显示查询涉及的表名(或别名),若为衍生表会显示
derivedN(N 为 id)。 - 优化关注点:表的顺序决定 JOIN 顺序,不合理的 JOIN 顺序会导致大量数据扫描。
4. type:访问类型(核心!)
表示 MySQL 如何查找表中的行,是判断索引是否生效、是否最优的核心指标(从优到差排序):
| 取值 | 含义 | 是否走索引 | 优化建议 |
|---|---|---|---|
| system | 表只有 1 行(系统表) | - | 无需优化 |
| const | 通过主键 / 唯一索引查询,最多 1 行 | 是 | 最优状态 |
| eq_ref | JOIN 时,被驱动表通过主键 / 唯一索引匹配 | 是 | 理想的 JOIN 方式 |
| ref | 非唯一索引匹配(如普通索引) | 是 | 性能良好,可接受 |
| ref_or_null | 类似 ref,包含 NULL 值查询 | 是 | 注意 NULL 值对索引的影响 |
| range | 索引范围查询(如 BETWEEN、IN、>、<) | 是 | 比 ref 差,但比全表扫描好 |
| index | 扫描整个索引(索引覆盖但无过滤) | 是(但低效) | 需优化索引,增加过滤条件 |
| ALL | 全表扫描(Full Table Scan) | 否 | 必须优化!优先加索引 |
关键结论:
- 优化目标:至少达到
range,最好是ref/eq_ref; - 若出现
ALL(全表扫描),说明未走索引,是性能问题的核心信号。
5. possible_keys:可能使用的索引
- 含义:MySQL 优化器认为可能适用的索引列表(理论上可走的索引)。
- 注意:该字段仅表示 "候选索引",不代表实际使用。
6. key:实际使用的索引
- 含义:MySQL 实际执行时使用的索引(空值表示未走索引)。
- 核心判断点 :
- 若
possible_keys有值但key为空:说明索引失效,需分析失效原因; - 若
key有值但非预期索引:说明使用了非最优索引。
- 若
7. key_len:使用的索引长度
- 含义:表示索引使用的字节数,可判断索引是否被完全使用。
- 计算规则:
- 字符类型:varchar (10) utf8mb4 → 10*4=40 字节;
- 数值类型:int → 4 字节,bigint → 8 字节;
- 允许 NULL:额外加 1 字节。
- 优化关注点:
key_len越小,说明索引使用越不充分(如联合索引只用到前 1 列)。
8. ref:与索引匹配的列
- 含义:显示与索引
key匹配的列或常量(如const、user.age)。 - 示例:
ref = 'const'表示使用常量匹配索引;ref = 'u.id'表示用关联表的字段匹配索引。
9. rows:预估扫描行数
- 含义:MySQL 优化器预估需要扫描的行数(非精确值,但可反映量级)。
- 优化关注点:
rows越大,查询效率越低;优化目标是减少该值(通过索引过滤数据)。
10. filtered:过滤后的行百分比
- 含义:表示经过 WHERE 条件过滤后,剩余行数占扫描行数的百分比(值越大越好)。
- 示例:
rows=10000,filtered=10→ 最终仅返回 1000 行,过滤效率低,需优化 WHERE 条件。
11. Extra:额外信息(重要补充)
该字段包含执行计划的关键补充信息,是定位问题的重要依据,核心取值:
| 取值 | 含义 | 优化建议 |
|---|---|---|
| Using index | 索引覆盖(查询字段都在索引中,无需回表) | 最优状态,保留! |
| Using where | 使用 WHERE 条件过滤数据 | 正常,但若无索引则需优化 |
| Using temporary | 使用临时表(如 GROUP BY/ORDER BY 无索引) | 必须优化!加索引避免临时表 |
| Using filesort | 外部排序(ORDER BY 无索引) | 必须优化!加排序索引 |
| Using join buffer | JOIN 时使用缓冲区(无索引匹配) | 优化 JOIN 索引,减少缓冲区使用 |
| Impossible WHERE | WHERE 条件恒假(如 1=0) | 检查业务逻辑,避免无效查询 |
核心结论:
- 出现
Using temporary/Using filesort:性能杀手,必须优化; - 出现
Using index:理想状态,无需优化。
三、实战:如何通过 EXPLAIN 排查 SQL 问题?
步骤 1:判断是否走了索引
核心看 2 个字段:
type:是否为ALL(全表扫描 = 未走索引);key:是否为空(空 = 未走索引)。
示例 1:未走索引的情况
java
EXPLAIN SELECT * FROM user WHERE name = '张三';
执行计划结果:
type = ALL(全表扫描);key = NULL(未使用索引);rows = 10000(扫描 1 万行)。
原因分析 :name字段未建索引,导致全表扫描。
步骤 2:判断是否使用了最优索引
即使key不为空(走了索引),也可能不是最优索引,需检查:
possible_keys是否包含更优索引(如唯一索引 / 覆盖索引);type是否为低效的索引访问(如range/index);key_len是否过小(联合索引未完全使用)。
示例 2:使用了非最优索引
java
-- 表结构:user(id, age, name),索引:idx_age(age)、idx_age_name(age, name)
EXPLAIN SELECT * FROM user WHERE age = 25 AND name = '张三';
执行计划结果:
possible_keys = idx_age, idx_age_name;key = idx_age(仅使用了单字段索引);type = ref;rows = 500。
问题分析 :最优索引应为idx_age_name(联合索引,过滤更精准),但 MySQL 选择了idx_age,导致扫描行数更多。
步骤 3:定位索引失效的常见原因
若possible_keys有值但key为空,说明索引失效,常见原因:
-
索引字段参与函数 / 运算 :
java-- 错误:age+1参与运算,索引失效 SELECT * FROM user WHERE age + 1 = 26; -- 正确:改写为age = 25,索引生效 SELECT * FROM user WHERE age = 25; -
索引字段使用模糊查询(% 开头) :
java-- 错误:%开头,索引失效 SELECT * FROM user WHERE name LIKE '%张三'; -- 正确:仅尾部模糊,索引生效 SELECT * FROM user WHERE name LIKE '张三%'; -
OR 条件中部分字段无索引 :
java-- 错误:name无索引,导致age索引也失效 SELECT * FROM user WHERE age = 25 OR name = '张三'; -- 正确:给name建索引,或拆分查询 -
联合索引不满足最左匹配原则 :
java-- 索引:idx_age_name(age, name) -- 错误:跳过age,直接查name,索引失效 SELECT * FROM user WHERE name = '张三'; -- 正确:包含最左列age,索引生效 SELECT * FROM user WHERE age = 25 AND name = '张三'; -
数据类型不匹配 :
java-- 错误:age是int类型,传入字符串,索引失效 SELECT * FROM user WHERE age = '25'; -- 正确:传入数值类型,索引生效 SELECT * FROM user WHERE age = 25;
步骤 4:解决 SQL 性能问题的核心方法
1. 未走索引:添加合适的索引
- 优先创建联合索引(覆盖 WHERE 条件 + 排序 / 分组字段);
- 遵循最左匹配原则;
- 避免创建冗余索引(如已有 idx_age_name,无需再建 idx_age)。
2. 走了索引但非最优:强制使用最优索引 / 优化索引
-
强制索引(临时方案):
javaEXPLAIN SELECT * FROM user FORCE INDEX (idx_age_name) WHERE age = 25 AND name = '张三'; -
优化索引(长期方案):删除冗余索引,重新统计索引信息(
ANALYZE TABLE user;)。
3. 避免临时表 / 文件排序:添加排序索引
-
问题 SQL(出现 Using filesort):
javaEXPLAIN SELECT * FROM user WHERE age = 25 ORDER BY create_time; -
优化方案:创建联合索引
idx_age_create_time(age, create_time),覆盖过滤 + 排序,消除文件排序。
4. 减少扫描行数:优化 WHERE 条件
- 避免全表扫描的条件(如
IS NULL、!=); - 拆分大查询为小查询;
- 限制返回行数(LIMIT)。
四、实操案例:从 EXPLAIN 到 SQL 优化
问题场景
查询用户订单信息,耗时 5 秒:
java
SELECT u.name, o.order_no, o.amount
FROM user u
LEFT JOIN `order` o ON u.id = o.user_id
WHERE u.age > 20 AND o.create_time > '2026-01-01'
ORDER BY o.amount DESC;
步骤 1:执行 EXPLAIN 分析
核心结果:
| id | select_type | table | type | possible_keys | key | rows | Extra |
|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | u | ALL | idx_age | NULL | 10000 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | o | ALL | idx_user_id | NULL | 50000 | Using where; Using join buffer |
步骤 2:问题分析
user表 type=ALL(全表扫描):idx_age 索引未生效(age>20 是范围查询,但若数据量大,应走 range);order表 type=ALL(全表扫描):idx_user_id 未生效;- Extra 出现 Using temporary/Using filesort(临时表 + 文件排序);
- JOIN 使用了 join buffer(无索引匹配)。
步骤 3:优化方案
- 给
user表添加索引:idx_age(age)(确保 age>20 走 range); - 给
order表添加联合索引:idx_user_id_create_time_amount(user_id, create_time, amount)(覆盖 JOIN + 过滤 + 排序); - 优化 JOIN 顺序(MySQL 会自动优化,但确保小表驱动大表)。
步骤 4:优化后 SQL 及 EXPLAIN 结果
优化后 SQL 无变化(仅添加索引),EXPLAIN 核心结果:
| id | select_type | table | type | possible_keys | key | rows | Extra |
|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | u | range | idx_age | idx_age | 2000 | Using where |
| 1 | SIMPLE | o | ref | idx_user_id_create_time_amount | idx_user_id_create_time_amount | 5 | Using where; Using index |
优化效果
查询耗时从 5 秒降至 0.1 秒,核心变化:
- type 从 ALL 变为 range/ref(走了索引);
- rows 从 10 万 + 降至 2000+5;
- Extra 消除了临时表和文件排序,且出现 Using index(索引覆盖)。
五、总结
EXPLAIN是 SQL 性能优化的核心工具,重点关注type(访问类型)、key(实际索引)、rows(扫描行数)、Extra(额外信息)四个字段;- 索引生效的核心判断:
key非空且type不为 ALL;最优索引的判断:type为 ref/eq_ref,且rows尽可能小; - 索引失效常见原因:函数运算、% 开头模糊查询、最左匹配失效、数据类型不匹配,优化时需针对性解决;
- 优化目标:消除全表扫描(ALL)、临时表(Using temporary)、文件排序(Using filesort),尽可能使用索引覆盖(Using index)。
通过掌握EXPLAIN的使用方法,我们能从 "凭经验优化" 转向 "数据驱动优化",精准定位 SQL 性能瓶颈,让数据库查询更高效。