全部学科
NodeJS全栈
nodejs
Python全栈
python
小程序首页
📅 2026-05-20 8 分钟 ✍️ juanwangdev

执行计划分析

EXPLAIN 是分析 SQL 执行计划的核心工具。理解执行计划各字段含义,可以快速定位全表扫描、索引失效、临时表、文件排序等性能问题。本文系统讲解 EXPLAIN 输出解读方法和索引优化策略。

EXPLAIN 基本用法

SQL
EXPLAIN SELECT * FROM user WHERE username = 'zhangsan';

MySQL 8.0.18+ 推荐使用 EXPLAIN ANALYZE,不仅显示执行计划,还展示实际执行时的行数统计和耗时:

SQL
EXPLAIN ANALYZE SELECT * FROM user WHERE username = 'zhangsan';

EXPLAIN 输出字段详解

执行 EXPLAIN 后输出以下列:

列名含义重要性
id查询序列号,标识 SELECT 所属的查询块
select_type查询类型(SIMPLE/PRIMARY/SUBQUERY/DERIVED/UNION)
table当前行涉及的表名
partitions匹配的分区(分区表场景)
type访问类型(ALL/index/range/ref/eq_ref/const)极高
possible_keys可能使用的索引列表
key实际使用的索引极高
key_len使用索引的字节长度
ref与索引比较的列或常量
rows预估扫描行数极高
filtered条件过滤后的行百分比
Extra附加信息(Using filesort/Using temporary)极高

注意:EXPLAIN 中加粗的 type、key、rows、Extra 是性能调优最关注的 4 个字段。

type 访问类型(性能从高到低)

type含义说明性能
system表只有一行记录(系统表)极少见最优
const通过唯一索引一次定位(PRIMARY KEY/UNIQUE)WHERE id = 1极优
eq_ref联表查询中使用主键/唯一索引等值匹配JOIN ON a.id = b.id
ref非唯一索引等值查询WHERE username = 'x'
fulltext全文索引查询MATCH ... AGAINST
ref_or_null类似 ref,额外包含 NULL 值WHERE username IS NULL
index_merge索引合并优化多索引联合使用
range索引范围扫描WHERE age > 20
index全索引扫描(只遍历索引树)SELECT COUNT(*)较差
ALL全表扫描(遍历所有数据行)无索引或索引失效最差

type 示例分析

const 级别(最优)

SQL
EXPLAIN SELECT * FROM user WHERE id = 1;
SQL
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref   | rows |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+
|  1 | SIMPLE      | user  | NULL       | const | PRIMARY       | PRIMARY | 4       | const |    1 |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+

主键等值查询,type = const,rows = 1,最优。

ref 级别(良好)

SQL
EXPLAIN SELECT * FROM user WHERE username = 'zhangsan';
SQL
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+
| id | select_type | table | partitions | type | possible_keys | key      | key_len | ref   | rows |
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+
|  1 | SIMPLE      | user  | NULL       | ref  | idx_username  | idx_username | 194  | const |    1 |
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+

普通索引等值查询,type = ref,rows 预估为 1,性能良好。

range 级别(中等)

SQL
EXPLAIN SELECT * FROM user WHERE age BETWEEN 20 AND 30;
SQL
+----+-------------+-------+------------+-------+---------------+-----------+---------+------+------+
| id | select_type | table | partitions | type  | possible_keys | key       | key_len | ref  | rows |
+----+-------------+-------+------------+-------+---------------+-----------+---------+------+------+
|  1 | SIMPLE      | user  | NULL       | range | idx_age       | idx_age   | 5       | NULL |  523 |
+----+-------------+-------+------------+-------+---------------+-----------+---------+------+------+

索引范围扫描,type = range,rows = 523,预估扫描 523 行。

ALL 级别(最差,必须优化)

SQL
EXPLAIN SELECT * FROM user WHERE email = 'test@example.com';
SQL
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows  |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+
|  1 | SIMPLE      | user  | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 10000 |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+

全表扫描 10,000 行,type = ALL,possible_keys = NULL,说明 email 字段没有索引,必须创建索引。

key_len 索引长度分析

key_len 表示使用索引的字节数,用于判断联合索引实际使用了几个字段:

SQL
CREATE TABLE user (
    id INT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100),
    status TINYINT NOT NULL,
    INDEX idx_union (username, status, email)
);

联合索引 idx_union(username, status, email) 的 key_len 分析:

字段类型可为空字节计算累计
usernameVARCHAR(50)NOT NULL50 × 3(UTF-8) + 2(长度字节) = 152152
statusTINYINTNOT NULL1153
emailVARCHAR(100)可为 NULL100 × 3 + 2 + 1(NULL标记) = 303456
SQL
EXPLAIN SELECT * FROM user WHERE username = 'zhangsan' AND status = 1;
SQL
+----+------+------+------+---------------+-----------+---------+-------------+------+
| id | type | table | key  | key_len       | ref                           | rows |
+----+------+------+------+---------------+-----------+---------+-------------+------+
|  1 | ref  | user | idx_union | 153 | const,const |   50 |
+----+------+------+------+---------------+-----------+---------+-------------+------+

key_len = 153,说明只使用了 username(152) + status(1) 两个字段,email 未被使用。

注意:key_len 计算规则 — 可变长度类型(VARCHAR)需 +2 字节长度标记;可为 NULL 的字段需 +1 字节 NULL 标记;UTF-8 编码 VARCHAR(n) = n × 3 + 2。

Extra 附加信息解读

Extra 字段包含关键的性能提示信息:

Extra 值含义影响优化建议
Using index覆盖索引,仅从索引获取数据,不回表极优无需优化
Using where使用了 WHERE 条件过滤正常检查是否有索引
Using index condition索引条件下推(ICP),索引层过滤良好通常无需优化
Using filesort文件排序,需要额外排序操作为 ORDER BY 字段建索引
Using temporary使用临时表(GROUP BY/DISTINCT)优化 GROUP BY 或加索引
Using join buffer使用连接缓冲区(无索引 JOIN)为 JOIN 条件加索引
Start temporary / End temporary半连接物化优化正常MySQL 内部优化
Using MRR多范围读取优化良好减少回表随机 IO
Using where; Using index覆盖索引 + WHERE 过滤极优理想状态
NULL无特殊优化--

注意:Using filesort 并非真的使用了文件排序,而是表示 MySQL 需要进行额外的排序操作(可能在内存或磁盘)。当排序数据超过 sort_buffer_size 时才会真正使用磁盘文件。

Using filesort 优化

SQL
-- 未优化:Using filesort
EXPLAIN SELECT * FROM user ORDER BY create_time DESC;
SQL
+----+------+------+------+---------------+------+---------+------+-------+----------------+
| id | type | table | key  | key_len       | ref  | rows    | Extra              |
+----+------+------+------+---------------+------+---------+------+-------+----------------+
|  1 | ALL  | user | NULL | NULL          | NULL | 10000   | Using filesort     |
+----+------+------+------+---------------+------+---------+------+-------+----------------+

创建索引后:

SQL
CREATE INDEX idx_create_time ON user(create_time);

EXPLAIN SELECT * FROM user ORDER BY create_time DESC;
SQL
+----+-------+------+------+---------------+----------------+---------+------+-------+-------------+
| id | type  | table | key  | key_len       | ref            | rows    | Extra              |
+----+-------+------+------+---------------+----------------+---------+------+-------+-------------+
|  1 | index | user | idx_create_time | 6 | NULL | 10000   | Using index       |
+----+-------+------+------+---------------+----------------+---------+------+-------+-------------+

Using filesort 消失,排序直接在索引树中完成。

Using temporary 优化

SQL
-- 产生 Using temporary
EXPLAIN SELECT username, COUNT(*) FROM user GROUP BY username;
XML
+----+------+------+------+---------------+------+---------+------+-------+-----------------+
| id | type | table | key  | key_len       | ref  | rows    | Extra               |
+----+------+------+------+---------------+------+---------+------+-------+-----------------+
|  1 | ALL  | user | NULL | NULL          | NULL | 10000   | Using temporary     |
+----+------+------+------+---------------+------+---------+------+-------+-----------------+

为 GROUP BY 字段创建索引后:

Java
CREATE INDEX idx_username ON user(username);

EXPLAIN SELECT username, COUNT(*) FROM user GROUP BY username;
SQL
+----+-------+------+---------------+----------------+---------+------+-------+-----------------+
| id | type  | table | key           | key_len        | ref     | rows  | Extra               |
+----+-------+------+---------------+----------------+---------+------+-------+-----------------+
|  1 | index | user | idx_username  | 194            | NULL    | 10000 | Using index         |
+----+-------+------+---------------+----------------+---------+------+-------+-----------------+

索引失效场景

1. 函数运算导致索引失效

SQL
-- 索引失效:在索引列上使用函数
EXPLAIN SELECT * FROM user WHERE YEAR(create_time) = 2025;
-- type = ALL,全表扫描

-- 优化:改为范围查询
EXPLAIN SELECT * FROM user
WHERE create_time >= '2025-01-01' AND create_time < '2026-01-01';
-- type = range,使用索引

2. 隐式类型转换导致索引失效

text
-- 索引失效:phone 是 VARCHAR 类型,传入数字字面量导致隐式转换
EXPLAIN SELECT * FROM user WHERE phone = 13800138000;
-- type = ALL

-- 正确:使用字符串
EXPLAIN SELECT * FROM user WHERE phone = '13800138000';
-- type = ref

3. LIKE 左模糊导致索引失效

text
-- 索引失效:左模糊 '%abc'
EXPLAIN SELECT * FROM user WHERE username LIKE '%zhang';
-- type = ALL

-- 正确:右模糊 'abc%' 可使用索引
EXPLAIN SELECT * FROM user WHERE username LIKE 'zhang%';
-- type = range

4. OR 条件导致索引失效

text
-- 索引失效:OR 连接的两个字段只有一个有索引
EXPLAIN SELECT * FROM user WHERE username = 'zhangsan' OR email = 'test@test.com';
-- 若 email 无索引,整个条件 type = ALL

-- 优化:为 email 也创建索引
CREATE INDEX idx_email ON user(email);
-- 此时可能触发 index_merge 或仍使用单一索引

5. 联合索引不满足最左前缀原则

text
CREATE INDEX idx_union (username, status, email) ON user(username, status, email);

-- 可以使用索引
EXPLAIN SELECT * FROM user WHERE username = 'zhangsan';
-- type = ref, key = idx_union

-- 可以使用索引
EXPLAIN SELECT * FROM user WHERE username = 'zhangsan' AND status = 1;
-- type = ref, key = idx_union

-- 索引失效:跳过最左列 username
EXPLAIN SELECT * FROM user WHERE status = 1 AND email = 'test@test.com';
-- type = ALL, key = NULL

-- 索引部分使用(仅 username)
EXPLAIN SELECT * FROM user WHERE username = 'zhangsan' AND email = 'test@test.com';
-- type = ref, key = idx_union, key_len = 152(只用了 username)

6. != 和 IS NOT NULL 导致索引失效

text
-- 通常不走索引(优化器认为扫描范围过大)
EXPLAIN SELECT * FROM user WHERE status != 1;
-- type = ALL

-- 优化:改为 IN 明确值
EXPLAIN SELECT * FROM user WHERE status IN (2, 3, 4);
-- type = range

MyBatis 中执行 EXPLAIN

MyBatis Mapper 中可以直接执行 EXPLAIN 分析 SQL:

text
<select id="explainSelect" resultType="map">
    EXPLAIN SELECT * FROM user WHERE username = #{username}
</select>
text
List<Map<String, Object>> plan = mapper.explainSelect("zhangsan");
for (Map<String, Object> row : plan) {
    System.out.println("type: " + row.get("type"));
    System.out.println("key: " + row.get("key"));
    System.out.println("rows: " + row.get("rows"));
    System.out.println("Extra: " + row.get("Extra"));
}

注意:实际开发中更推荐直接在数据库客户端执行 EXPLAIN,无需在 Mapper 中编写。

索引优化策略

选择合适的索引类型

场景推荐索引说明
精确等值查询(id、唯一编号)主键 / 唯一索引const 级别,最优
等值查询(用户名、状态)普通单列索引ref 级别
范围查询(时间区间、价格区间)普通单列索引range 级别
多条件组合查询联合索引遵循最左前缀原则
模糊查询(前缀匹配)普通索引'abc%' 可使用
全文搜索全文索引(FULLTEXT)LIKE '%abc%' 的替代方案
ORDER BY / GROUP BY对应字段索引避免 Using filesort / Using temporary

覆盖索引优化

text
-- 场景:只需查询 id 和 username
SELECT id, username FROM user WHERE username = 'zhangsan';

-- 若存在索引 idx_username(username),由于 username 在索引中,
-- 但 id(主键)也在索引叶子节点中,所以可直接从索引获取数据
-- Extra = Using index,无需回表
text
-- 覆盖索引:联合索引包含所有查询字段
CREATE INDEX idx_cover ON user(username, email, status);

SELECT username, email, status FROM user WHERE username = 'zhangsan';
-- Extra = Using index,索引完全覆盖

要点总结

  • type 访问类型是性能第一指标:const > ref > range > ALL,ALL 必须优化
  • key 为 NULL 说明未使用任何索引,possible_keys 列出候选索引
  • key_len 用于判断联合索引实际使用了几个字段
  • Extra 关注 Using filesort(需排序索引)、Using temporary(需优化 GROUP BY)
  • 索引失效六大场景:函数运算、类型转换、LIKE 左模糊、OR 条件、最左前缀断裂、!= 操作符
  • 联合索引遵循最左前缀原则:(a, b, c) 可匹配 a、ab、abc,不可匹配 b、bc
  • 覆盖索引(Extra = Using index)是最优查询状态,无需回表
  • EXPLAIN ANALYZE 可同时查看预估行数与实际执行耗时,定位更精准
  • MyBatis 本身不改变 SQL 执行计划,EXPLAIN 分析的是底层 SQL 与数据库索引关系

📝 发现内容有误?点击此处直接编辑

← 上一篇 慢查询定位与优化
下一篇 → 批量操作性能优化
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库