
写 MySQL 的这些年,我越来越觉得:慢查询不是某一个人写坏了 SQL,而是大家都以为没问题。一个 where 条件拼错位置,一个 like 写成 %xxx,一个字段没建索引,这些东西在 QPS 低的时候一点感觉都没有,但线上一旦达到几百 QPS,RT 就能轻松飙到 200ms 以上。尤其是一次线上事故让我彻底长记性:一个看似无害的模糊查询,晚上 2 点把整个订单库打成了 90 CPU。那天值班的是我,凌晨三点我坐在工位上盯着 explain,看着扫描行数从几十万跳到几百万,那感觉真的忘不了。
平时大家总说“看下慢 SQL 啊”。但到底怎么查?怎么分析?应该怎么改?很多新人可能只知道 explain,有时候看着 explain 的结果都不知道哪里不对。这里我就把自己这些年踩坑得来的经验,连同一些真实案例组装成一次“从慢查询日志看到最终优化”的完整流程,希望对大家有所帮助。
有些同事喜欢凭感觉,说“感觉这个 SQL 有点慢”。线上可不是这么判断,慢查询是有明确指标的,MySQL 默认认为执行超过 10 秒的 SQL 叫慢查询。生产一般都会调到 1 秒以下,甚至 300ms。我们有个项目高峰 QPS 在 600 左右,慢查询阈值是 200ms,超过这个值必定要查。
这里顺便说一句:不要把阈值设太低,否则一天能给你刷上几千条慢日志,运维直接骂你。
很多团队线上环境居然没开慢查询日志,这真的会害死排查。打开方式很简单,只要在 my.cnf 加两行就够了:
slow_query_log=1
long_query_time=0.5也可以直接 SQL 动态开启:
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 在线上执行的是怎样的参数组合、发生了几次、占了多大比例。正确做法应该是这样的:
为什么强调真实参数?因为很多 SQL 在不同参数下会走不同索引。我踩过最恶心的一个坑:where user_id in (...),用户少的时候走索引,in 列表大到几十个的时候直接把我拖回全表扫,导致扫描行数从 50 行变成 20 万,RT 从 5ms 变成 150ms。
很多 explain 字段其实根本不用全部背,我工作里最常看的无非这几个:type、key、rows、extra。
你只要记住一点:type 值的质量排序 roughly 是这样排的:
const > eq_ref > ref > range > index > ALL看见 ALL 基本就是全表扫。某次事故就是因为 type=ALL,直接扫 40 万行。
这个地方坑特别多,其中我最讨厌看到的就是:
Using temporary
Using filesort文件排序意味着你 order by 的字段没有索引或者索引没命中,线上一旦遇到并发,这种 SQL 轻轻松松把磁盘 IOPS 干冒烟。
索引失效一般都不是“天灾”,都是人祸,而且大家常犯的几种场景非常固定,列几个我经常遇到的:
这种写法千万别用,线上 90 CPU 那次,我们的订单搜索接口就是因为 like '%keyword%',type 直接变 ALL。
正确写法一般是:
想全部匹配就必须用索引不适合的方案,千万别强行用 MySQL。
where date(create_time) = '2024-01-01'这类写法直接让 MySQL 放弃索引,因为你对 create_time 做了函数计算。
正确写法:
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 是:
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 倍。表结构设计永远不是无关紧要的。字段长度越短,索引越小,缓存越容易命中。
讲一个我亲手优化的例子,印象特别深。线上报警:某用户查询接口 RT 频繁超过 800ms。慢日志抓到 SQL:
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) 的组合索引。最终方案是:
改完后:
type=range rows=50 extra=Using index,RT 直接从 800ms 掉到 8ms,并发高峰时 CPU 占用下降了 30。
加索引是把读优化到极致,但写操作会越来越慢。如果你的业务是写多读少,那别一股脑给字段加索引。我遇到过一个团队,一个订单表建了 15 个索引,写入每秒只有区区 100 左右,后来删成 7 个索引之后,写入性能直接翻倍。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。