执行计划分析
EXPLAIN 是分析 SQL 执行计划的核心工具。理解执行计划各字段含义,可以快速定位全表扫描、索引失效、临时表、文件排序等性能问题。本文系统讲解 EXPLAIN 输出解读方法和索引优化策略。
EXPLAIN 基本用法
EXPLAIN SELECT * FROM user WHERE username = 'zhangsan';
MySQL 8.0.18+ 推荐使用 EXPLAIN ANALYZE,不仅显示执行计划,还展示实际执行时的行数统计和耗时:
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 级别(最优)
EXPLAIN SELECT * FROM user WHERE id = 1;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+
| 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 级别(良好)
EXPLAIN SELECT * FROM user WHERE username = 'zhangsan';
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+
| 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 级别(中等)
EXPLAIN SELECT * FROM user WHERE age BETWEEN 20 AND 30;
+----+-------------+-------+------------+-------+---------------+-----------+---------+------+------+
| 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 级别(最差,必须优化)
EXPLAIN SELECT * FROM user WHERE email = 'test@example.com';
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+
| 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 表示使用索引的字节数,用于判断联合索引实际使用了几个字段:
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 分析:
| 字段 | 类型 | 可为空 | 字节计算 | 累计 |
|---|---|---|---|---|
| username | VARCHAR(50) | NOT NULL | 50 × 3(UTF-8) + 2(长度字节) = 152 | 152 |
| status | TINYINT | NOT NULL | 1 | 153 |
| VARCHAR(100) | 可为 NULL | 100 × 3 + 2 + 1(NULL标记) = 303 | 456 |
EXPLAIN SELECT * FROM user WHERE username = 'zhangsan' AND status = 1;
+----+------+------+------+---------------+-----------+---------+-------------+------+
| 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 优化
-- 未优化:Using filesort
EXPLAIN SELECT * FROM user ORDER BY create_time DESC;
+----+------+------+------+---------------+------+---------+------+-------+----------------+
| id | type | table | key | key_len | ref | rows | Extra |
+----+------+------+------+---------------+------+---------+------+-------+----------------+
| 1 | ALL | user | NULL | NULL | NULL | 10000 | Using filesort |
+----+------+------+------+---------------+------+---------+------+-------+----------------+
创建索引后:
CREATE INDEX idx_create_time ON user(create_time);
EXPLAIN SELECT * FROM user ORDER BY create_time DESC;
+----+-------+------+------+---------------+----------------+---------+------+-------+-------------+
| id | type | table | key | key_len | ref | rows | Extra |
+----+-------+------+------+---------------+----------------+---------+------+-------+-------------+
| 1 | index | user | idx_create_time | 6 | NULL | 10000 | Using index |
+----+-------+------+------+---------------+----------------+---------+------+-------+-------------+
Using filesort 消失,排序直接在索引树中完成。
Using temporary 优化
-- 产生 Using temporary
EXPLAIN SELECT username, COUNT(*) FROM user GROUP BY username;
+----+------+------+------+---------------+------+---------+------+-------+-----------------+
| id | type | table | key | key_len | ref | rows | Extra |
+----+------+------+------+---------------+------+---------+------+-------+-----------------+
| 1 | ALL | user | NULL | NULL | NULL | 10000 | Using temporary |
+----+------+------+------+---------------+------+---------+------+-------+-----------------+
为 GROUP BY 字段创建索引后:
CREATE INDEX idx_username ON user(username);
EXPLAIN SELECT username, COUNT(*) FROM user GROUP BY username;
+----+-------+------+---------------+----------------+---------+------+-------+-----------------+
| id | type | table | key | key_len | ref | rows | Extra |
+----+-------+------+---------------+----------------+---------+------+-------+-----------------+
| 1 | index | user | idx_username | 194 | NULL | 10000 | Using index |
+----+-------+------+---------------+----------------+---------+------+-------+-----------------+
索引失效场景
1. 函数运算导致索引失效
-- 索引失效:在索引列上使用函数
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. 隐式类型转换导致索引失效
-- 索引失效:phone 是 VARCHAR 类型,传入数字字面量导致隐式转换
EXPLAIN SELECT * FROM user WHERE phone = 13800138000;
-- type = ALL
-- 正确:使用字符串
EXPLAIN SELECT * FROM user WHERE phone = '13800138000';
-- type = ref
3. LIKE 左模糊导致索引失效
-- 索引失效:左模糊 '%abc'
EXPLAIN SELECT * FROM user WHERE username LIKE '%zhang';
-- type = ALL
-- 正确:右模糊 'abc%' 可使用索引
EXPLAIN SELECT * FROM user WHERE username LIKE 'zhang%';
-- type = range
4. OR 条件导致索引失效
-- 索引失效: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. 联合索引不满足最左前缀原则
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 导致索引失效
-- 通常不走索引(优化器认为扫描范围过大)
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:
<select id="explainSelect" resultType="map">
EXPLAIN SELECT * FROM user WHERE username = #{username}
</select>
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 |
覆盖索引优化
-- 场景:只需查询 id 和 username
SELECT id, username FROM user WHERE username = 'zhangsan';
-- 若存在索引 idx_username(username),由于 username 在索引中,
-- 但 id(主键)也在索引叶子节点中,所以可直接从索引获取数据
-- Extra = Using index,无需回表
-- 覆盖索引:联合索引包含所有查询字段
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 与数据库索引关系
📝 发现内容有误?点击此处直接编辑