同一条业务查询,写法稍有不同,性能可能相差几个数量级。差距往往不在 SQL 看起来多复杂,而在优化器最终选了哪条执行路径——走索引还是全表扫,用了哪个索引,是否回表,是否排序落盘。EXPLAIN 就是窥探这条路径的窗口。看不懂执行计划,优化就只能靠玄学试错;看懂了,慢 SQL 的病因往往一眼就能定位。
场景:加了索引为什么还是慢
工程师常有的困惑:明明在 WHERE 的字段上建了索引,查询却依旧慢。EXPLAIN 一看,type 是 ALL(全表扫描),索引压根没用上。原因可能是字段上做了函数运算、隐式类型转换、或 LIKE '%xx' 前导通配——这些都会让索引失效。执行计划不会骗人,它直接告诉你优化器到底怎么打算执行。
机制:读懂 EXPLAIN 的关键列
1 | EXPLAIN SELECT * FROM orders |
输出里真正决定性能的是这几列:
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 | Using index → 覆盖索引,无需回表,非常好 |
看到 Using filesort 和 Using temporary 要格外警觉,它们是高负载查询的常见元凶。
索引为什么会失效:最左前缀与回表
InnoDB 的二级索引是 B+ 树,叶子节点存的是索引列 + 主键值。理解两个核心概念,多数索引问题迎刃而解。
最左前缀原则。 联合索引 (a, b, c) 相当于按 a、再 b、再 c 排好序。查询必须从最左列开始连续匹配才能用上索引:
1 | -- 索引 (a, b, c) |
范围查询会"截断"后续列:WHERE a = 1 AND b > 5 AND c = 3 中,c 用不上索引排序,因为 b 是范围,其后的 c 在索引里不再有序。
回表与覆盖索引。 走二级索引找到主键后,若需要的列不在索引里,还得拿主键去聚簇索引再查一次,这叫回表。如果查询要的列全在索引里(Using index),就免了回表,性能大增。这就是覆盖索引的价值——把高频查询需要的列纳入联合索引,让查询"不回表"。
让索引失效的典型写法
1 | -- 字段上有函数/运算 → 失效 |
这些都源于同一原理:索引是有序结构,任何破坏"对索引列原值有序比较"的操作都会让有序性失效。 记住这条,就不用背诵失效清单了。
工程权衡与踩坑
统计信息不准导致选错索引。 优化器靠采样统计的基数(cardinality)估算成本。数据分布剧烈变化、大批量增删后,统计可能过时,优化器选了次优索引。手动 ANALYZE TABLE t 刷新统计常能立竿见影;个别场景可用 FORCE INDEX 强制,但这是双刃剑,数据分布一变又可能成为枷锁,应慎用。
索引不是越多越好。 每个索引都要在写入时维护,拖慢 INSERT/UPDATE,并占用空间。高频更新的表上滥建索引,写入会明显变慢。索引设计要服务于真实的高频查询,而非"给每个字段都来一个"。
深分页是隐藏杀手。 LIMIT 1000000, 10 会先扫描并丢弃前一百万行,代价巨大。优化思路是用"游标"——记住上次的最大主键,改成 WHERE id > last_id LIMIT 10,或先用覆盖索引定位主键再回表:
1 | -- 慢 |
EXPLAIN 的 rows 只是估算。 别把它当精确值,它是优化器基于统计的猜测。需要看真实执行情况时,用 EXPLAIN ANALYZE(实际执行并给出真实耗时与行数),定位 estimate 与 actual 的巨大偏差,往往能揪出统计信息问题。
小结
SQL 优化的核心是把执行路径从全表扫描引向高效的索引访问。看 EXPLAIN 先抓 type(有没有 ALL)、key(索引用没用上)、rows(扫多少行)、Extra(有没有 filesort / temporary)。索引失效的万变不离其宗——破坏了索引列的有序比较;用好覆盖索引免回表、遵守最左前缀、警惕深分页与函数运算。把"优化器在想什么"看明白,慢 SQL 的病灶大多无所遁形。