同一条业务查询,写法稍有不同,性能可能相差几个数量级。差距往往不在 SQL 看起来多复杂,而在优化器最终选了哪条执行路径——走索引还是全表扫,用了哪个索引,是否回表,是否排序落盘。EXPLAIN 就是窥探这条路径的窗口。看不懂执行计划,优化就只能靠玄学试错;看懂了,慢 SQL 的病因往往一眼就能定位。

场景:加了索引为什么还是慢

工程师常有的困惑:明明在 WHERE 的字段上建了索引,查询却依旧慢。EXPLAIN 一看,typeALL(全表扫描),索引压根没用上。原因可能是字段上做了函数运算、隐式类型转换、或 LIKE '%xx' 前导通配——这些都会让索引失效。执行计划不会骗人,它直接告诉你优化器到底怎么打算执行。

机制:读懂 EXPLAIN 的关键列

1
2
3
EXPLAIN SELECT * FROM orders
WHERE user_id = 100 AND status = 'paid'
ORDER BY created_at DESC LIMIT 10;

输出里真正决定性能的是这几列:

type —— 访问类型,最该先看的列。 从好到坏大致是:

1
system > const > eq_ref > ref > range > index > ALL
  • const:主键/唯一索引等值查询,最多一行,最快。
  • ref:普通索引等值查询,可能多行。
  • range:索引范围扫描(BETWEEN>IN)。
  • index:扫整棵索引树(比 ALL 略好,但仍扫全部)。
  • ALL:全表扫描,数据量大时的灾难,优化的首要目标是消灭它。

key 与 possible_keys。 possible_keys 是优化器考虑过的索引,key最终选用的。两者不一致,甚至 key 为 NULL,说明索引没被有效利用。

rows 与 filtered。 rows 是优化器估算要扫描的行数(基于统计信息,不是精确值);filtered 是过滤后剩余行的百分比。rows × filtered 约等于真正参与后续操作的行数。rows 很大是慢的强信号。

Extra —— 隐藏的成本所在。 这一列的文字提示往往是性能瓶颈的真凶:

1
2
3
4
5
Using index          → 覆盖索引,无需回表,非常好
Using where → 存储引擎返回后还要在 Server 层过滤
Using filesort → 排序无法用索引完成,需额外排序(可能落盘),警惕
Using temporary → 用了临时表(常见于 GROUP BY/DISTINCT),警惕
Using index condition→ 索引下推(ICP),在引擎层提前过滤,好事

看到 Using filesortUsing temporary 要格外警觉,它们是高负载查询的常见元凶。

索引为什么会失效:最左前缀与回表

InnoDB 的二级索引是 B+ 树,叶子节点存的是索引列 + 主键值。理解两个核心概念,多数索引问题迎刃而解。

最左前缀原则。 联合索引 (a, b, c) 相当于按 a、再 b、再 c 排好序。查询必须从最左列开始连续匹配才能用上索引:

1
2
3
4
5
-- 索引 (a, b, c)
WHERE a = 1 AND b = 2 AND c = 3 -- 全用上
WHERE a = 1 AND b = 2 -- 用上 a, b
WHERE a = 1 AND c = 3 -- 只用上 a,c 用不上(b 断了)
WHERE b = 2 AND c = 3 -- 完全用不上(缺最左的 a)

范围查询会"截断"后续列:WHERE a = 1 AND b > 5 AND c = 3 中,c 用不上索引排序,因为 b 是范围,其后的 c 在索引里不再有序。

回表与覆盖索引。 走二级索引找到主键后,若需要的列不在索引里,还得拿主键去聚簇索引再查一次,这叫回表。如果查询要的列全在索引里(Using index),就免了回表,性能大增。这就是覆盖索引的价值——把高频查询需要的列纳入联合索引,让查询"不回表"。

让索引失效的典型写法

1
2
3
4
5
6
7
8
9
10
11
-- 字段上有函数/运算 → 失效
WHERE YEAR(created_at) = 2025 -- 改写成 created_at >= '2025-01-01' AND < '2026-01-01'

-- 隐式类型转换 → 失效(phone 是 varchar 却传数字)
WHERE phone = 13800138000 -- 应传字符串 '13800138000'

-- 前导通配 → 失效
WHERE name LIKE '%张' -- 后导 '张%' 才能用索引

-- OR 连接非索引列 → 可能全表扫
WHERE indexed_col = 1 OR no_index_col = 2

这些都源于同一原理:索引是有序结构,任何破坏"对索引列原值有序比较"的操作都会让有序性失效。 记住这条,就不用背诵失效清单了。

工程权衡与踩坑

统计信息不准导致选错索引。 优化器靠采样统计的基数(cardinality)估算成本。数据分布剧烈变化、大批量增删后,统计可能过时,优化器选了次优索引。手动 ANALYZE TABLE t 刷新统计常能立竿见影;个别场景可用 FORCE INDEX 强制,但这是双刃剑,数据分布一变又可能成为枷锁,应慎用。

索引不是越多越好。 每个索引都要在写入时维护,拖慢 INSERT/UPDATE,并占用空间。高频更新的表上滥建索引,写入会明显变慢。索引设计要服务于真实的高频查询,而非"给每个字段都来一个"。

深分页是隐藏杀手。 LIMIT 1000000, 10 会先扫描并丢弃前一百万行,代价巨大。优化思路是用"游标"——记住上次的最大主键,改成 WHERE id > last_id LIMIT 10,或先用覆盖索引定位主键再回表:

1
2
3
4
5
6
-- 慢
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;
-- 快(延迟关联)
SELECT o.* FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 10) t
ON o.id = t.id;

EXPLAIN 的 rows 只是估算。 别把它当精确值,它是优化器基于统计的猜测。需要看真实执行情况时,用 EXPLAIN ANALYZE(实际执行并给出真实耗时与行数),定位 estimate 与 actual 的巨大偏差,往往能揪出统计信息问题。

小结

SQL 优化的核心是把执行路径从全表扫描引向高效的索引访问。看 EXPLAIN 先抓 type(有没有 ALL)、key(索引用没用上)、rows(扫多少行)、Extra(有没有 filesort / temporary)。索引失效的万变不离其宗——破坏了索引列的有序比较;用好覆盖索引免回表、遵守最左前缀、警惕深分页与函数运算。把"优化器在想什么"看明白,慢 SQL 的病灶大多无所遁形。