手把手教你如何看懂 EXPLAIN:MySQL 性能调优的“透视镜”!
MySQL性能优化神器EXPLAIN详解:手把手教你定位SQL瓶颈。文章详细解析EXPLAIN命令的7个核心字段,包括type、key、rows等关键指标,通过SpringBoot实战案例演示如何分析执行计划。重点指出type=ALL和Extra中出现filesort/temporary是性能瓶颈信号,并提供索引优化方案,如建立联合索引实现覆盖查询。最后给出EXPLAIN速查表,帮助开发者快速识别
视频看了几百小时还迷糊?关注我,几分钟让你秒懂!
在 Spring Boot 项目中,你是不是经常遇到这样的问题:
- 接口突然变慢,SQL 查询要好几秒?
- 明明加了索引,但查询还是全表扫描?
- 不知道 MySQL 到底有没有用上你的索引?
这时候,EXPLAIN 就是你最强大的“透视镜”!
它能告诉你:MySQL 是怎么执行这条 SQL 的?用了什么索引?扫了多少行?
今天我们就手把手教你 看懂 EXPLAIN 的每一列含义,并结合 Java + Spring Boot 实战,让小白也能快速定位性能瓶颈!
一、什么是 EXPLAIN?
EXPLAIN 是 MySQL 提供的一个命令,用于分析 SQL 语句的执行计划(Execution Plan)。
你不需要真正执行 SQL,就能知道:
- 会不会走索引?
- 会扫描多少行?
- 是否需要临时表或文件排序?
基本用法
EXPLAIN SELECT * FROM user_info WHERE phone = '13800138000';
返回结果是一个表格,包含若干列(通常 10+ 列),我们重点看 7 个核心字段。
二、EXPLAIN 核心字段详解(附实战案例)
我们以一张用户订单表为例:
CREATE TABLE order_info (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
order_status TINYINT NOT NULL, -- 0:待支付, 1:已支付
create_time DATETIME NOT NULL,
amount DECIMAL(10,2) NOT NULL
);
-- 只建了一个单列索引
CREATE INDEX idx_user ON order_info(user_id);
现在执行:
EXPLAIN
SELECT * FROM order_info
WHERE user_id = 1001
AND order_status = 1
AND create_time >= '2026-01-01';
返回结果如下(简化):
| id | select_type | table | type | possible_keys | key | key_len | rows | Extra |
|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | order_info | ref | idx_user | idx_user | 8 | 50 | Using where |
下面逐个解释!
1️⃣ id:查询序列号
- 表示 SELECT 子句或操作表的顺序
- id 相同:从上到下执行
- id 不同:id 越大越先执行(如子查询)
✅ 一般单表查询都是 id=1,不用太关注。
2️⃣ select_type:查询类型
| 值 | 含义 |
|---|---|
SIMPLE |
简单查询(无子查询、UNION)✅ 最常见 |
PRIMARY |
最外层查询 |
SUBQUERY |
子查询 |
DERIVED |
FROM 中的子查询(派生表) |
UNION |
UNION 中的第二个或之后的 SELECT |
📌 重点关注是否是 SIMPLE,复杂查询可能性能差。
3️⃣ table:表名
- 当前操作的表
- 如果是多表 JOIN,会列出多个行
4️⃣ type:访问类型(最重要!)
表示 MySQL 如何查找行,性能从最优到最差:
| type | 含义 | 性能 | 是否可接受 |
|---|---|---|---|
system |
表只有一行(系统表) | ⚡ 极快 | ✅ |
const |
通过主键或唯一索引查一行 | ⚡ 快 | ✅ |
eq_ref |
主键/唯一索引 JOIN | ⚡ 快 | ✅ |
ref |
非唯一索引等值查询 | ✅ 好 | ✅ |
range |
范围扫描(如 BETWEEN, >, IN) | ✅ 可接受 | ✅ |
index |
全索引扫描(遍历索引树) | ⚠️ 慢 | ❌ 尽量避免 |
ALL |
全表扫描 | 💥 极慢 | ❌ 必须优化! |
🔥 黄金法则:生产环境绝不能出现
ALL!
在我们的例子中:type=ref → 用了非唯一索引等值查询,OK!
5️⃣ possible_keys:可能用到的索引
- MySQL 认为可以使用的索引列表
- 如果为
NULL,说明没有可用索引
在例子中:possible_keys = idx_user → 只有这个索引可选。
6️⃣ key:实际使用的索引(关键!)
- MySQL 最终选择的索引
- 如果为
NULL,说明没走索引(全表扫描)
✅ 我们看到 key = idx_user → 索引用上了!
⚠️ 但如果 possible_keys 有值,而 key 是 NULL,说明优化器认为走索引不如全表扫描快(比如低区分度字段)。
7️⃣ key_len:索引使用长度(字节)
- 表示索引中被使用部分的字节数
- 可用来判断联合索引用了几列
计算规则(InnoDB):
| 类型 | 长度 |
|---|---|
BIGINT |
8 字节 |
INT |
4 字节 |
DATETIME |
5 字节 |
VARCHAR(n) (UTF8MB4) |
n*4 + 2(可变长需额外 2 字节) |
| 允许 NULL | +1 字节 |
在我们的例子:
user_id是BIGINT NOT NULL→ 8 字节key_len = 8→ 只用了联合索引的第一列(如果有的话)
✅ 如果建了联合索引 (user_id, order_status),但 key_len=8,说明第二列没用上!
8️⃣ rows:预估扫描行数
- MySQL 估计需要扫描的行数
- 不是实际结果行数!
- 越小越好(理想是几十,别上万)
在例子中:rows=50 → 扫描 50 行,很好!
❌ 如果 rows=500000,即使 type=ref,也可能慢!
9️⃣ Extra:额外信息(非常重要!)
常见值及含义:
| Extra 值 | 含义 | 建议 |
|---|---|---|
Using where |
在存储引擎返回后,Server 层再过滤 | ✅ 正常(如多条件查,只一个走索引) |
Using index |
覆盖索引!无需回表 | ✅ 极佳! |
Using index condition |
索引条件下推(ICP) | ✅ 好 |
Using filesort |
需要额外排序(没用索引排序) | ❌ 必须优化! |
Using temporary |
用了临时表(如 GROUP BY 无索引) | ❌ 严重性能问题! |
Using join buffer |
JOIN 时用了缓冲区(没索引) | ❌ 需优化 JOIN 条件 |
💥 看到
Using filesort或Using temporary,立刻警惕!
三、Spring Boot 中如何使用 EXPLAIN?
方法 1:直接在数据库客户端执行(推荐开发阶段)
EXPLAIN SELECT * FROM order_info WHERE user_id = 1001;
方法 2:在 MyBatis Mapper 中写(仅调试用)
@Mapper
public interface OrderMapper {
@Select("EXPLAIN SELECT * FROM order_info WHERE user_id = #{userId}")
List<Map<String, Object>> explainFindByUserId(@Param("userId") Long userId);
}
然后在 Service 中调用:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public void debugQuery(Long userId) {
List<Map<String, Object>> plan = orderMapper.explainFindByUserId(userId);
plan.forEach(System.out::println);
// 输出:{id=1, select_type=SIMPLE, table=order_info, type=ref, ...}
}
}
⚠️ 注意:不要在生产接口中调用 EXPLAIN,仅用于本地调试或压测分析。
四、实战案例:从“慢查询”到“毫秒级”
❌ 问题 SQL
SELECT * FROM order_info
WHERE order_status = 1
ORDER BY create_time DESC
LIMIT 10;
EXPLAIN 结果:
| type | key | rows | Extra |
|---|---|---|---|
| ALL | NULL | 1000000 | Using where; Using filesort |
💥 问题:
- 全表扫描(
type=ALL) - 额外排序(
Using filesort)
✅ 优化:建联合索引
-- 覆盖查询 + 排序
CREATE INDEX idx_status_time ON order_info(order_status, create_time);
再次 EXPLAIN:
| type | key | rows | Extra |
|---|---|---|---|
| range | idx_status_time | 700000 | Using index |
✅ 效果:
- 走索引范围扫描
Using index→ 覆盖索引,无需回表- 自动按 create_time 排序,无需 filesort
查询从 2 秒 → 20 毫秒!
五、常见误区 & 注意事项
❌ 误区 1:“有索引就一定快”
→ 错!如果 type=ALL 或 rows 很大,索引可能没用上。
❌ 误区 2:“EXPLAIN 的 rows 是实际结果数”
→ 错!它是基于统计信息的估算值,可能不准(可通过 ANALYZE TABLE 更新统计)。
✅ 建议 1:高频查询必须 EXPLAIN
上线前对核心 SQL 执行 EXPLAIN,确保:
type不是ALLkey不是NULLExtra没有filesort/temporary
✅ 建议 2:善用覆盖索引
把 SELECT 的字段也放进索引,避免回表:
-- 查询只用索引就能完成
CREATE INDEX idx_cover ON order_info(user_id, create_time, amount);
SELECT create_time, amount FROM order_info WHERE user_id = 1001;
六、总结:EXPLAIN 速查表
| 字段 | 关注点 | 好 | 坏 |
|---|---|---|---|
type |
访问类型 | const/ref/range |
index/ALL |
key |
实际索引 | 非 NULL | NULL |
rows |
扫描行数 | < 1000 | > 10000 |
Extra |
额外操作 | Using index |
filesort/temporary |
记住:EXPLAIN 不是魔法,而是你和 MySQL 对话的桥梁。看懂它,你就掌握了 SQL 性能调优的钥匙!
视频看了几百小时还迷糊?关注我,几分钟让你秒懂!
更多推荐


所有评论(0)