MySQL 慢查询从定位到治理:慢日志、EXPLAIN 与索引一次讲清

举报
yd_46858814 发表于 2026/11/08 19:43:53 2026/11/08
【摘要】 慢查询很少是突然变慢的。多数时候,它是数据量悄悄长大、索引慢慢失效、一条写得很随意的 SQL 被反复执行,最后在业务高峰时一起爆发。治理慢查询也不靠某一个神奇参数,而是一条链路:先定位,再看执行计划,然后改 SQL 或改索引,最后把复发拦在流程里。 一、先定位:别凭感觉猜是哪条 SQL第一步是让慢查询现形。打开慢日志是最直接的办法:SET GLOBAL slow_query_log = 'O...

慢查询很少是突然变慢的。多数时候,它是数据量悄悄长大、索引慢慢失效、一条写得很随意的 SQL 被反复执行,最后在业务高峰时一起爆发。治理慢查询也不靠某一个神奇参数,而是一条链路:先定位,再看执行计划,然后改 SQL 或改索引,最后把复发拦在流程里。

一、先定位:别凭感觉猜是哪条 SQL

第一步是让慢查询现形。打开慢日志是最直接的办法:

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = 'ON';

long_query_time = 1 表示执行超过 1 秒的记一条。初期可以设得宽一点,比如 2 秒,先把最严重的揪出来;稳定之后再收紧到 0.5 秒。log_queries_not_using_indexes 会把没走索引的查询也记下来,方便发现漏建索引的表,但要注意它在数据量很小的表上也会产生噪音,分析时要结合表大小一起看。

拿到慢日志之后不要逐行人肉读。用汇总工具按总耗时排序,先看累计耗时最高的几条,而不是单次最慢的那一条。一条跑 30 秒但一天只跑一次的 SQL,和一条跑 0.8 秒但每分钟跑几百次的 SQL,后者的总伤害往往更大。排序的维度建议固定下来:总耗时、平均耗时、执行次数、扫描行数。这四个数字放在一起,优先级基本就清楚了。

云上还有一个更省事的入口:数据库的性能监控与慢 SQL 统计页。它一般已经按执行次数和平均耗时排好序,还能看到慢查询出现的时间分布。如果慢查询集中在每天某个整点,大概率是定时任务或批量作业;如果均匀分布在全天,更可能是线上接口里常驻的坏 SQL。先看时间分布,再决定往哪个方向查。

二、再看执行计划:EXPLAIN 要读的其实只有几列

定位到具体 SQL 之后,下一步不是立刻加索引,而是先看它到底怎么执行的:

EXPLAIN SELECT * FROM orders
WHERE user_id = 1001 AND status = 1
ORDER BY created_at DESC LIMIT 20;

EXPLAIN 输出列很多,日常排查抓住这几列就够了:

type:访问方式。 从好到差大致是 const、eq_ref、ref、range、index、ALL。看到 ALL 就是全表扫描,先打个问号;看到 index 是全索引扫描,也不一定就是好的,可能是用索引当表来扫。目标是把高频查询推到 ref 或 range 这一档。

key 和 key_len:实际用了哪个索引,用了多长。 key 为空说明没走索引。key_len 可以帮你判断复合索引用了前几列:如果索引是 (user_id, status, created_at),而 key_len 只覆盖了 user_id,说明后面的列没用上,可能是查询条件写法的问题。

rows:预估要扫描的行数。 这是估算值,不是精确值,但量级很有参考意义。rows 是几十和几万,完全是两个世界。如果 rows 很大而最终只返回几行,说明过滤效率很低。

Extra:额外信息。 看到 Using filesort 说明排序没走索引;看到 Using temporary 说明用了临时表;看到 Using index 是好消息,说明覆盖索引生效,不需要回表。这三项是判断要不要动索引的最直接信号。

一个常见的误区是只看 type 是 range 就放心了。range 加上 rows 几十万,依然可能很慢。执行计划要和扫描行数、返回行数一起读。

三、最常见的五种慢法,对照着改

1. 索引建了,但没用上

最典型的是在索引列上做函数或运算:

-- 慢:对 created_at 做了函数,索引失效
WHERE DATE(created_at) = '2026-11-07'

-- 快:改成范围条件,索引能用上
WHERE created_at >= '2026-11-07 00:00:00'
  AND created_at < '2026-11-08 00:00:00'

隐式类型转换也一样致命。字段是 varchar,查询时传了数字,MySQL 会把字段值逐行转类型,索引直接失效。字符串字段就用字符串查,写 SQL 时顺手核对一下类型。

2. 复合索引只用了最左边的列

复合索引 (user_id, status, created_at) 能服务 WHERE user_id = ? 和 WHERE user_id = ? AND status = ?,但服务不了只有 WHERE status = ? 的查询,因为它跳过了最左列。建索引之前先把高频查询的 WHERE 条件列出来,按出现频率排个序,最常用的列放最前面。不要为每个单列各建一个索引,让优化器去猜;一个设计好的复合索引往往比三个单列索引更管用。

3. LIKE 以百分号开头

LIKE '%关键词%' 走不了 B 树索引,只能全表扫描。如果业务上确实需要模糊搜索,考虑全文索引,或者把搜索下沉到专门的检索引擎。实在要在数据库里做,至少把范围先缩小:先用能走索引的条件过滤出一个小集合,再在这个集合里做模糊匹配。

4. 深分页越翻越慢

-- 翻到第 10 万页时,前面 100 万行都要先扫一遍再丢掉
SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;

改成游标分页,用上一页最后一条的 id 做起点:

SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 20;

前提是排序字段单调递增且有索引。列表页、导出任务、后台翻页都适合这种改法,改完之后翻页耗时基本不再随页码增长。

5. 循环里查数据库

接口里一个 for 循环调几百次单条查询,是典型的 N+1 问题。改成一次 IN 查询把数据取回来,在内存里按 key 组织成映射,再去填充结果。批量查询加批量写入,是所有高频接口都应该先检查的习惯。

四、索引不是越多越好

加索引有代价:每次写入都要维护索引,索引本身也占空间。治理时顺带做一次清理:

  • 长时间没被用到的索引可以下线,先在测试环境验证,再到线上操作。
  • 重复索引和前缀重复的索引要合并,比如已经有 (a, b),再建一个单独的 (a) 往往是多余的。
  • 区分度很低的列,比如只有两三个取值的状态字段,单独建索引意义不大,放在复合索引的后面列更合适。

上线新索引的顺序也有讲究:先在备库或测试环境验证执行计划确实变好了,再上生产;大表加索引要选低峰期,避免长时间锁表拖垮业务。

五、把复发拦住:治理是一次流程,不是一次救火

单次优化做完,慢查询还会回来,除非把它变成固定流程:

  1. 每周看一次慢 SQL 榜单。 只看新增的和排名上升的,老的、已经确认无害的不要反复讨论。
  2. 上线前看一眼执行计划。 涉及核心表的 SQL 变更、新增查询条件、改分页逻辑,发布前跑一遍 EXPLAIN,rows 量级不对就先拦下来。
  3. 给长查询设上限。 超过阈值的查询及时终止并告警,避免一条坏 SQL 把连接池和 CPU 一起拖垮。连接数打满的问题,很多时候根子就是几条长时间不释放的慢查询。
  4. 慢查询和业务变更一起复盘。 新功能上线后慢查询榜单变了,先问最近改了什么,再问数据库怎么了。多数慢查询的源头在代码,不在数据库参数。

自查清单

  • [ ] 慢日志已开启,阈值与统计口径固定
  • [ ] 本周新增慢 SQL 已按总耗时排过序
  • [ ] 高频 SQL 的 EXPLAIN 已读过,type、key、rows、Extra 四列无异常
  • [ ] 索引列上没有函数、运算与隐式类型转换
  • [ ] 深分页已改成游标分页,循环查询已改成批量查询
  • [ ] 无用与重复索引已清理,大表加索引走低峰期
  • [ ] 慢查询周报与上线前 EXPLAIN 检查已形成固定动作

慢查询治理没有银弹,但有固定打法:定位靠慢日志与耗时排序,诊断靠 EXPLAIN 的四列,修复靠改 SQL 与改索引,收尾靠流程。把这套打法跑上几周,慢查询榜单会自己变短。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。