返回首页

MySQL 慢查询排查:从现象到 EXPLAIN 的实战路径

慢查询优化不要一上来就加索引。本文按现象确认、SQL 定位、EXPLAIN 分析、索引改造和回归验证的顺序,梳理一套 MySQL 慢查询排查流程。

44 约 4 分钟 · 3124 字 后端

慢查询不是一句“加索引”能解决

MySQL 慢查询最常见的误区是:看到接口慢,马上给 where 字段加索引。

有时这确实有效,但更多时候问题没这么简单:

  • SQL 写法导致索引用不上
  • where 条件选择性太差
  • order by 和 limit 触发 filesort
  • join 顺序不合理
  • 查询返回字段太多
  • 应用层一次发了太多查询
  • 慢的不是 SQL,而是锁等待或网络传输

所以慢查询排查要按流程来,先确认现象,再定位 SQL,最后才谈改索引。

第一步:确认慢在哪里

用户说“页面很慢”,不等于数据库慢。

先拆成几段:

浏览器渲染时间

接口响应时间

后端业务处理时间

数据库查询时间

网络传输时间

如果接口本身 80ms,页面 3 秒才可交互,那是前端问题。
如果接口 5 秒,但 SQL 只有 20ms,可能是后端循环、第三方接口或文件处理。
如果接口 5 秒,SQL 占 4.8 秒,再进入数据库排查。

不要跳过这个步骤。否则很容易优化了半天索引,结果瓶颈在别处。

第二步:找到具体 SQL

慢查询需要定位到具体 SQL,不能只看接口名。

常用方法:

  • 打开 MySQL slow query log
  • 在应用日志里记录 SQL 耗时
  • 对可疑接口加临时计时
  • 查看数据库监控里的 Top SQL

记录 SQL 时最好同时记录:

  • SQL 模板
  • 参数
  • 执行耗时
  • 返回行数
  • 调用接口
  • 用户或请求 id

只知道“某接口慢”不够,因为一个接口里可能有多条 SQL。

第三步:用 EXPLAIN 看执行计划

拿到 SQL 后,先跑:

EXPLAIN SELECT ...

重点看这几个字段:

字段 关注点
type 访问类型,ALL 通常表示全表扫描
possible_keys 理论上可用的索引
key 实际使用的索引
rows 预计扫描行数
filtered 条件过滤比例
Extra 是否 Using filesort / Using temporary

一个典型问题:

type: ALL
key: NULL
rows: 500000
Extra: Using where

这说明 MySQL 可能在扫全表。

但看到 ALL 也不要立刻恐慌。如果表只有几百行,全表扫描可能比走索引还快。优化要看数据量和真实耗时。

第四步:检查 where 条件

索引能不能用上,很大程度取决于 where 条件。

常见导致索引失效的写法:

WHERE DATE(created_at) = '2026-06-08'

函数包住了索引列,可能导致索引用不上。更好的写法:

WHERE created_at >= '2026-06-08 00:00:00'
  AND created_at <  '2026-06-09 00:00:00'

另一个常见问题是前置通配:

WHERE title LIKE '%nuxt%'

普通 BTree 索引很难支持这种查询。此时要考虑全文索引、搜索引擎或降低需求范围。

第五步:设计联合索引

单列索引不是万能的。真实查询经常是:

SELECT id, title, published_at
FROM posts
WHERE status = 'published'
  AND category_id = 3
ORDER BY published_at DESC
LIMIT 20

这类 SQL 可以考虑联合索引:

CREATE INDEX idx_posts_status_category_published
ON posts (status, category_id, published_at);

联合索引设计要看:

  • 等值条件放前面
  • 范围条件通常放后面
  • order by 能否顺着索引走
  • 选择性太低的字段单独建索引意义不大

不要机械背“最左前缀”,要结合具体 SQL 看执行计划。

第六步:减少返回和扫描

有时慢不是因为 where,而是因为返回太多。

坏例子:

SELECT *
FROM access_logs
ORDER BY created_at DESC
LIMIT 1000;

如果 request_bodyuser_agent 这类字段很长,SELECT * 会带来额外 IO 和网络传输。

更好的做法:

SELECT id, ip, method, path, status_code, response_time_ms, created_at
FROM access_logs
ORDER BY created_at DESC
LIMIT 100;

原则:

  • 只查页面真正需要的字段
  • 列表页不要带详情大字段
  • 分页 limit 不要过大
  • 详情页再查详情

第七步:关注排序和分页

分页越往后越慢,常见于:

SELECT id, title
FROM posts
ORDER BY created_at DESC
LIMIT 100000, 20;

MySQL 需要跳过前面 100000 行,成本很高。

如果业务允许,可以用游标分页:

SELECT id, title, created_at
FROM posts
WHERE created_at < ?
ORDER BY created_at DESC
LIMIT 20;

后台管理列表通常还能接受传统分页,但高流量前台列表或无限滚动更适合游标分页。

第八步:验证而不是凭感觉

改完索引或 SQL 后,至少验证:

  • EXPLAIN 是否使用预期索引
  • 扫描 rows 是否下降
  • 实际耗时是否下降
  • 返回结果是否一致
  • 写入性能是否受影响
  • 是否影响其他查询

索引不是免费午餐。它会增加写入成本和存储空间。小表可以少建索引,大表要谨慎建对。

一个排查清单

遇到慢查询,按这个顺序看:

  1. 慢的是页面、接口还是 SQL?
  2. 具体慢 SQL 是哪一条?
  3. SQL 参数和返回行数是多少?
  4. EXPLAIN 的 type、key、rows、Extra 怎么样?
  5. where 条件是否对索引列做了函数或隐式转换?
  6. 是否需要联合索引而不是单列索引?
  7. order by / limit 是否能利用索引?
  8. 是否 SELECT 了不必要的大字段?
  9. 深分页是否需要改成游标分页?
  10. 改完后是否做了对照验证?

总结

慢查询优化的核心不是“多建索引”,而是减少不必要的扫描、排序和传输。

先定位 SQL,再看执行计划,再根据查询模式设计索引和改写 SQL。每一步都要验证,不要靠感觉。这样做虽然慢一点,但能避免把数据库优化成一堆没人敢删的索引。


下次见。

-EOF-