一、先看 type:访问方式
从好到坏:
| type | 含义 | 评价 |
|---|---|---|
system | 表只有一行 | 最好 |
const | 主键/唯一索引等值查 | 极快 |
eq_ref | 联表时用主键/唯一索引 | 很快 |
ref | 普通索引等值查 | 好 |
range | 索引范围扫描 | 可接受 |
index | 全索引扫描 | 偏差(扫整个索引) |
ALL | 全表扫描 | 最差,重点优化 |
看到 ALL 且 rows 很大,基本就是问题所在。但也要看数据量:几百行的小表全表扫完全没问题。
二、possible_keys 与 key
possible_keys:优化器认为可能可用的索引。key:实际选用的索引,为NULL表示没走索引。
两者不一致很常见:有可用索引但优化器算下来觉得全表更快(比如要扫 60% 的数据),这时 key 是 NULL、type 是 ALL。这种情况强行 FORCE INDEX 往往更慢,正确做法是改查询条件或调整索引。
三、rows 与 filtered
rows:预估需要扫描的行数(不是返回行数)。filtered:扫描后按条件过滤剩下的百分比。
rows × filtered% 约等于最终参与的行数。如果 rows 是 100 万而 filtered 只有 1%,说明索引没起到过滤作用,只是扫了一遍。
四、Extra 里的关键信息
| 值 | 含义 | 处理 |
|---|---|---|
Using index | 覆盖索引,不回表 | 好,保持 |
Using where | 存储引擎返回后再过滤 | 常见,看过滤比例 |
Using filesort | 需要额外排序 | 考虑给排序字段建索引 |
Using temporary | 用了临时表 | 多见于 GROUP BY / DISTINCT,尽量优化 |
Using index condition | 索引条件下推 | 好 |
Using join buffer | 联表没走索引 | 给关联字段补索引 |
Using filesort 不一定真的写磁盘,但确实要额外排序,数据量大时很慢。消除办法是让 ORDER BY 的列顺序与索引一致:
-- 索引 (user_id, created_at)
SELECT * FROM orders WHERE user_id = 7 ORDER BY created_at DESC LIMIT 20;
这样能直接按索引顺序取,不用排序。
Using temporary 常见于 GROUP BY 非索引列、DISTINCT 多列、UNION。能通过调整索引或改写 SQL 消除就消除,消不掉也要控制数据量。
五、一个完整的判断流程
EXPLAIN SELECT id, amount FROM orders
WHERE user_id = 7 AND status = 1
ORDER BY created_at DESC
LIMIT 20;
假设输出:type=ref、key=idx_user、rows=12000、Extra=Using where; Using filesort。
判断:
ref说明用上了索引,但只用到user_id。rows=12000偏大,说明该用户的订单很多。Using filesort说明排序没走索引。
优化:建联合索引 (user_id, status, created_at),让过滤和排序都在索引里完成,再配合覆盖索引把 amount 也带上,就能消除回表与排序。
六、EXPLAIN ANALYZE(MySQL 8.0.18+)
EXPLAIN ANALYZE SELECT ...;
它真正执行查询并给出实际耗时与行数,能和预估对比。预估 rows=100 实际 actual rows=100000 说明统计信息过期,可以 ANALYZE TABLE 更新。
注意它会真跑一遍,别在写操作或大表上随便用。
七、几个常见的误判
- 小表全表扫不是问题:几十行的配置表,
ALL比走索引还快。 rows是估算值:优化器基于统计信息,可能偏差很大。- 加了索引不代表会用:低区分度、隐式转换、函数运算都会让它失效。
- 只看一条 SQL 不够:要看整体 QPS 与并发,单条快但每秒跑一万次也会拖垮数据库。
小结
type 看访问方式(ALL 最差),key 看实际用了哪个索引,rows × filtered 看扫描量,Extra 里的 filesort/temporary 是优化重点;8.0 用 EXPLAIN ANALYZE 对比预估与实际。