MySQL EXPLAIN:从执行计划定位索引问题
线上慢查询的处理,最容易走向“先加一个索引试试”。但索引是否真的被使用、使用的是哪个索引、扫描了多少行,不能靠猜。对于一条具体 SQL,EXPLAIN 给出的执行计划才是优化讨论的共同起点。
本文以 MySQL 常见的 InnoDB 场景为例,整理一套从执行计划到优化验证的排查方法。
先保留原始现场
拿到慢 SQL 时,不要立即改写。先记录完整 SQL、绑定参数、表结构、索引和数据量;同一条参数化 SQL 在不同参数下,优化器可能选择不同的路径。
例如有一张订单表:
1 | |
业务需要查询某个用户最近 30 天的已完成订单:
1 | |
先执行:
1 | |
MySQL 8.0 还可以使用 EXPLAIN ANALYZE 查看实际执行时间与实际行数;生产环境应先评估语句成本,避免直接对高负载的大范围查询执行。
读懂最关键的几列
type 表示访问方式,通常由好到坏可以大致理解为 const、eq_ref、ref、range、index、ALL。它不是唯一目标:一次返回大量数据的 range 扫描也可能很慢,但看到 ALL 时应优先确认是否发生了全表扫描。
possible_keys 是优化器认为可选的索引;key 是实际选择的索引;key_len 能帮助判断联合索引实际用了多少列;rows 是优化器预估扫描行数。还应关注 Extra:
Using index往往意味着查询被覆盖索引满足,不必回表。Using where只表示仍需条件过滤,本身不是错误。Using filesort表示排序不能直接利用索引顺序,不一定真的写磁盘,但值得检查。Using temporary常见于分组、排序等中间结果处理,在大数据量下需要重点关注。
不要把“没有 Using index”简单等同于慢。这里的 Using index 指覆盖索引;普通二级索引查到主键后再回表,同样可能是合理计划。
联合索引要匹配查询路径
上例中的 idx_user_created(user_id, created_at) 可以先用等值条件锁定用户,再在该用户的时间范围内扫描,并天然满足 created_at DESC 的排序方向。可是 status 不在索引中,数据库仍需要回表过滤状态。
如果业务中这类查询很常见,可根据实际选择性考虑:
1 | |
联合索引并非字段越多越好。一般将等值过滤列放在前面,范围列放在后面;一旦前面的列是范围条件,后续列通常难以继续用于缩小扫描范围。还要考虑索引维护成本、写入频率以及是否真的能减少读取行数。
创建索引前后都用相同参数执行 EXPLAIN,比较 key、rows 和 Extra。如果可用,使用 EXPLAIN ANALYZE 比较实际扫描行数,才算完成验证。
四类常见的索引失效
1. 在索引列上做函数或表达式
1 | |
这会让数据库先计算每行的 DATE(created_at),通常无法直接利用 created_at 的范围索引。改成半开区间:
1 | |
2. 隐式类型转换
若 user_id 是数字列,却传入带引号且内容不规范的字符串,或字符集、排序规则不一致,优化器可能无法按预期走索引。接口层应让参数类型与字段类型一致,并在慢日志中保留真实参数以便复现。
3. 前导通配符
1 | |
普通 B+ 树索引无法根据开头定位范围。若这是核心检索能力,应评估全文索引、搜索引擎或调整交互方式;不要误以为给 title 建普通索引就能解决包含搜索。
4. 低选择性字段单独建索引
status 只有几个取值时,单列索引可能要回表读取大量行,优化器选择全表扫描反而更便宜。这并不代表优化器“没用索引”。更有效的做法通常是把高选择性的业务条件与状态组合为联合索引,并根据真实查询模式排序。
用小闭环完成优化
一次可靠的优化应形成闭环:
- 从慢日志或监控中拿到真实 SQL 与参数。
- 查看表结构、索引基数和
EXPLAIN,确认主要扫描路径。 - 只针对明确原因调整 SQL 或索引,并评估写入代价。
- 用相同参数复测执行计划与耗时,观察扫描行数是否下降。
- 灰度后继续看慢日志、错误率和数据库负载,必要时回滚。
索引优化不是让每条 SQL 都出现某个“漂亮”的 type,而是让数据库以更少的读取和更稳定的代价得到业务真正需要的数据。先用执行计划确认事实,再做最小且可验证的改变,通常比批量加索引更安全。