MySQL EXPLAIN执行计划分析
约 1775 字大约 6 分钟
mysqlexplain
2025-05-05
EXPLAIN 是 MySQL 中分析查询性能最重要的工具。通过解读执行计划,可以了解优化器如何选择索引、表的访问方式以及连接顺序,从而针对性地优化慢查询。
EXPLAIN 基本用法
-- 基本用法
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
-- 格式化输出(推荐 JSON 或 TREE)
EXPLAIN FORMAT=JSON SELECT ...;
EXPLAIN FORMAT=TREE SELECT ...;
-- EXPLAIN ANALYZE(MySQL 8.0.18+):实际执行并显示真实耗时
EXPLAIN ANALYZE SELECT * FROM users WHERE age > 25;EXPLAIN 输出列详解
id 列
标识 SELECT 的序号,id 越大越先执行;id 相同则从上到下执行。
-- id 相同:多表 JOIN
EXPLAIN SELECT * FROM t1 JOIN t2 ON t1.id = t2.t1_id;
-- id=1, table=t1
-- id=1, table=t2
-- id 不同:子查询
EXPLAIN SELECT * FROM t1 WHERE id IN (SELECT t1_id FROM t2);
-- id=1, table=t1
-- id=2, table=t2 ← 先执行select_type 列
| select_type | 说明 |
|---|---|
| SIMPLE | 简单查询,不含子查询或 UNION |
| PRIMARY | 最外层查询 |
| SUBQUERY | WHERE 中的子查询 |
| DERIVED | FROM 中的子查询(派生表) |
| UNION | UNION 中的第二个及后续查询 |
| DEPENDENT SUBQUERY | 相关子查询(依赖外层) |
| MATERIALIZED | 子查询被物化为临时表 |
type 列(访问类型)
这是 EXPLAIN 中最重要的列,反映了优化器对表的访问方式。性能从最优到最差排列:
| type | 说明 | 示例 |
|---|---|---|
| system | 表只有一行 | 系统表 |
| const | 主键或唯一索引等值查询,最多返回一行 | WHERE id = 1 |
| eq_ref | JOIN 时被驱动表使用主键/唯一索引等值匹配 | JOIN t2 ON t1.id = t2.id |
| ref | 非唯一索引等值查询 | WHERE name = 'Alice' |
| range | 索引范围扫描 | WHERE id > 10 AND id < 100 |
| index | 全索引扫描(遍历索引树) | 覆盖索引但无 WHERE 条件 |
| ALL | 全表扫描 | 无可用索引 |
-- const 示例
EXPLAIN SELECT * FROM users WHERE id = 1;
-- type: const
-- eq_ref 示例
EXPLAIN SELECT * FROM orders o JOIN users u ON o.user_id = u.id;
-- users 表:type=eq_ref
-- ref 示例(name 有普通索引)
EXPLAIN SELECT * FROM users WHERE name = 'Alice';
-- type: ref
-- range 示例
EXPLAIN SELECT * FROM users WHERE age BETWEEN 20 AND 30;
-- type: range
-- ALL 示例(无索引列查询)
EXPLAIN SELECT * FROM users WHERE bio LIKE '%something%';
-- type: ALLpossible_keys 和 key 列
-- possible_keys: 可能使用的索引(基于查询条件)
-- key: 实际选择的索引
-- 有时 possible_keys 有多个,但 key 只选一个
EXPLAIN SELECT * FROM users WHERE name = 'Alice' AND email = 'a@b.com';
-- possible_keys: idx_name, idx_email
-- key: idx_email (优化器基于选择性选择)
-- 强制使用指定索引
SELECT * FROM users FORCE INDEX (idx_name) WHERE name = 'Alice';key_len 列
key_len 表示使用了索引的多少字节,可以判断复合索引使用了哪些列。
-- 计算规则
-- INT: 4 bytes
-- BIGINT: 8 bytes
-- VARCHAR(N): N × 字符集字节数 + 2 (变长标记)
-- 允许 NULL 的列额外 +1 byte
-- 复合索引 (name VARCHAR(50), age INT),UTF8MB4
-- 仅使用 name: key_len = 50 × 4 + 2 = 202
-- 使用 name + age: key_len = 202 + 4 = 206
CREATE INDEX idx_name_age ON users(name, age);
EXPLAIN SELECT * FROM users WHERE name = 'Alice';
-- key_len: 202 (仅用了 name)
EXPLAIN SELECT * FROM users WHERE name = 'Alice' AND age = 25;
-- key_len: 206 (name + age 都用了)rows 和 filtered 列
-- rows: 优化器估算需要扫描的行数(估算值,非精确)
-- filtered: 经过条件过滤后剩余行数的百分比
EXPLAIN SELECT * FROM users WHERE name = 'Alice' AND status = 1;
-- rows: 100, filtered: 10.00
-- 实际结果估计 = 100 × 10% = 10 行Extra 列
Extra 列包含额外的执行信息,以下是常见且重要的值:
| Extra 值 | 含义 | 好坏 |
|---|---|---|
| Using index | 覆盖索引,无需回表 | 好 |
| Using where | 存储引擎返回后在 Server 层过滤 | 中性 |
| Using index condition | 索引条件下推(ICP) | 好 |
| Using temporary | 使用临时表 | 需优化 |
| Using filesort | 额外排序操作 | 需优化 |
| Using join buffer | JOIN 时使用了 Block Nested Loop | 需优化 |
| Select tables optimized away | 直接从索引获取结果(如 MIN/MAX) | 好 |
| Impossible where | WHERE 条件永远为 false | 检查逻辑 |
-- Using index(覆盖索引)
EXPLAIN SELECT id, name FROM users WHERE name = 'Alice';
-- Extra: Using index (idx_name 覆盖了查询需要的所有列)
-- Using filesort(额外排序)
EXPLAIN SELECT * FROM users WHERE age > 20 ORDER BY name;
-- Extra: Using filesort (age 索引排序和 name 排序不一致)
-- Using temporary(临时表)
EXPLAIN SELECT DISTINCT name FROM users ORDER BY age;
-- Extra: Using temporary; Using filesort
-- Using index condition(ICP)
EXPLAIN SELECT * FROM users WHERE name LIKE 'A%' AND name LIKE '%e';
-- Extra: Using index conditionEXPLAIN ANALYZE
MySQL 8.0.18 引入的 EXPLAIN ANALYZE 会实际执行查询,显示每个步骤的真实耗时和行数。
EXPLAIN ANALYZE
SELECT u.name, COUNT(o.id)
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.age > 25
GROUP BY u.name;输出示例:
-> Table scan on <temporary> (cost=X rows=Y) (actual time=A..B rows=Z loops=1)
-> Aggregate using temporary table (cost=X rows=Y) (actual time=A..B rows=Z loops=1)
-> Nested loop inner join (cost=X rows=Y) (actual time=A..B rows=Z loops=1)
-> Filter: (u.age > 25) (cost=X rows=Y) (actual time=A..B rows=Z loops=1)
-> Table scan on u (cost=X rows=Y) (actual time=A..B rows=Z loops=1)
-> Index lookup on o using idx_user_id (user_id=u.id) (cost=X rows=Y) (actual time=A..B rows=Z loops=W)关键信息:
- actual time=A..B:A 是返回第一行的时间,B 是返回所有行的时间(毫秒)
- rows:实际返回的行数(与估算对比)
- loops:该步骤执行的次数
实战优化示例
案例一:索引选择错误
-- 问题查询
EXPLAIN SELECT * FROM orders
WHERE user_id = 100 AND status = 'paid'
ORDER BY created_at DESC LIMIT 10;
-- type: ref, key: idx_user_id
-- Extra: Using where; Using filesort ← filesort 需要优化
-- 优化:创建复合索引
CREATE INDEX idx_uid_status_created ON orders(user_id, status, created_at);
-- 优化后
-- type: ref, key: idx_uid_status_created
-- Extra: Using where ← 无 filesort,利用索引排序案例二:子查询优化
-- 问题:相关子查询
EXPLAIN SELECT * FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.amount > 1000
);
-- select_type: DEPENDENT SUBQUERY(对每行外层记录执行一次子查询)
-- 优化:改为 JOIN
EXPLAIN SELECT DISTINCT u.* FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.amount > 1000;
-- select_type: SIMPLE, 无 DEPENDENT SUBQUERY案例三:分页优化
-- 问题:深分页
EXPLAIN SELECT * FROM articles ORDER BY id LIMIT 1000000, 10;
-- type: ALL 或 index,需要扫描 1000010 行
-- 优化:基于游标分页
EXPLAIN SELECT * FROM articles
WHERE id > 1000000
ORDER BY id LIMIT 10;
-- type: range,只扫描 10 行执行计划分析流程
总结
- EXPLAIN 的核心是 type 列,至少应达到 range 级别
- Extra 中的
Using filesort和Using temporary是优化重点 EXPLAIN ANALYZE提供真实执行数据,比 EXPLAIN 更准确- 使用
key_len判断复合索引是否被充分利用 - 定期使用
ANALYZE TABLE更新统计信息,确保优化器做出正确决策
贡献者
更新日志
2026/3/14 13:09
查看所有更新日志
9f6c2-feat: organize wiki content and refresh site setup于