MySQL 慢查询到底怎么排查?
我遇到过很多不会排查的人,一上来就把 SQL 拿去 explain,但根本不知道这个 SQL 在线上执行的是怎样的参数组合、发生了几次、占了多大比例。看慢日志,确认 SQL 样子拿真实参数还原 SQLexplain 看执行计划看索引结构、字段类型改 SQL 或加索引再 explain 验证测试库跑压力验证为什么强调真实参数?因为很多 SQL 在不同参数下会走不同索引。
平时大家总说“看下慢 SQL 啊”。但到底怎么查?怎么分析?应该怎么改?很多新人可能只知道 explain,有时候看着 explain 的结果都不知道哪里不对。这里我就把自己这些年踩坑得来的经验,连同一些真实案例组装成一次“从慢查询日志看到最终优化”的完整流程,希望对大家有所帮助。
到底什么算慢查询?别再用感觉判断了
有些同事喜欢凭感觉,说“感觉这个 SQL 有点慢”。线上可不是这么判断,慢查询是有明确指标的,MySQL 默认认为执行超过 10 秒的 SQL 叫慢查询。生产一般都会调到 1 秒以下,甚至 300ms。我们有个项目高峰 QPS 在 600 左右,慢查询阈值是 200ms,超过这个值必定要查。
这里顺便说一句:不要把阈值设太低,否则一天能给你刷上几千条慢日志,运维直接骂你。
慢查询日志怎么开?不开就等着线上揣摩人生
很多团队线上环境居然没开慢查询日志,这真的会害死排查。打开方式很简单,只要在 my.cnf 加两行就够了:
代码语言:ini
AI代码解释
slow_query_log=1
long_query_time=0.5
也可以直接 SQL 动态开启:
代码语言:sql
AI代码解释
set global slow_query_log=1;
set global long_query_time=0.5;
记住:long_query_time 不会精确到小数,只能写成 0.5,但 MySQL 实际会记录超过 0.5 秒的。有慢日志之后,你能看到每条 SQL 的执行时间、扫描行数、锁等待时间,这些比你凭感觉靠谱太多。
排查慢 SQL,我一般这样走:先日志再 explain 最后才去改
我遇到过很多不会排查的人,一上来就把 SQL 拿去 explain,但根本不知道这个 SQL 在线上执行的是怎样的参数组合、发生了几次、占了多大比例。正确做法应该是这样的:
- 看慢日志,确认 SQL 样子
- 拿真实参数还原 SQL
- explain 看执行计划
- 看索引结构、字段类型
- 改 SQL 或加索引
- 再 explain 验证
- 测试库跑压力验证
为什么强调真实参数?因为很多 SQL 在不同参数下会走不同索引。我踩过最恶心的一个坑:where user_id in (...),用户少的时候走索引,in 列表大到几十个的时候直接把我拖回全表扫,导致扫描行数从 50 行变成 20 万,RT 从 5ms 变成 150ms。
explain 到底怎么看?哪些字段才是重点?
很多 explain 字段其实根本不用全部背,我工作里最常看的无非这几个:type、key、rows、extra。
type 指查询方式,越往右越垃圾
你只要记住一点:type 值的质量排序 roughly 是这样排的:
代码语言:txt
AI代码解释
const > eq_ref > ref > range > index > ALL
看见 ALL 基本就是全表扫。某次事故就是因为 type=ALL,直接扫 40 万行。
extra 里面的“Using filesort”“Using temporary”都不是什么好词
这个地方坑特别多,其中我最讨厌看到的就是:
代码语言:txt
AI代码解释
Using temporary
Using filesort
文件排序意味着你 order by 的字段没有索引或者索引没命中,线上一旦遇到并发,这种 SQL 轻轻松松把磁盘 IOPS 干冒烟。
这些 SQL 写法最容易让索引失效
索引失效一般都不是“天灾”,都是人祸,而且大家常犯的几种场景非常固定,列几个我经常遇到的:
like '%xxx' 带前缀百分号的,基本直接废掉索引
这种写法千万别用,线上 90 CPU 那次,我们的订单搜索接口就是因为 like '%keyword%',type 直接变 ALL。
正确写法一般是:
- 接入 ES
- 或者 like 'xxx%' 限制左匹配
想全部匹配就必须用索引不适合的方案,千万别强行用 MySQL。
字段计算导致索引无效
代码语言:sql
AI代码解释
where date(create_time) = '2024-01-01'
这类写法直接让 MySQL 放弃索引,因为你对 create_time 做了函数计算。
正确写法:
代码语言:sql
AI代码解释
where create_time >= '2024-01-01' and create_time < '2024-01-02'
左边不匹配,组合索引直接报废
假设你建了组合索引 (a, b, c),那么必须遵守最左前缀,比如 where b=xx 根本走不了。这是我见过最多人搞错的,包括经验很久的人都会偶尔忘。
范围查询为什么也会让索引用不满?
where a > 3 and b = 1。很多人以为 b 会继续命中索引,结果 explain type 却是 range,只用到了 (a) 部分。原因也很简单:范围查询会让后面的字段没办法做有序判断。
这就是为什么组合索引的顺序非常重要。如果你的查询是 order_id between xxx and yyy and user_id = xxx,那索引最好是 (order_id, user_id)。
我之前因为这个顺序写反了,线上扫描行数从几百跳到五万,整整排查了一个下午。
怎么选字段做索引?不是越多越好,写操作会哭
实际工作中,我选索引的几个经验:字段区分度高、高频过滤、经常出现在 where、排序、join 上的字段优先建索引;布尔字段、性别这种几乎无区分度的字段一般别建。还有一点很多人忽略:索引会大幅增加写入成本。我参与的一个项目订单表有 11 个索引,每次写入消耗都很高,后来删掉了两个没人用的索引,写入 RT 从 20ms 降到 7ms。
索引不是越多越好,这是真理。
覆盖索引不回表,为什么这么香?
解释之前先说一段实际踩坑。我曾经查一个用户表,SQL 是:
代码语言:sql
AI代码解释
select id, name from user where phone = 'xxxx'
phone 字段有索引,但是因为 name 不在二级索引里,所以 MySQL 会回表再查一次,rows 明明只有 1 行,但 IO 开销不小。后来我加了组合索引 (phone, name),直接覆盖索引,不需要回表,RT 从 5ms 降到 1ms。回表的问题就是:你只查一个值,却需要查两次存储结构,热点数据会慢得特别明显。
表结构设计和索引优化一定要一起看,不要分开搞
有些团队喜欢一个人写表结构,一个人写 SQL,一个人调索引,结果全乱套。有一次我们数据库里有个 varchar(200) 的手机号字段,索引就 200 字节宽,导致 BTREE 巨大。后来改成 varchar(20),索引大小直接缩 10 倍。表结构设计永远不是无关紧要的。字段长度越短,索引越小,缓存越容易命中。
真实的慢 SQL 优化案例:从 800ms 砍到 8ms
讲一个我亲手优化的例子,印象特别深。线上报警:某用户查询接口 RT 频繁超过 800ms。慢日志抓到 SQL:
代码语言:sql
AI代码解释
select * from order where user_id = 10001 and status = 1 order by create_time desc limit 20;
看似毫无问题对吧?但 explain 一看傻眼了:type=ALL rows=40万 extra=Using filesort
原因很简单:status 在 where 中排在 user_id 前面,导致走不上 (user_id, status, create_time) 的组合索引。最终方案是:
- 调整 SQL 字段顺序
- 补上合适顺序的组合索引
改完后:
type=range rows=50 extra=Using index,RT 直接从 800ms 掉到 8ms,并发高峰时 CPU 占用下降了 30。
加索引是把读优化到极致,但写操作会越来越慢。如果你的业务是写多读少,那别一股脑给字段加索引。我遇到过一个团队,一个订单表建了 15 个索引,写入每秒只有区区 100 左右,后来删成 7 个索引之后,写入性能直接翻倍
更多推荐

所有评论(0)