EXPLAIN执行计划详解
文章目录
- @[toc]
- 一、EXPLAIN基础
- 1.1 什么是EXPLAIN?
- 1.2 基本用法
- 二、EXPLAIN输出字段详解
- 2.1 标准EXPLAIN输出示例
- 2.2 字段含义总览
- 三、实际案例分析
- 3.1 案例1:主键查询(最优)
- 3.2 案例2:普通索引查询
- 3.3 案例3:范围查询
- 3.4 案例4:全表扫描(最差)
- 3.5 案例5:覆盖索引
- 3.6 案例6:索引失效
- 3.7 案例7:多表JOIN
- 3.8 案例8:子查询
- 3.9 案例9:UNION查询
- 四、type类型详解
- 4.1 type类型性能排序
- 4.2 system - 表中只有一行
- 4.3 const - 主键或唯一索引等值查询
- 4.4 eq_ref - JOIN时使用主键或唯一索引
- 4.5 ref - 非唯一索引等值查询
- 4.6 ref_or_null - ref + NULL值查询
- 4.7 index_merge - 合并多个索引
- 4.8 range - 范围扫描
- 4.9 index - 全索引扫描
- 4.10 ALL - 全表扫描
- 五、Extra信息详解
- 5.1 Using index - 覆盖索引(最优)
- 5.2 Using where - WHERE过滤
- 5.3 Using index condition - 索引下推(ICP)
- 5.4 Using filesort - 文件排序(需优化)
- 5.5 Using temporary - 使用临时表(需优化)
- 5.6 Using join buffer - JOIN缓冲
- 5.7 Impossible WHERE - WHERE条件永远为假
- 5.8 Select tables optimized away
- 5.9 Using union/intersect/sort_union
- 六、EXPLAIN的变体
- 6.1 EXPLAIN ANALYZE(MySQL 8.0.18+)
- 6.2 EXPLAIN FORMAT=JSON
- 6.3 EXPLAIN FORMAT=TREE(MySQL 8.0.16+)
- 七、优化实战案例
- 7.1 案例1:慢查询优化
- 7.2 案例2:JOIN优化
- 7.3 案例3:子查询优化
- 八、常见问题诊断
- 8.1 如何判断SQL是否需要优化?
- 8.2 为什么possible_keys有值,但key是NULL?
- 8.3 如何理解key_len?
- 8.4 rows和filtered的关系
- 8.5 为什么EXPLAIN显示rows=1000,但实际扫描了更多?
- 总结
- 关键指标速查表
- EXPLAIN优化流程
- 最佳实践
文章目录
- @[toc]
- 一、EXPLAIN基础
- 1.1 什么是EXPLAIN?
- 1.2 基本用法
- 二、EXPLAIN输出字段详解
- 2.1 标准EXPLAIN输出示例
- 2.2 字段含义总览
- 三、实际案例分析
- 3.1 案例1:主键查询(最优)
- 3.2 案例2:普通索引查询
- 3.3 案例3:范围查询
- 3.4 案例4:全表扫描(最差)
- 3.5 案例5:覆盖索引
- 3.6 案例6:索引失效
- 3.7 案例7:多表JOIN
- 3.8 案例8:子查询
- 3.9 案例9:UNION查询
- 四、type类型详解
- 4.1 type类型性能排序
- 4.2 system - 表中只有一行
- 4.3 const - 主键或唯一索引等值查询
- 4.4 eq_ref - JOIN时使用主键或唯一索引
- 4.5 ref - 非唯一索引等值查询
- 4.6 ref_or_null - ref + NULL值查询
- 4.7 index_merge - 合并多个索引
- 4.8 range - 范围扫描
- 4.9 index - 全索引扫描
- 4.10 ALL - 全表扫描
- 五、Extra信息详解
- 5.1 Using index - 覆盖索引(最优)
- 5.2 Using where - WHERE过滤
- 5.3 Using index condition - 索引下推(ICP)
- 5.4 Using filesort - 文件排序(需优化)
- 5.5 Using temporary - 使用临时表(需优化)
- 5.6 Using join buffer - JOIN缓冲
- 5.7 Impossible WHERE - WHERE条件永远为假
- 5.8 Select tables optimized away
- 5.9 Using union/intersect/sort_union
- 六、EXPLAIN的变体
- 6.1 EXPLAIN ANALYZE(MySQL 8.0.18+)
- 6.2 EXPLAIN FORMAT=JSON
- 6.3 EXPLAIN FORMAT=TREE(MySQL 8.0.16+)
- 七、优化实战案例
- 7.1 案例1:慢查询优化
- 7.2 案例2:JOIN优化
- 7.3 案例3:子查询优化
- 八、常见问题诊断
- 8.1 如何判断SQL是否需要优化?
- 8.2 为什么possible_keys有值,但key是NULL?
- 8.3 如何理解key_len?
- 8.4 rows和filtered的关系
- 8.5 为什么EXPLAIN显示rows=1000,但实际扫描了更多?
- 总结
- 关键指标速查表
- EXPLAIN优化流程
- 最佳实践
一、EXPLAIN基础
1.1 什么是EXPLAIN?
EXPLAIN 是MySQL提供的查询分析工具,用于查看SQL语句的执行计划,帮助我们了解:
- MySQL如何执行这条SQL
- 是否使用了索引
- 扫描了多少行数据
- 是否需要临时表或排序
1.2 基本用法
-- 基本语法
EXPLAIN SELECT * FROM users WHERE id = 1;-- 查看详细信息(MySQL 8.0.18+)
EXPLAIN ANALYZE SELECT * FROM users WHERE id = 1;-- JSON格式(更详细)
EXPLAIN FORMAT=JSON SELECT * FROM users WHERE id = 1;-- 传统格式(MySQL 5.7+)
EXPLAIN FORMAT=TRADITIONAL SELECT * FROM users WHERE id = 1;-- 树形格式(MySQL 8.0.16+)
EXPLAIN FORMAT=TREE SELECT * FROM users WHERE id = 1;
二、EXPLAIN输出字段详解
2.1 标准EXPLAIN输出示例
EXPLAIN SELECT * FROM users WHERE id = 10;
输出结果:
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| 1 | SIMPLE | users | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
2.2 字段含义总览
| 字段 | 含义 | 重要性 |
|---|---|---|
| id | 查询序列号 | ⭐⭐⭐ |
| select_type | 查询类型 | ⭐⭐⭐ |
| table | 访问的表 | ⭐⭐⭐⭐ |
| partitions | 匹配的分区 | ⭐⭐ |
| type | 访问类型 | ⭐⭐⭐⭐⭐ 最重要 |
| possible_keys | 可能使用的索引 | ⭐⭐⭐ |
| key | 实际使用的索引 | ⭐⭐⭐⭐⭐ 最重要 |
| key_len | 索引使用的字节数 | ⭐⭐⭐⭐ |
| ref | 索引的哪一列被使用 | ⭐⭐⭐ |
| rows | 预估扫描行数 | ⭐⭐⭐⭐⭐ 最重要 |
| filtered | 过滤后的行百分比 | ⭐⭐⭐ |
| Extra | 额外信息 | ⭐⭐⭐⭐⭐ 最重要 |
三、实际案例分析
3.1 案例1:主键查询(最优)
-- 测试表
CREATE TABLE users (id INT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(50),age INT,email VARCHAR(100)
);-- 查询
EXPLAIN SELECT * FROM users WHERE id = 10;
执行结果:
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | NULL |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
字段解读:
id = 1:第一个(也是唯一的)SELECTselect_type = SIMPLE:简单查询(非子查询、非UNION)table = users:访问users表type = const:最优! 通过主键或唯一索引查询单行possible_keys = PRIMARY:可能使用主键索引key = PRIMARY:实际使用了主键索引key_len = 4:索引长度4字节(INT类型)ref = const:使用常量比较rows = 1:只需扫描1行Extra = NULL:无额外信息
性能评价:⭐⭐⭐⭐⭐ 完美!
3.2 案例2:普通索引查询
-- 创建索引
CREATE INDEX idx_name ON users(name);-- 查询
EXPLAIN SELECT * FROM users WHERE name = '张三';
执行结果:
+----+-------------+-------+------+---------------+----------+---------+-------+------+-----------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+----------+---------+-------+------+-----------+
| 1 | SIMPLE | users | ref | idx_name | idx_name | 203 | const | 5 | NULL |
+----+-------------+-------+------+---------------+----------+---------+-------+------+-----------+
字段解读:
type = ref:非唯一索引等值查询key = idx_name:使用了name索引key_len = 203:VARCHAR(50) × 4字节(utf8mb4) + 2字节(长度) + 1字节(NULL) = 203rows = 5:预计扫描5行(可能有5个叫"张三"的用户)
性能评价:⭐⭐⭐⭐ 很好!
3.3 案例3:范围查询
-- 创建索引
CREATE INDEX idx_age ON users(age);-- 查询
EXPLAIN SELECT * FROM users WHERE age BETWEEN 20 AND 30;
执行结果:
+----+-------------+-------+-------+---------------+---------+---------+------+------+-----------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-----------------------+
| 1 | SIMPLE | users | range | idx_age | idx_age | 5 | NULL | 500 | Using index condition |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-----------------------+
字段解读:
type = range:范围扫描key = idx_age:使用了age索引key_len = 5:INT(4字节) + NULL标志(1字节)ref = NULL:范围查询无refrows = 500:预计扫描500行Extra = Using index condition:使用了索引下推优化(ICP)
性能评价:⭐⭐⭐ 较好
3.4 案例4:全表扫描(最差)
-- 没有索引的列
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
执行结果:
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| 1 | SIMPLE | users | ALL | NULL | NULL | NULL | NULL | 100000 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
字段解读:
type = ALL:最差! 全表扫描possible_keys = NULL:没有可用索引key = NULL:未使用索引rows = 100000:需要扫描全部10万行Extra = Using where:使用WHERE过滤,但在存储引擎层之后
性能评价:⭐ 很差!需要优化
优化方案:
-- 添加索引
CREATE INDEX idx_email ON users(email);-- 再次查询
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
优化后结果:
+----+-------------+-------+------+---------------+-----------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+-----------+---------+-------+------+-------+
| 1 | SIMPLE | users | ref | idx_email | idx_email | 403 | const | 1 | NULL |
+----+-------------+-------+------+---------------+-----------+---------+-------+------+-------+
性能提升:从扫描10万行 → 1行,提升10万倍!
3.5 案例5:覆盖索引
-- 创建联合索引
CREATE INDEX idx_name_age ON users(name, age);-- 查询(只查询索引中的列)
EXPLAIN SELECT name, age FROM users WHERE name = '张三';
执行结果:
+----+-------------+-------+------+---------------+--------------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------------+---------+-------+------+-------------+
| 1 | SIMPLE | users | ref | idx_name_age | idx_name_age | 203 | const | 5 | Using index |
+----+-------------+-------+------+---------------+--------------+---------+-------+------+-------------+
字段解读:
type = ref:索引查询key = idx_name_age:使用联合索引Extra = Using index:覆盖索引! 无需回表,性能最优
对比:需要回表的查询:
-- 查询索引外的列
EXPLAIN SELECT * FROM users WHERE name = '张三';
结果:
+----+-------------+-------+------+---------------+--------------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------------+---------+-------+------+-------+
| 1 | SIMPLE | users | ref | idx_name_age | idx_name_age | 203 | const | 5 | NULL |
+----+-------------+-------+------+---------------+--------------+---------+-------+------+-------+
Extra = NULL:需要回表查询email等其他列
3.6 案例6:索引失效
-- 索引列使用函数
EXPLAIN SELECT * FROM users WHERE YEAR(create_time) = 2024;
执行结果:
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| 1 | SIMPLE | users | ALL | NULL | NULL | NULL | NULL | 100000 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
问题:索引列使用了函数,导致索引失效,全表扫描!
优化方案:
-- 改写SQL
EXPLAIN SELECT * FROM users
WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';
优化后:
+----+-------------+-------+-------+------------------+------------------+---------+------+------+-----------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+------------------+------------------+---------+------+------+-----------------------+
| 1 | SIMPLE | users | range | idx_create_time | idx_create_time | 5 | NULL | 5000 | Using index condition |
+----+-------------+-------+-------+------------------+------------------+---------+------+------+-----------------------+
3.7 案例7:多表JOIN
-- 查询用户及其订单
EXPLAIN SELECT u.name, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE u.age > 20;
执行结果:
+----+-------------+-------+-------+------------------+---------+---------+-----------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+------------------+---------+---------+-----------+------+-------------+
| 1 | SIMPLE | u | range | idx_age | idx_age | 5 | NULL | 5000 | Using where |
| 1 | SIMPLE | o | ref | idx_user_id | idx_user_id | 4 | u.id | 2 | NULL |
+----+-------------+-------+-------+------------------+---------+---------+-----------+------+-------------+
字段解读:
- 两行结果:表示两个表的连接
- 第1行(驱动表):
table = u:users表作为驱动表type = range:使用age索引范围扫描rows = 5000:预计扫描5000行
- 第2行(被驱动表):
table = o:orders表type = ref:通过user_id索引查找ref = u.id:使用users表的id字段关联rows = 2:每个用户平均2个订单
执行流程:
- 先扫描users表(5000行,age>20)
- 对每个用户,通过索引在orders表中查找订单(每次2行)
- 总扫描:5000 + (5000 × 2) = 15000行
3.8 案例8:子查询
-- 标量子查询
EXPLAIN SELECT u.name,(SELECT COUNT(*) FROM orders WHERE user_id = u.id) AS order_count
FROM users u
WHERE u.age > 20;
执行结果:
+----+--------------------+--------+-------+------------------+--------------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+--------------------+--------+-------+------------------+--------------+---------+-------+------+-------------+
| 1 | PRIMARY | u | range | idx_age | idx_age | 5 | NULL | 5000 | Using where |
| 2 | DEPENDENT SUBQUERY | orders | ref | idx_user_id | idx_user_id | 4 | u.id | 2 | Using index |
+----+--------------------+--------+-------+------------------+--------------+---------+-------+------+-------------+
字段解读:
id = 1, select_type = PRIMARY:主查询id = 2, select_type = DEPENDENT SUBQUERY:依赖外部查询的子查询- 子查询对每个外部行执行一次(5000次)
性能问题:DEPENDENT SUBQUERY性能较差(执行多次)
优化方案:改为JOIN
EXPLAIN SELECT u.name,COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.age > 20
GROUP BY u.id, u.name;
3.9 案例9:UNION查询
EXPLAIN
SELECT name FROM users WHERE age < 20
UNION
SELECT name FROM users WHERE age > 60;
执行结果:
+----+--------------+------------+-------+------------------+---------+---------+------+------+-----------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+--------------+------------+-------+------------------+---------+---------+------+------+-----------------+
| 1 | PRIMARY | users | range | idx_age | idx_age | 5 | NULL | 1000 | Using where |
| 2 | UNION | users | range | idx_age | idx_age | 5 | NULL | 500 | Using where |
| NULL | UNION RESULT | <union1,2> | ALL | NULL | NULL | NULL | NULL | NULL | Using temporary |
+----+--------------+------------+-------+------------------+---------+---------+------+------+-----------------+
字段解读:
id = 1, select_type = PRIMARY:第一个SELECTid = 2, select_type = UNION:UNION的第二个SELECTtable = <union1,2>:UNION的临时表select_type = UNION RESULT:UNION的结果集Extra = Using temporary:使用了临时表(用于去重)
优化建议:
-- 如果不需要去重,使用UNION ALL(性能更好)
EXPLAIN
SELECT name FROM users WHERE age < 20
UNION ALL
SELECT name FROM users WHERE age > 60;
优化后:
+----+-------------+-------+-------+------------------+---------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+------------------+---------+---------+------+------+-------------+
| 1 | PRIMARY | users | range | idx_age | idx_age | 5 | NULL | 1000 | Using where |
| 2 | UNION | users | range | idx_age | idx_age | 5 | NULL | 500 | Using where |
+----+-------------+-------+-------+------------------+---------+---------+------+------+-------------+
- 无
UNION RESULT行 - 无
Using temporary - 性能提升明显!
四、type类型详解
type字段是EXPLAIN中最重要的字段,表示MySQL访问数据的方式。
4.1 type类型性能排序
性能从好到差:
system > const > eq_ref > ref > fulltext > ref_or_null >
index_merge > unique_subquery > index_subquery > range >
index > ALL常用类型:
⭐⭐⭐⭐⭐ system/const - 最优
⭐⭐⭐⭐⭐ eq_ref - 最优
⭐⭐⭐⭐ ref - 很好
⭐⭐⭐ range - 较好
⭐⭐ index - 需优化
⭐ ALL - 最差(需优化)
4.2 system - 表中只有一行
-- 系统表或只有一行的表
EXPLAIN SELECT * FROM (SELECT 1) t;
结果:
+----+-------------+-------+--------+
| id | select_type | table | type |
+----+-------------+-------+--------+
| 1 | SIMPLE | t | system |
+----+-------------+-------+--------+
性能:⭐⭐⭐⭐⭐ 最优
4.3 const - 主键或唯一索引等值查询
-- 通过主键查询
EXPLAIN SELECT * FROM users WHERE id = 1;-- 通过唯一索引查询
EXPLAIN SELECT * FROM users WHERE email = 'unique@example.com';
结果:
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | NULL |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
特点:
- 最多返回一行数据
- 使用主键或唯一索引
- 性能极佳
性能:⭐⭐⭐⭐⭐ 最优
4.4 eq_ref - JOIN时使用主键或唯一索引
-- 每个users记录在orders表中最多匹配一行(user_id是主键或唯一索引)
EXPLAIN SELECT *
FROM orders o
INNER JOIN users u ON o.user_id = u.id;
结果:
+----+-------------+-------+--------+---------------+---------+---------+-----------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+--------+---------------+---------+---------+-----------+------+-------+
| 1 | SIMPLE | o | ALL | idx_user_id | NULL | NULL | NULL | 1000 | NULL |
| 1 | SIMPLE | u | eq_ref | PRIMARY | PRIMARY | 4 | o.user_id | 1 | NULL |
+----+-------------+-------+--------+---------------+---------+---------+-----------+------+-------+
特点:
- 用于JOIN场景
- 被驱动表通过主键或唯一索引关联
- 每次最多匹配一行
性能:⭐⭐⭐⭐⭐ 最优
4.5 ref - 非唯一索引等值查询
-- name是普通索引(非唯一)
EXPLAIN SELECT * FROM users WHERE name = '张三';
结果:
+----+-------------+-------+------+---------------+----------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+----------+---------+-------+------+-------+
| 1 | SIMPLE | users | ref | idx_name | idx_name | 203 | const | 5 | NULL |
+----+-------------+-------+------+---------------+----------+---------+-------+------+-------+
特点:
- 使用非唯一索引
- 可能返回多行
- 性能很好
性能:⭐⭐⭐⭐ 很好
4.6 ref_or_null - ref + NULL值查询
-- 查询name='张三'或name IS NULL
EXPLAIN SELECT * FROM users WHERE name = '张三' OR name IS NULL;
结果:
+----+-------------+-------+-------------+---------------+----------+---------+-------+------+-----------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------------+---------------+----------+---------+-------+------+-----------------------+
| 1 | SIMPLE | users | ref_or_null | idx_name | idx_name | 203 | const | 6 | Using index condition |
+----+-------------+-------+-------------+---------------+----------+---------+-------+------+-----------------------+
性能:⭐⭐⭐⭐ 很好
4.7 index_merge - 合并多个索引
-- 使用OR连接不同索引列
EXPLAIN SELECT * FROM users WHERE name = '张三' OR age = 20;
结果:
+----+-------------+-------+-------------+-------------------+-------------------+---------+------+------+-------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------------+-------------------+-------------------+---------+------+------+-------------------------------------------+
| 1 | SIMPLE | users | index_merge | idx_name,idx_age | idx_name,idx_age | 203,5 | NULL | 10 | Using union(idx_name,idx_age); Using where |
+----+-------------+-------+-------------+-------------------+-------------------+---------+------+------+-------------------------------------------+
特点:
- 使用多个索引,然后合并结果
- 常见于OR查询
性能:⭐⭐⭐ 较好(但不如单个索引)
4.8 range - 范围扫描
-- 范围查询
EXPLAIN SELECT * FROM users WHERE age BETWEEN 20 AND 30;
EXPLAIN SELECT * FROM users WHERE age > 20;
EXPLAIN SELECT * FROM users WHERE age IN (20, 25, 30);
EXPLAIN SELECT * FROM users WHERE name LIKE '张%';
结果:
+----+-------------+-------+-------+---------------+---------+---------+------+------+-----------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-----------------------+
| 1 | SIMPLE | users | range | idx_age | idx_age | 5 | NULL | 500 | Using index condition |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-----------------------+
适用场景:
>、<、>=、<=BETWEEN ... AND ...IN(...)LIKE 'prefix%'
性能:⭐⭐⭐ 较好
4.9 index - 全索引扫描
-- 扫描整个索引树(比全表扫描好,但仍不理想)
EXPLAIN SELECT name FROM users;
结果:
+----+-------------+-------+-------+---------------+----------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+----------+---------+------+--------+-------------+
| 1 | SIMPLE | users | index | NULL | idx_name | 203 | NULL | 100000 | Using index |
+----+-------------+-------+-------+---------------+----------+---------+------+--------+-------------+
特点:
- 扫描整个索引树
- 比全表扫描快(索引文件通常更小)
- 但仍需优化
性能:⭐⭐ 需优化
4.10 ALL - 全表扫描
-- 没有可用索引,全表扫描
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
结果:
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| 1 | SIMPLE | users | ALL | NULL | NULL | NULL | NULL | 100000 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
特点:
- 扫描全部数据行
- 性能最差
- 必须优化!
性能:⭐ 最差(需优化)
五、Extra信息详解
Extra字段提供了查询的额外信息,是优化的重要参考。
5.1 Using index - 覆盖索引(最优)
CREATE INDEX idx_name_age ON users(name, age);
EXPLAIN SELECT name, age FROM users WHERE name = '张三';
结果:
Extra: Using index
含义:
- ✅ 查询的列都在索引中
- ✅ 无需回表查询
- ✅ 性能最优
5.2 Using where - WHERE过滤
EXPLAIN SELECT * FROM users WHERE age > 20;
结果:
Extra: Using where
含义:
- 使用WHERE条件过滤
- 过滤发生在Server层(而非存储引擎层)
- 正常情况,不一定需要优化
5.3 Using index condition - 索引下推(ICP)
CREATE INDEX idx_name_age ON users(name, age);
EXPLAIN SELECT * FROM users WHERE name LIKE '张%' AND age > 20;
结果:
Extra: Using index condition
含义:
- ✅ MySQL 5.6+ 的优化特性
- ✅ 在索引遍历时就进行过滤(而非回表后过滤)
- ✅ 减少回表次数,提升性能
对比:
无ICP:
1. 通过name索引找到所有'张%'的记录
2. 回表获取完整数据
3. 过滤age > 20有ICP:
1. 通过name索引找到所有'张%'的记录
2. 在索引中直接过滤age > 20(减少回表)
3. 回表获取过滤后的数据
5.4 Using filesort - 文件排序(需优化)
-- age列没有索引
EXPLAIN SELECT * FROM users ORDER BY age;
结果:
Extra: Using filesort
含义:
- ❌ MySQL需要额外的排序操作
- ❌ 无法利用索引排序
- ❌ 性能较差,需要优化
优化方案:
-- 为排序列创建索引
CREATE INDEX idx_age ON users(age);-- 再次查询
EXPLAIN SELECT * FROM users ORDER BY age;
优化后:
Extra: Using index
5.5 Using temporary - 使用临时表(需优化)
-- GROUP BY非索引列
EXPLAIN SELECT age, COUNT(*) FROM users GROUP BY email;
结果:
Extra: Using temporary; Using filesort
含义:
- ❌ MySQL创建临时表来处理查询
- ❌ 常见于GROUP BY、DISTINCT、UNION
- ❌ 性能较差,需要优化
优化方案:
-- 为GROUP BY列创建索引
CREATE INDEX idx_email ON users(email);
5.6 Using join buffer - JOIN缓冲
-- JOIN列没有索引
EXPLAIN SELECT *
FROM users u
INNER JOIN orders o ON u.email = o.user_email;
结果:
Extra: Using join buffer (Block Nested Loop)
含义:
- ⚠️ JOIN列没有索引
- ⚠️ MySQL使用join_buffer缓存
- ⚠️ 性能不佳,建议添加索引
优化方案:
CREATE INDEX idx_user_email ON orders(user_email);
5.7 Impossible WHERE - WHERE条件永远为假
EXPLAIN SELECT * FROM users WHERE 1 = 0;
结果:
Extra: Impossible WHERE
含义:
- WHERE条件永远不成立
- MySQL直接返回空结果,不执行查询
5.8 Select tables optimized away
EXPLAIN SELECT MIN(id) FROM users;
结果:
Extra: Select tables optimized away
含义:
- ✅ 优化器直接从索引中获取结果
- ✅ 无需扫描表
- ✅ 性能极佳
5.9 Using union/intersect/sort_union
-- index_merge时出现
EXPLAIN SELECT * FROM users WHERE name = '张三' OR age = 20;
结果:
Extra: Using union(idx_name, idx_age); Using where
含义:
- 使用多个索引
- union:合并索引结果(OR)
- intersect:交集(AND)
- sort_union:先排序再合并
六、EXPLAIN的变体
6.1 EXPLAIN ANALYZE(MySQL 8.0.18+)
EXPLAIN ANALYZE SELECT * FROM users WHERE age > 20;
输出:
-> Filter: (users.age > 20) (cost=10.25 rows=100) (actual time=0.045..0.127 rows=95 loops=1)-> Table scan on users (cost=10.25 rows=100) (actual time=0.037..0.101 rows=100 loops=1)
优势:
- ✅ 显示实际执行时间(actual time)
- ✅ 显示实际返回行数(actual rows)
- ✅ 显示成本估算(cost)
- ✅ 更准确的性能分析
6.2 EXPLAIN FORMAT=JSON
EXPLAIN FORMAT=JSON SELECT * FROM users WHERE id = 1\G
输出:
{"query_block": {"select_id": 1,"cost_info": {"query_cost": "1.00"},"table": {"table_name": "users","access_type": "const","possible_keys": ["PRIMARY"],"key": "PRIMARY","used_key_parts": ["id"],"key_length": "4","ref": ["const"],"rows_examined_per_scan": 1,"rows_produced_per_join": 1,"filtered": "100.00","cost_info": {"read_cost": "0.00","eval_cost": "0.10","prefix_cost": "0.00","data_read_per_join": "1K"},"used_columns": ["id", "name", "age", "email"]}}
}
优势:
- 提供更详细的成本信息
- 机器可读格式
- 便于程序化分析
6.3 EXPLAIN FORMAT=TREE(MySQL 8.0.16+)
EXPLAIN FORMAT=TREE SELECT * FROM users WHERE age > 20\G
输出:
-> Filter: (users.age > 20) (cost=10.25 rows=100)-> Table scan on users (cost=10.25 rows=100)
优势:
- 树形结构,更直观
- 显示执行流程
- 显示成本估算
七、优化实战案例
7.1 案例1:慢查询优化
问题SQL:
SELECT * FROM orders
WHERE DATE(create_time) = '2024-01-01'
ORDER BY amount DESC;
EXPLAIN分析:
EXPLAIN SELECT * FROM orders
WHERE DATE(create_time) = '2024-01-01'
ORDER BY amount DESC;
结果:
+----+-------------+--------+------+---------------+------+---------+------+--------+-----------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+------+---------------+------+---------+------+--------+-----------------------------+
| 1 | SIMPLE | orders | ALL | NULL | NULL | NULL | NULL | 100000 | Using where; Using filesort |
+----+-------------+--------+------+---------------+------+---------+------+--------+-----------------------------+
问题分析:
- ❌
type = ALL:全表扫描 - ❌
key = NULL:未使用索引(函数导致索引失效) - ❌
rows = 100000:扫描全部10万行 - ❌
Extra = Using filesort:需要额外排序
优化方案:
-- 1. 改写SQL(去掉函数)
SELECT * FROM orders
WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02'
ORDER BY amount DESC;-- 2. 创建联合索引
CREATE INDEX idx_create_amount ON orders(create_time, amount);
优化后EXPLAIN:
+----+-------------+--------+-------+-------------------+-------------------+---------+------+------+-----------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+-------+-------------------+-------------------+---------+------+------+-----------------------+
| 1 | SIMPLE | orders | range | idx_create_amount | idx_create_amount | 5 | NULL | 1000 | Using index condition |
+----+-------------+--------+-------+-------------------+-------------------+---------+------+------+-----------------------+
优化效果:
- ✅
type = range:索引范围扫描 - ✅
key = idx_create_amount:使用索引 - ✅
rows = 1000:只扫描1000行(从10万降到1000) - ✅ 无
Using filesort:利用索引排序 - 性能提升:100倍
7.2 案例2:JOIN优化
问题SQL:
SELECT u.name, o.amount
FROM users u
INNER JOIN orders o ON CONCAT(u.id, '') = o.user_id
WHERE u.age > 20;
EXPLAIN分析:
+----+-------------+-------+------+---------------+---------+---------+------+--------+--------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+---------+---------+------+--------+--------------------------------------------+
| 1 | SIMPLE | u | ALL | idx_age | NULL | NULL | NULL | 100000 | Using where |
| 1 | SIMPLE | o | ALL | NULL | NULL | NULL | NULL | 100000 | Using where; Using join buffer (hash join) |
+----+-------------+-------+------+---------------+---------+---------+------+--------+--------------------------------------------+
问题分析:内连接(INNER JOIN)、左连接(LEFT JOIN)、右连接(RIGHT JOIN)和全连接(FULL JOIN)
- ❌ JOIN条件使用了函数
CONCAT() - ❌ 两个表都是全表扫描
- ❌ 使用join buffer(说明JOIN列没有索引)
- ❌ 扫描行数:100000 × 100000 = 100亿次比较!
优化方案:
-- 去掉JOIN条件中的函数
SELECT u.name, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE u.age > 20;-- 确保索引存在
CREATE INDEX idx_age ON users(age);
CREATE INDEX idx_user_id ON orders(user_id);
优化后EXPLAIN:
+----+-------------+-------+-------+------------------+-------------+---------+-----------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+------------------+-------------+---------+-----------+------+-------------+
| 1 | SIMPLE | u | range | idx_age | idx_age | 5 | NULL | 5000 | Using where |
| 1 | SIMPLE | o | ref | idx_user_id | idx_user_id | 4 | u.id | 2 | NULL |
+----+-------------+-------+-------+------------------+-------------+---------+-----------+------+-------------+
优化效果:
- ✅
type = range/ref:使用索引 - ✅ 扫描行数:5000 + (5000 × 2) = 15000行
- 性能提升:从100亿次降到15000次,提升66万倍!
7.3 案例3:子查询优化
问题SQL:
SELECT *
FROM users
WHERE id IN (SELECT user_id FROM orders WHERE amount > 100);
EXPLAIN分析:
+----+--------------------+--------+------+---------------+-------------+---------+-------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+--------------------+--------+------+---------------+-------------+---------+-------+--------+-------------+
| 1 | PRIMARY | users | ALL | NULL | NULL | NULL | NULL | 100000 | Using where |
| 2 | DEPENDENT SUBQUERY | orders | ref | idx_user_id | idx_user_id | 4 | func | 10 | Using where |
+----+--------------------+--------+------+---------------+-------------+---------+-------+--------+-------------+
问题分析:
- ❌
DEPENDENT SUBQUERY:子查询对每个外部行执行一次 - ❌ users表全表扫描
- ❌ 子查询执行10万次
优化方案:改为JOIN
SELECT DISTINCT u.*
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.amount > 100;
优化后EXPLAIN:
+----+-------------+-------+-------+------------------+-------------+---------+-----------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+------------------+-------------+---------+-----------+------+-------------+
| 1 | SIMPLE | o | range | idx_user_id | idx_amount | 5 | NULL | 1000 | Using where |
| 1 | SIMPLE | u | eq_ref| PRIMARY | PRIMARY | 4 | o.user_id | 1 | NULL |
+----+-------------+-------+-------+------------------+-------------+---------+-----------+------+-------------+
优化效果:
- ✅ 无子查询
- ✅ 使用索引
- 性能提升:100倍
八、常见问题诊断
8.1 如何判断SQL是否需要优化?
看这4个关键指标:
-
type字段:
- ❌
ALL、index→ 需要优化 - ✅
range、ref、eq_ref、const→ 良好
- ❌
-
key字段:
- ❌
NULL→ 未使用索引,需要优化 - ✅ 显示索引名 → 使用了索引
- ❌
-
rows字段:
- ❌ 数值很大(如10万+)→ 需要优化
- ✅ 数值较小 → 良好
-
Extra字段:
- ❌
Using filesort→ 需要优化排序 - ❌
Using temporary→ 需要优化(临时表) - ❌
Using join buffer→ 需要为JOIN列添加索引 - ✅
Using index→ 覆盖索引,最优
- ❌
8.2 为什么possible_keys有值,但key是NULL?
原因:优化器认为不走索引更快(如返回大部分数据)
示例:
-- 假设users表有100万行,其中80万人age>18
CREATE INDEX idx_age ON users(age);EXPLAIN SELECT * FROM users WHERE age > 18;
结果:
possible_keys: idx_age
key: NULL
type: ALL
原因:返回80%的数据,全表扫描比索引扫描更快(避免大量回表)
解决方案:
- 缩小查询范围
- 使用覆盖索引
- 或接受全表扫描(可能确实是最优选择)
8.3 如何理解key_len?
key_len 表示索引使用的字节数,可以判断联合索引使用了几列。
计算规则:
INT: 4字节
BIGINT: 8字节
DATETIME: 5字节(MySQL 5.6.4+)
VARCHAR(n): n × 字符集字节数 + 2(长度) + 1(NULL标志)字符集字节数:
- latin1: 1字节
- gbk: 2字节
- utf8: 3字节
- utf8mb4: 4字节
示例:
-- 联合索引
CREATE INDEX idx_name_age_email ON users(name VARCHAR(50), -- utf8mb4age INT,email VARCHAR(100) -- utf8mb4
);-- 查询1
EXPLAIN SELECT * FROM users WHERE name = '张三';
-- key_len = 203
-- 计算:50 × 4 + 2 + 1 = 203
-- 结论:只使用了name列-- 查询2
EXPLAIN SELECT * FROM users WHERE name = '张三' AND age = 20;
-- key_len = 208
-- 计算:203(name) + 4(INT) + 1(NULL) = 208
-- 结论:使用了name和age两列-- 查询3
EXPLAIN SELECT * FROM users WHERE name = '张三' AND age = 20 AND email = 'test@example.com';
-- key_len = 611
-- 计算:203(name) + 5(age) + 403(email: 100×4+2+1) = 611
-- 结论:使用了全部三列
8.4 rows和filtered的关系
rows:预估扫描的行数
filtered:过滤后剩余的百分比
实际返回行数 = rows × filtered / 100
示例:
EXPLAIN SELECT * FROM users WHERE age > 20 AND name = '张三';
结果:
rows: 5000
filtered: 10.00
解读:
- 扫描5000行(age > 20)
- 其中10%满足name=‘张三’
- 实际返回:5000 × 10% = 500行
8.5 为什么EXPLAIN显示rows=1000,但实际扫描了更多?
原因:EXPLAIN显示的是预估值,基于统计信息。
解决方案:
-- 1. 更新统计信息
ANALYZE TABLE users;-- 2. 使用EXPLAIN ANALYZE查看实际值(MySQL 8.0.18+)
EXPLAIN ANALYZE SELECT * FROM users WHERE age > 20;
总结
关键指标速查表
| 指标 | 优 | 良 | 中 | 差 | 需优化 |
|---|---|---|---|---|---|
| type | const, system, eq_ref | ref | range | index | ALL |
| key | 显示索引名 | 显示索引名 | 显示索引名 | 显示索引名 | NULL |
| rows | < 100 | < 1000 | < 10000 | < 100000 | > 100000 |
| Extra | Using index | Using where | Using index condition | Using filesort | Using temporary |
EXPLAIN优化流程
1. 执行EXPLAIN↓
2. 检查type- ALL/index → 添加索引↓
3. 检查key- NULL → 索引失效,检查原因↓
4. 检查rows- 过大 → 优化WHERE条件或索引↓
5. 检查Extra- Using filesort → 添加排序索引- Using temporary → 优化GROUP BY- Using join buffer → 为JOIN列添加索引↓
6. 再次EXPLAIN验证优化效果
最佳实践
- 所有生产SQL都应该EXPLAIN分析
- 关注type、key、rows、Extra四个关键字段
- 定期更新统计信息(ANALYZE TABLE)
- 使用EXPLAIN ANALYZE查看实际执行情况(MySQL 8.0+)
- 建立慢查询监控,自动EXPLAIN分析
