Explain 执行计划深度解读
候选人小赵参加腾讯 P6 面试,面试官看了他写的慢查询:
SELECT * FROM orders WHERE status = 1 ORDER BY create_time DESC LIMIT 10;
问:"这条 SQL 怎么分析性能?"
小赵说:"用 EXPLAIN 看一下?"面试官:"那你给我解读一下执行计划。"
小赵看着输出,愣了半天:"type 是 ALL...key 是 NULL..."面试官:"这说明什么?"
小赵答不上来。
【面试官心理】
这道题我用来测试候选人对 EXPLAIN 的掌握程度。能说出 type=ALL 是全表扫描的占 80%,能完整解读所有字段的占 30%,能根据 Extra 优化 SQL 的占 10%。EXPLAIN 是 MySQL 优化的基本功,但真正能读懂的人不多。
一、EXPLAIN 基础解读 🔴
1.1 EXPLAIN 输出格式
EXPLAIN SELECT * FROM orders WHERE status = 1;
1.2 核心字段详解
type:查询类型(最重要)
-- system: 表只有一行
EXPLAIN SELECT * FROM (SELECT 1) AS t;
-- const: 主键查询
EXPLAIN SELECT * FROM orders WHERE id = 1001;
-- eq_ref: 关联查询用主键
EXPLAIN SELECT * FROM orders o JOIN order_items i ON o.id = i.order_id;
-- ref: 普通索引查询
EXPLAIN SELECT * FROM orders WHERE user_id = '1001';
-- range: 范围查询
EXPLAIN SELECT * FROM orders WHERE id > 1000;
-- index: 全索引扫描
EXPLAIN SELECT id FROM orders; -- 只查 id 列,用 idx_id 扫描
-- ALL: 全表扫描
EXPLAIN SELECT * FROM orders WHERE status = 1; -- status 无索引
⚠️
type=ALL 是性能杀手,意味着要扫描整张表。如果是大表,这条 SQL 就是生产事故的根源。
key:实际使用的索引
EXPLAIN SELECT * FROM orders WHERE user_id = '1001';
possible_keys:MySQL 认为可能用到的索引(候选列表)
key:MySQL 实际选择的索引
key = NULL 说明 MySQL 认为全表扫描比走索引更快。
rows:估算扫描行数
-- rows 告诉你 MySQL 估算要扫描多少行
EXPLAIN SELECT * FROM orders WHERE user_id = '1001';
-- rows = 5000,意味着 MySQL 预估要扫描 5000 行
rows 越大,性能越差。结合 type 字段判断:
- type=ALL, rows=10000000:大表全表扫描
- type=ref, rows=10:精准索引查询
1.3 ❌ 错误示范
候选人原话:"EXPLAIN 的 key 是 NULL,说明没建索引。"
问题诊断:
- 混淆了 possible_keys 和 key
- 忽略了优化器可能选择不走索引的情况
- 不理解
key=NULL + type=ALL 的组合含义
2.1 Using filesort:文件排序
EXPLAIN SELECT * FROM orders WHERE status = 1 ORDER BY create_time;
Using filesort 表示 MySQL 无法利用索引顺序排序,只能在内存或磁盘中排序。
graph TD
A[无法用索引排序] --> B["ORDER BY 字段不在索引中 或 方向不一致"]
B --> C["MySQL 在内存/磁盘中排序"]
C --> D["数据量大时性能严重下降"]
性能影响:filesort 是一次性排序,数据量超过 sort_buffer_size 时会落盘。
-- ❌ 触发 Using filesort
-- 索引 (status, create_time),但查询条件导致索引失效
SELECT * FROM orders WHERE status = 1 ORDER BY create_time DESC;
-- 索引是正序,查询是倒序,无法使用
-- ✅ 不触发 Using filesort
-- 建倒序索引
ALTER TABLE orders ADD INDEX idx_status_time_desc (status, create_time DESC);
2.2 Using temporary:临时表
EXPLAIN SELECT * FROM orders WHERE status = 1 GROUP BY user_id;
Using temporary 表示 MySQL 需要创建临时表保存中间结果。
graph TD
A[GROUP BY / DISTINCT / UNION] --> B[结果无法在索引中直接得到]
B --> C[创建临时表存储中间结果]
C --> D["Using filesort 对临时表排序"]
💡
Using temporary + Using filesort 是性能杀手。如果在生产环境中出现,要重点优化。
2.3 Using index:覆盖索引
EXPLAIN SELECT id, user_id FROM orders WHERE user_id = '1001';
Using index 表示完全在索引中返回数据,不需要回表。
2.4 Using index condition:索引下推
EXPLAIN SELECT * FROM orders WHERE user_id = '1001' AND status IN (1, 2);
Using index condition 表示使用了索引下推(ICP),在索引遍历中过滤条件。
2.5 Using where:WHERE 条件在 Server 层过滤
EXPLAIN SELECT * FROM orders WHERE status = 1 AND create_time > '2024-01-01';
Using where 表示 Storage 引擎返回数据后,Server 层还需要根据 WHERE 条件过滤。
注意:Using where 本身不代表性能差,要结合其他字段判断。
三、实战:解读一条慢查询 🟡
3.1 原始查询
SELECT o.id, o.order_no, o.amount, o.status, u.name, u.phone
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 1
AND o.create_time >= '2024-01-01'
AND u.city = '北京'
ORDER BY o.create_time DESC
LIMIT 20;
3.2 EXPLAIN 输出
EXPLAIN SELECT o.id, o.order_no, o.amount, o.status, u.name, u.phone
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 1
AND o.create_time >= '2024-01-01'
AND u.city = '北京'
ORDER BY o.create_time DESC
LIMIT 20;
3.3 问题分析
- orders 表:type=range(范围查询),rows=50000(扫描 5 万行),有 Using filesort
- users 表:type=eq_ref(主键关联),Using where(city='北京' 在 Server 层过滤)
优化方向:
- 给 orders 加覆盖索引
(status, create_time, id, order_no, amount)
- 给 users.city 加索引
- 让 city 条件也能在索引中过滤
3.4 优化后
-- 添加索引
ALTER TABLE orders ADD INDEX idx_cover (status, create_time, id, order_no, amount);
ALTER TABLE users ADD INDEX idx_city (city, id, name, phone);
-- 验证优化
EXPLAIN SELECT o.id, o.order_no, o.amount, o.status, u.name, u.phone
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 1
AND o.create_time >= '2024-01-01'
AND u.city = '北京'
ORDER BY o.create_time DESC
LIMIT 20;
Using index 出现了,说明覆盖索引生效了。
四、EXPLAIN ANALYZE(MySQL 8.0+)🟡
4.1 比 EXPLAIN 更精确
EXPLAIN ANALYZE SELECT * FROM orders WHERE status = 1;
-> Nested loop inner join (cost=1000 rows=5000)
-> Table scan on o (cost=500 rows=10000)
-> Single-row index lookup on u using PRIMARY (id=o.user_id) (cost=1 rows=1)
EXPLAIN ANALYZE 显示的是实际执行代价,而不是预估:
- actual time:实际执行时间
- rows examined:实际扫描行数
- loops:循环次数
💡
生产环境优化时,优先用 EXPLAIN ANALYZE,它能告诉你优化器的预估和实际执行的差异。
五、生产避坑
5.1 误区:rows 越小越好
-- rows=1 但 type=ALL,仍然很慢
EXPLAIN SELECT * FROM orders WHERE order_no = 'abc123'; -- order_no 没有索引
-- Using index 但有 Using filesort
EXPLAIN SELECT id, create_time FROM orders ORDER BY create_time;
-- 索引覆盖但无法利用索引顺序
【面试官心理】
我问他 EXPLAIN,通常还会追问"如果 type=ALL 你怎么优化"、"Using filesort 怎么消除"。能完整回答这三个问题的,基本都有 SQL 优化实战经验。