MySQL 慢查询排查实战

举报
yd_248971855 发表于 2026/10/29 09:57:43 2026/10/29
【摘要】 一次 MySQL 慢查询引发的接口超时:从 3 秒到 80 毫秒的优化实战一个列表页接口突然从 200ms 涨到 3 秒多,客户端频繁超时。最后定位到一个没走索引的隐式类型转换,加一个字段变更加联合索引,查询从 3.2 秒降到 80 毫秒。本文记录完整的定位过程,重点讲讲那个最容易被忽略的坑:隐式类型转换。环境:华为云 ECS(4vCPU/8GB,Ubuntu 22.04),自建 MySQL...

一次 MySQL 慢查询引发的接口超时:从 3 秒到 80 毫秒的优化实战

一个列表页接口突然从 200ms 涨到 3 秒多,客户端频繁超时。最后定位到一个没走索引的隐式类型转换,加一个字段变更加联合索引,查询从 3.2 秒降到 80 毫秒。本文记录完整的定位过程,重点讲讲那个最容易被忽略的坑:隐式类型转换。

环境:华为云 ECS(4vCPU/8GB,Ubuntu 22.04),自建 MySQL 8.0.36,库大小约 120GB,核心业务表 8000 万行。

一、问题出现

周五下午开始,运营反馈后台列表页加载极慢,翻了 5 页之后基本转圈。监控看板显示应用接口 P95 从平时的 200ms 一路涨到 3200ms。

第一反应先确认是应用层还是数据库层。两个快速判断点:

1. 应用服务器 CPU 正常,无 Full GC 堆积;
2. MySQL 所在 ECS 的 CPU 飙到 90%+,io 等待也高。

基本锁定数据库侧。上数据库看现场。

二、看现场

第一时间抓两个东西:当前正在跑的慢 SQL,以及慢查询日志里的高频条目。

SHOW PROCESSLIST 里能看到若干条 state 为 Sending data 的查询,执行时间都在 2-4 秒,SQL 形如:

SELECT id, order_no, user_id, amount, status, created_at
FROM orders
WHERE user_id = 13800138000
ORDER BY created_at DESC
LIMIT 20;

慢查询日志(long_query_time 设的是 1 秒)里,这一条在过去一小时出现了 400 多次。

初步怀疑 user_id 上没索引。但 SHOW INDEX FROM orders 一看,索引明明存在:

SHOW INDEX FROM orders;
-- KEY idx_user_created (user_id, created_at) 已存在

索引在,却没被用——这就是这次问题的核心。

三、EXPLAIN 揭示真相

把这条 SQL 拿去 EXPLAIN:

EXPLAIN SELECT id, order_no, user_id, amount, status, created_at
FROM orders
WHERE user_id = 13800138000
ORDER BY created_at DESC
LIMIT 20;

关键输出:

type: ALL
rows: 78,234,501
Extra: Using where; Using filesort

type 为 ALL 意味着全表扫描,8000 万行全扫,难怪 3 秒。

索引明明在,为什么优化器不用?注意 WHERE 条件里的字面量:13800138000。它没加引号,是个数字。而表结构里 user_id 是 varchar 类型!

DESC orders;
-- user_id varchar(32) NOT NULL

MySQL 里,字符串列与数字比较时,会发生隐式类型转换:把**列**转成数字去和数值比。转换作用在列上,就等于是对每一行做 CAST,索引直接失效。这就是“索引在却没用”的经典原因。

四、修复方案

最稳妥的修复分两步走。

第一步,立即止血:把 SQL 里的字面量改成字符串,让它走索引:

SELECT ... WHERE user_id = '13800138000' ORDER BY created_at DESC LIMIT 20;

改成字符串后 EXPLAIN:

type: ref
rows: 263
Extra: Using index condition

扫描行数从 8000 万降到 263 行。实测执行时间从 3200ms 降到 80ms。

第二步,根治:应用层代码修改。找到 ORM 或 MyBatis 的 mapper,把传参类型从 Long 改成 String,保证查询参数类型与列类型一致。这一步如果只改 SQL 不改代码,下次新同事写代码还会踩一次。

五、举一反三:同类问题的检查清单

这次是字符串列对数字。反过来的方向同样会踩坑,我把常见的隐式转换失效场景整理成清单,建议大家对照自查:

1. varchar 列与数字字面量比较(本文场景):user_id = 12345678901,索引失效;
2. int 列与字符串比较:amount = '100',MySQL 会把字符串转成数字,这个方向通常仍能走索引,但要注意 MySQL 8.0 的某些 join 场景下仍可能失效;
3. 隐式字符集转换:两表 join 时 utf8 和 utf8mb4 列关联,索引失效,这个在分库或老库改造时很常见;
4. 前置通配符:LIKE '%keyword%',这个不是隐式转换但同样是"索引在用不上",B+ 树没法用;
5. 列上套函数:DATE(created_at) = '2026-10-29',对列做运算导致索引失效,应改成范围条件 created_at >= '2026-10-29' AND created_at < '2026-10-29 2'。

六、如何提前发现:监控与规范

事后复盘,这个 SQL 上线一周了才出事,说明监控有盲区。补两个机制。

第一,开启并利用好慢查询日志。除了 long_query_time,配合 pt-query-digest 定期分析聚合,找出扫描行数异常大的语句:

pt-query-digest /var/log/mysql/slow.log | head -50

重点看每条语句的 Rows examine 与 Rows sent 的比值,比值大(比如扫了 8000 万行只返回 20 行)基本就是索引失效或索引不合理。

第二,上审核规范。用 pt-query-digest 或者公司内部的 SQL 审核平台,把"WHERE 条件类型与列类型必须一致"作为强制规则拦在上线前。8000 万行的表,一次全表扫描就是一次生产事故的预演。

七、复盘

时间线回顾:问题根因是 WHERE 条件参数类型与列类型不一致导致隐式类型转换,索引失效引发全表扫描。直接原因是应用层把手机号当 Long 传;深层原因是建表时用 varchar 存 user_id(手机号),而代码层又按数字处理,两边对同一字段的类型认知不统一。

修复动作:SQL 改传字符串(止血),代码改参数类型(根治),慢日志加监控(防复发)。

这类问题的讽刺之处在于:索引建了、监控上了、SQL 看着也没毛病——字面量是数字还是字符串,肉眼根本看不出区别,只有 EXPLAIN 会告诉你真相。所以团队里我一直推一条规矩:所有新 SQL 上线前必须附 EXPLAIN 结果,type 至少达到 ref 级别,ALL 一律打回。规矩虽然土,但省了不知道多少次凌晨告警。

本文基于真实排障过程整理,EXPLAIN 输出有简化。如果你的库也遇到过"索引在却全表扫"的情况,欢迎评论区交流你们踩过的隐式转换坑。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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