Article
MySQL 索引优化:从慢查询到可解释的执行计划
MySQL 索引优化:从慢查询到可解释的执行计划
慢查询很少是「MySQL 突然抽风」。更常见的是查询形状与索引形状不匹配:该走索引的条件写在表达式里,排序字段不在联合索引尾部,或者索引建了一堆却从未被优化器选中。优化的第一步不是盲加索引,而是让每条慢 SQL 变得可解释。对博客这类读多写少系统,把列表与归档相关的几条 SQL 吃透,往往比空谈「分库分表」有用得多。
先把慢查询捞出来
打开慢查询日志,并设定适合业务的阈值。开发环境可以把阈值调低,方便暴露问题;生产环境要避免日志把磁盘打满,并做好轮转。记录里至少要能定位:SQL 文本、执行时长、扫描行数、时间戳。如果有 performance_schema,也可以看哪些语句总耗时最高,而不只是「单次最慢」。
拿到 SQL 后立刻跑 EXPLAIN(有条件再跑 EXPLAIN ANALYZE)。关注这些信号:
type=ALL:全表扫描,通常最危险key为空:没有用到你以为存在的索引rows很大:预估扫描量高,往往伴随响应抖动Extra出现Using filesort/Using temporary:排序或分组成本偏高
不要只看「有没有 Using index」。覆盖索引很好,但更关键的是优化器是否走了正确路径,以及实际耗时是否下降。EXPLAIN 是预估,EXPLAIN ANALYZE(高版本)能看到实际执行;两者不一致时,以实际为准,并怀疑统计信息过旧——必要时 ANALYZE TABLE。
条件写法会直接废掉索引
在列上包函数,是最高频的失误:
-- 很难走 created_at 索引
WHERE DATE(created_at) = '2026-08-01'
-- 改成范围,才能用上 BTree
WHERE created_at >= '2026-08-01' AND created_at < '2026-08-02'
隐式类型转换同样危险。把数字列和字符串比较、或在索引列上做 CAST,优化器可能放弃索引。写 SQL 时尽量让「列保持原样,常量去迁就列的类型」。
左右模糊同理:LIKE '%keyword' 无法利用 BTree 前缀;真要做标题搜索,要么接受额外方案(全文索引、外部搜索),要么做冗余检索字段,而不是幻想「再建一个普通索引就好」。前缀模糊 LIKE 'Go%' 可以走索引,但要注意前缀区分度;区分度太差时,优化器仍可能选全表。
另一个常见误区是给低基数列单独建索引,比如只有两三个值的 status。单独索引往往选择性很差;更合理的是把高过滤条件放前面,做联合索引,例如文章列表的 (status, published_at)。你也可以用 SHOW INDEX / 统计信息估算基数,避免凭感觉建「心理安慰索引」。
联合索引要按查询形状设计
联合索引不是字段的简单堆叠,顺序决定了它能服务哪些查询。经验规则是:等值条件 → 范围条件 → 排序字段。和你的 WHERE / ORDER BY 对齐,比「把所有过滤列都塞进去」有效得多。
以博客列表为例:
SELECT id, title, slug, summary, published_at FROM articles
WHERE status = 'published'
ORDER BY published_at DESC
LIMIT 10;
更合适的索引常常是 (status, published_at)。如果你经常按分类过滤,再评估 (category_id, status, published_at) 是否值得,而不是无脑并行建三四个近似索引。注意 SELECT * 会迫使回表拿大字段(尤其是 Markdown 正文);列表接口显式挑列,既能减少 IO,也更可能贴近覆盖索引。
范围条件会截断后续索引列的使用。假如 WHERE status=? AND published_at>? ORDER BY id,把排序字段错放到范围列前面,filesort 可能依然出现。设计索引时把「最常见的那条 SQL」贴在旁边对照,比抽象口诀可靠。
索引是有成本的:写入更慢、占用空间、优化器候选变多。每次新增都要问:它打掉的是哪条真实慢查询?有没有重复覆盖?没用的索引要删,删之前确认没有被其它报表或后台任务依赖。
用验证闭环代替直觉
改索引之后,不要只看 EXPLAIN 变好看。用相同的数据量对比:
- 执行计划是否选择了预期索引
- 实际耗时是否下降
- 写入路径(发布文章、更新评论)是否明显变慢
- 在接近生产的数据量上复测(空表上再漂亮的计划也没有意义)
有条件在从库或备份实例验证;生产高峰期直接大改索引,可能被在线 DDL 拖住。大表变更要评估锁表策略与执行窗口,并准备好观察写入延迟。改完持续盯一段时间慢查询,确认没有「优化了 A、打爆了 B」。
应用层也能少制造慢查询
ORM 很方便,也很容易在列表页打出 N+1。文章列表循环取标签、作者,如果不预加载,数据库会收到一连串小查询,慢查询日志不一定立刻标红,但整体延迟会变差。列表接口只加载展示所需字段;详情页再加载重关联。分页务必带稳定排序,避免「翻页跳数据」。
缓存可以锦上添花,但不能拿来掩盖错误索引。热点文章可以短缓存,但失效策略、穿透保护都要一并考虑;流量不大时,正确索引通常比过早缓存更划算。先把 SQL 变老实,再谈 Redis,顺序反了会多维护一套「脏缓存」问题。
一个可复用的排查顺序
- 复现慢请求,拿到完整 SQL
EXPLAIN/EXPLAIN ANALYZE看访问类型与关键路径- 检查条件是否对列做了函数/隐式转换
- 对照最频繁查询设计或调整联合索引
- 限制返回列,消除 N+1
- 对比改前改后的耗时与写入影响
把这个顺序写成团队习惯,比收藏十篇「索引最佳实践」更能在半夜排障时救命。
小结
索引优化的本质,是让「业务真正发出去的 SQL」与「数据结构」互相认识。先记录慢查询,再用执行计划解释它,最后用最小索引集合去匹配查询形状,并在改动后做可量化验证。把这个闭环做熟,比背十个「最佳实践口诀」更能解决真实问题。博客表小不可笑:习惯在小表上养成,表变大时你才不会第一次就在生产赌运气。