文章目录
-
- 一、EXPLAIN基础
-
- [1.1 什么是EXPLAIN?](#1.1 什么是EXPLAIN?)
- [1.2 基本用法](#1.2 基本用法)
- 二、EXPLAIN输出字段详解
-
- [2.1 标准EXPLAIN输出示例](#2.1 标准EXPLAIN输出示例)
- [2.2 字段含义总览](#2.2 字段含义总览)
- 三、实际案例分析
-
- [3.1 案例1:主键查询(最优)](#3.1 案例1:主键查询(最优))
- [3.2 案例2:普通索引查询](#3.2 案例2:普通索引查询)
- [3.3 案例3:范围查询](#3.3 案例3:范围查询)
- [3.4 案例4:全表扫描(最差)](#3.4 案例4:全表扫描(最差))
- [3.5 案例5:覆盖索引](#3.5 案例5:覆盖索引)
- [3.6 案例6:索引失效](#3.6 案例6:索引失效)
- [3.7 案例7:多表JOIN](#3.7 案例7:多表JOIN)
- [3.8 案例8:子查询](#3.8 案例8:子查询)
- [3.9 案例9:UNION查询](#3.9 案例9:UNION查询)
- 四、type类型详解
-
- [4.1 type类型性能排序](#4.1 type类型性能排序)
- [4.2 system - 表中只有一行](#4.2 system - 表中只有一行)
- [4.3 const - 主键或唯一索引等值查询](#4.3 const - 主键或唯一索引等值查询)
- [4.4 eq_ref - JOIN时使用主键或唯一索引](#4.4 eq_ref - JOIN时使用主键或唯一索引)
- [4.5 ref - 非唯一索引等值查询](#4.5 ref - 非唯一索引等值查询)
- [4.6 ref_or_null - ref + NULL值查询](#4.6 ref_or_null - ref + NULL值查询)
- [4.7 index_merge - 合并多个索引](#4.7 index_merge - 合并多个索引)
- [4.8 range - 范围扫描](#4.8 range - 范围扫描)
- [4.9 index - 全索引扫描](#4.9 index - 全索引扫描)
- [4.10 ALL - 全表扫描](#4.10 ALL - 全表扫描)
- 五、Extra信息详解
-
- [5.1 Using index - 覆盖索引(最优)](#5.1 Using index - 覆盖索引(最优))
- [5.2 Using where - WHERE过滤](#5.2 Using where - WHERE过滤)
- [5.3 Using index condition - 索引下推(ICP)](#5.3 Using index condition - 索引下推(ICP))
- [5.4 Using filesort - 文件排序(需优化)](#5.4 Using filesort - 文件排序(需优化))
- [5.5 Using temporary - 使用临时表(需优化)](#5.5 Using temporary - 使用临时表(需优化))
- [5.6 Using join buffer - JOIN缓冲](#5.6 Using join buffer - JOIN缓冲)
- [5.7 Impossible WHERE - WHERE条件永远为假](#5.7 Impossible WHERE - WHERE条件永远为假)
- [5.8 Select tables optimized away](#5.8 Select tables optimized away)
- [5.9 Using union/intersect/sort_union](#5.9 Using union/intersect/sort_union)
- 六、EXPLAIN的变体
-
- [6.1 EXPLAIN ANALYZE(MySQL 8.0.18+)](#6.1 EXPLAIN ANALYZE(MySQL 8.0.18+))
- [6.2 EXPLAIN FORMAT=JSON](#6.2 EXPLAIN FORMAT=JSON)
- [6.3 EXPLAIN FORMAT=TREE(MySQL 8.0.16+)](#6.3 EXPLAIN FORMAT=TREE(MySQL 8.0.16+))
- 七、优化实战案例
-
- [7.1 案例1:慢查询优化](#7.1 案例1:慢查询优化)
- [7.2 案例2:JOIN优化](#7.2 案例2:JOIN优化)
- [7.3 案例3:子查询优化](#7.3 案例3:子查询优化)
- 八、常见问题诊断
-
- [8.1 如何判断SQL是否需要优化?](#8.1 如何判断SQL是否需要优化?)
- [8.2 为什么possible_keys有值,但key是NULL?](#8.2 为什么possible_keys有值,但key是NULL?)
- [8.3 如何理解key_len?](#8.3 如何理解key_len?)
- [8.4 rows和filtered的关系](#8.4 rows和filtered的关系)
- [8.5 为什么EXPLAIN显示rows=1000,但实际扫描了更多?](#8.5 为什么EXPLAIN显示rows=1000,但实际扫描了更多?)
- 总结
一、EXPLAIN基础
1.1 什么是EXPLAIN?
EXPLAIN 是MySQL提供的查询分析工具,用于查看SQL语句的执行计划,帮助我们了解:
- MySQL如何执行这条SQL
- 是否使用了索引
- 扫描了多少行数据
- 是否需要临时表或排序
1.2 基本用法
sql
-- 基本语法
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输出示例
sql
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:主键查询(最优)
sql
-- 测试表
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:普通索引查询
sql
-- 创建索引
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:范围查询
sql
-- 创建索引
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:全表扫描(最差)
sql
-- 没有索引的列
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过滤,但在存储引擎层之后
性能评价:⭐ 很差!需要优化
优化方案:
sql
-- 添加索引
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:覆盖索引
sql
-- 创建联合索引
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
:覆盖索引! 无需回表,性能最优
对比:需要回表的查询:
sql
-- 查询索引外的列
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:索引失效
sql
-- 索引列使用函数
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
-- 改写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
sql
-- 查询用户及其订单
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:子查询
sql
-- 标量子查询
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
sql
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查询
sql
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
:使用了临时表(用于去重)
优化建议:
sql
-- 如果不需要去重,使用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 - 表中只有一行
sql
-- 系统表或只有一行的表
EXPLAIN SELECT * FROM (SELECT 1) t;
结果:
+----+-------------+-------+--------+
| id | select_type | table | type |
+----+-------------+-------+--------+
| 1 | SIMPLE | t | system |
+----+-------------+-------+--------+
性能:⭐⭐⭐⭐⭐ 最优
4.3 const - 主键或唯一索引等值查询
sql
-- 通过主键查询
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时使用主键或唯一索引
sql
-- 每个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 - 非唯一索引等值查询
sql
-- 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值查询
sql
-- 查询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 - 合并多个索引
sql
-- 使用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 - 范围扫描
sql
-- 范围查询
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 - 全索引扫描
sql
-- 扫描整个索引树(比全表扫描好,但仍不理想)
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 - 全表扫描
sql
-- 没有可用索引,全表扫描
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 - 覆盖索引(最优)
sql
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过滤
sql
EXPLAIN SELECT * FROM users WHERE age > 20;
结果:
Extra: Using where
含义:
- 使用WHERE条件过滤
- 过滤发生在Server层(而非存储引擎层)
- 正常情况,不一定需要优化
5.3 Using index condition - 索引下推(ICP)
sql
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 - 文件排序(需优化)
sql
-- age列没有索引
EXPLAIN SELECT * FROM users ORDER BY age;
结果:
Extra: Using filesort
含义:
- ❌ MySQL需要额外的排序操作
- ❌ 无法利用索引排序
- ❌ 性能较差,需要优化
优化方案:
sql
-- 为排序列创建索引
CREATE INDEX idx_age ON users(age);
-- 再次查询
EXPLAIN SELECT * FROM users ORDER BY age;
优化后:
Extra: Using index
5.5 Using temporary - 使用临时表(需优化)
sql
-- GROUP BY非索引列
EXPLAIN SELECT age, COUNT(*) FROM users GROUP BY email;
结果:
Extra: Using temporary; Using filesort
含义:
- ❌ MySQL创建临时表来处理查询
- ❌ 常见于GROUP BY、DISTINCT、UNION
- ❌ 性能较差,需要优化
优化方案:
sql
-- 为GROUP BY列创建索引
CREATE INDEX idx_email ON users(email);
5.6 Using join buffer - JOIN缓冲
sql
-- 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缓存
- ⚠️ 性能不佳,建议添加索引
优化方案:
sql
CREATE INDEX idx_user_email ON orders(user_email);
5.7 Impossible WHERE - WHERE条件永远为假
sql
EXPLAIN SELECT * FROM users WHERE 1 = 0;
结果:
Extra: Impossible WHERE
含义:
- WHERE条件永远不成立
- MySQL直接返回空结果,不执行查询
5.8 Select tables optimized away
sql
EXPLAIN SELECT MIN(id) FROM users;
结果:
Extra: Select tables optimized away
含义:
- ✅ 优化器直接从索引中获取结果
- ✅ 无需扫描表
- ✅ 性能极佳
5.9 Using union/intersect/sort_union
sql
-- 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+)
sql
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
sql
EXPLAIN FORMAT=JSON SELECT * FROM users WHERE id = 1\G
输出:
json
{
"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+)
sql
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:
sql
SELECT * FROM orders
WHERE DATE(create_time) = '2024-01-01'
ORDER BY amount DESC;
EXPLAIN分析:
sql
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
:需要额外排序
优化方案:
sql
-- 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:
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亿次比较!
优化方案:
sql
-- 去掉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:
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
sql
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?
原因:优化器认为不走索引更快(如返回大部分数据)
示例:
sql
-- 假设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字节
示例:
sql
-- 联合索引
CREATE INDEX idx_name_age_email ON users(
name VARCHAR(50), -- utf8mb4
age 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
示例:
sql
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显示的是预估值,基于统计信息。
解决方案:
sql
-- 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分析