EXPLAIN 是分析慢 SQL 的"X 光机"。但很多人执行完 EXPLAIN 后面对十几个字段不知道看什么——其实只需要盯住 4 个核心字段,按固定顺序排查,90% 的慢 SQL 都能定位到原因。
下面按排查优先级来"看 EXPLAIN 的标准动作"。
执行 EXPLAIN SELECT ... 后,输出大概长这样:
+----+-------------+----------+-------+---------------+---------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+----------+-------+---------------+---------+---------+------+--------+-------------+
| 1 | SIMPLE | sys_user | range | PRIMARY,idx_n | PRIMARY | 8 | NULL | 20 | Using where |
+----+-------------+----------+-------+---------------+---------+---------+------+--------+-------------+
排查顺序:type → key → rows → Extra,这四个字段能告诉你 90% 的信息。
type(访问类型)—— 最先看,决定生死type 表示 MySQL 用什么方式找到数据的,从快到慢排:
const > eq_ref > ref > range > index > ALL
| type | 含义 | 性能 | 是否需优化 |
|---|---|---|---|
const | 主键/唯一索引等值查询,最多返回 1 行 | ⭐⭐⭐⭐⭐ | 不需要 |
eq_ref | JOIN 时主键/唯一索引关联 | ⭐⭐⭐⭐⭐ | 不需要 |
ref | 非唯一索引等值查询 | ⭐⭐⭐⭐ | 通常 OK |
range | 索引范围扫描(>, <, BETWEEN, IN) | ⭐⭐⭐⭐ | 游标分页的理想状态 |
index | 全索引扫描(遍历整个索引树) | ⭐⭐ | 需优化 |
ALL | 全表扫描(遍历整张表) | ⭐ | 必须优化 |
-- ❌ type = ALL → 全表扫描,千万级表直接卡死
EXPLAIN SELECT * FROM sys_user WHERE name = '张三';
-- 原因:name 字段没建索引
-- ✅ type = ref → 走非唯一索引
EXPLAIN SELECT * FROM sys_user WHERE phone = '13800138000';
-- 原因:phone 有普通索引
-- ✅ type = range → 索引范围扫描(你的游标分页就是这种)
EXPLAIN SELECT * FROM sys_user WHERE id > 100000 ORDER BY id LIMIT 20;
-- 原因:id 是主键,用了 > 条件
口诀:看到
ALL就一定要优化,看到index也要想想能不能改成range或ref。
key(实际使用的索引)key 告诉你 MySQL 最终选了哪个索引。
| 情况 | 含义 | 对策 |
|---|---|---|
key = PRIMARY | 走了主键索引 | √ 最好情况 |
key = 你建的某个索引 | 走了预期索引 | √ 正常 |
key = NULL | 没走任何索引 | × 必须查为什么 |
key 不是你期望的索引 | 优化器选错了 | 用 FORCE INDEX 或分析统计信息 |
key = NULL?常见原因:
-- 1. 对索引列做了函数操作
WHERE DATE(create_time) = '2024-01-01' -- × 索引失效
WHERE create_time >= '2024-01-01 00:00:00' AND create_time < '2024-01-02 00:00:00' -- √
-- 2. 隐式类型转换
WHERE phone = 13800138000 -- × phone 是 varchar,传入数字导致全表扫描
WHERE phone = '13800138000' -- √
-- 3. LIKE 以 % 开头
WHERE name LIKE '%张%' -- × 索引失效,全表扫描
WHERE name LIKE '张%' -- √ 可以用索引
-- 4. 优化器认为全表扫描更快(数据量太小或索引区分度太低)
-- 解决:ANALYZE TABLE sys_user; 更新统计信息
rows(估算扫描行数)rows 是优化器估算的需要扫描的行数(不是精确值)。
rows = 1 → 几乎完美
rows < 1000 → 正常
rows < 10000 → 还行,但需注意
rows > 100000 → 有问题,需要优化
rows = 全表行数 → 全表扫描,灾难
-- 游标分页
EXPLAIN SELECT * FROM sys_user WHERE id > 100000 ORDER BY id LIMIT 20;
-- rows ≈ 20 → √ 只扫 20 行
-- 深分页
EXPLAIN SELECT * FROM sys_user ORDER BY id LIMIT 100000, 20;
-- rows ≈ 100020 → × 扫了 10 万行
-- 全表扫描
EXPLAIN SELECT * FROM sys_user WHERE name LIKE '%张%';
-- rows ≈ 全表行数(比如 5000000)→ × 扫了 500 万行
口诀:
rows和你预期返回的行数差距越大,说明索引越没用上。
Extra(额外信息)—— 信息量最大Extra 是最容易暴露问题的字段,几个关键值:
| Extra 值 | 含义 |
|---|---|
Using index | 覆盖索引,不需要回表,最快 |
Using index condition | 索引下推 (ICP),在存储引擎层就过滤了数据,减少回表 |
| Extra 值 | 含义 | 怎么优化 |
|---|---|---|
Using filesort | 需要额外排序,没用上索引的有序性 | 建联合索引让 ORDER BY 走索引 |
Using temporary | 用了临时表(GROUP BY / DISTINCT / UNION) | 加索引或简化 SQL |
Using where | 服务层做了额外过滤(索引没完全过滤掉) | 看是否能在索引层面就过滤 |
Select tables optimized away | 优化器直接从索引返回结果,不需要查表 | ✅ 最好情况 |
-- 慢 SQL(执行 3 秒)
SELECT id, name, phone, status, create_time
FROM sys_user
WHERE name LIKE '%张%'
AND status = 1
ORDER BY create_time DESC
LIMIT 100000, 20;
+----+-------------+----------+------+---------------+------+---------+------+---------+-----------------------------+
| id | select_type | table | type | possible_keys | key | rows | Extra |
+----+-------------+----------+------+---------------+------+---------+------+---------+-----------------------------+
| 1 | SIMPLE | sys_user | ALL | idx_status | NULL | 5000000 | Using where; Using filesort |
+----+-------------+----------+------+---------------+------+---------+------+---------+-----------------------------+
| 字段 | 值 | 问题 |
|---|---|---|
type | ALL | ❌ 全表扫描 500 万行 |
key | NULL | ❌ 有 idx_status 但没走(因为 name LIKE '%张%' 让优化器觉得走索引不如全表扫) |
rows | 5000000 | ❌ 扫了全表 |
Extra | Using where; Using filesort | ❌ 没走索引过滤 + 需要额外排序 |
方案 A:业务层面(推荐)
LIKE '%xx%' 前导通配符,改成 LIKE '张%'(能走索引)方案 B:索引层面
-- 建联合索引,让 WHERE + ORDER BY 都能走索引
CREATE INDEX idx_status_create_time ON sys_user(status, create_time DESC);
-- 这样 status = 1 走索引,create_time 有序,避免 filesort
方案 C:搜索引擎
%关键词% 模糊搜索,把数据同步到 Elasticsearch很多人建了联合索引但 EXPLAIN 显示没走,是因为违反了最左前缀原则:
-- 建了联合索引:idx_a_b_c (a, b, c)
| SQL 条件 | 能走索引? | 说明 |
|---|---|---|
WHERE a = 1 | ✅ | 用了最左列 |
WHERE a = 1 AND b = 2 | ✅ | 用了 a + b |
WHERE a = 1 AND b = 2 AND c = 3 | ✅ | 用了 a + b + c |
WHERE b = 2 | ❌ | 没用最左列 a |
WHERE a = 1 AND c = 3 | ⚠️ | 只用了 a,c 用不上 |
WHERE a > 1 AND b = 2 | ⚠️ | a 走范围后,b 的有序性被破坏 |
口诀:联合索引像电话号,必须从最左边开始拨。
1. EXPLAIN 看 type
├── ALL → 全表扫描,检查 WHERE 条件有没有索引
├── index → 全索引扫描,看能不能改成 range
└── range/ref → 继续看下面
2. 看 key 是不是预期索引
├── NULL → 索引失效(函数/类型转换/LIKE 前缀%)
└── 不是预期的 → 优化器选错了,ANALYZE TABLE 或 FORCE INDEX
3. 看 rows 是不是远大于实际返回行数
└── 是 → 索引区分度低,考虑组合索引
4. 看 Extra
├── Using filesort → ORDER BY 没走索引,建联合索引
├── Using temporary → GROUP BY 没走索引
└── Using index → 完美,覆盖索引
5. 如果以上都 OK 但还是慢 → 看是不是锁等待、IO 瓶颈、网络问题
EXPLAIN FORMAT=JSON如果你用的是 MySQL 5.7+,可以用 JSON 格式看更详细的信息:
EXPLAIN FORMAT=JSON
SELECT * FROM sys_user WHERE id > 100000 ORDER BY id LIMIT 20;
JSON 输出里能看到:
query_cost:查询成本(数字越小越好)used_columns:用到了哪些列attached_condition:实际过滤条件看 EXPLAIN,记住四句话:
- type 是 ALL,必须得改
- key 是 NULL,索引白建
- rows 特别大,扫描太多
- Extra 有 filesort,排序没走索引