前言

最近有球友去面试,面试官问:线上慢SQL问题导致CPU飙升,如何处理?

这就是典型的慢SQL拖垮整个应用的案例。

今天,我就带大家从排查、分析到优化,完整走一遍线上慢SQL导致CPU飙升的实战处理流程。

希望对你会有所帮助。

一、如何快速定位问题?

当你发现应用或数据库CPU飙升时,不要慌,按以下步骤快速锁定元凶。

1.1 确认数据库层面的CPU消耗

登录数据库服务器,使用top或htop查看MySQL进程的CPU占用率。

如果mysqld的CPU超过100%(多核),基本可以确定是数据库内部有消耗大的操作。

接着,进入MySQL命令行,执行:

SHOW PROCESSLIST;

重点关注Time(执行时间)和State(状态)列。如果出现大量Sending data、Copying to tmp table、Sorting result等状态的会话,且执行时间很长,大概率就是慢SQL。

还可以开启慢查询日志(如果没开的话):

SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;  -- 超过1秒记录

然后通过mysqldumpslow工具分析慢日志:

pt-query-digest /var/log/mysql/slow.log  # 或使用mysqldumpslow

1.2 拿到具体的SQL

从SHOW PROCESSLIST中复制出问题SQL,或者从慢日志中提取。

例如:

SELECT o.id, o.amount, u.name, p.title
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
LEFT JOIN products p ON o.product_id = p.id
WHERE o.status = 'PAID'
  AND o.create_time > '2026-01-01'
ORDER BY o.amount DESC
LIMIT 1000;

二、为什么会慢?

慢SQL导致CPU飙升的根本原因是:数据库需要耗费大量CPU资源去执行全表扫描、排序、创建临时表等操作

2.1 执行计划分析

使用EXPLAIN查看SQL的执行计划:

EXPLAIN SELECT ...

关键列:

  1. type:ALL表示全表扫描(最差),range或ref表示用到索引。
  2. rows:估算扫描行数,越大越慢。
  3. Extra:Using filesort(文件排序)、Using temporary(临时表)都很耗CPU。

2.2 为什么全表扫描会飙高CPU?

  1. MySQL要逐行读取数据页到内存,然后逐行过滤条件。
  2. 如果表很大(千万级),即使内存足够,也要扫描大量数据,CPU忙于解析和比较。
  3. 排序、分组等操作需要额外内存和CPU计算。

2.3 索引失效的常见场景

  1. 对索引列使用函数(WHERE DATE(create_time) = '2026-01-01')
  2. 隐式类型转换(WHERE user_id = '123',但user_id是数值类型)
  3. 使用!=或<>
  4. LIKE '%abc'(前缀模糊匹配)
  5. OR 条件中有非索引列

三、紧急处理措施(止血)

在找到根本原因之前,先要让系统恢复可用,避免故障扩大。

3.1 杀掉慢查询

SHOW PROCESSLIST;
-- 找到Id列和执行时间长的会话,执行:
KILL <Id>;

可以写脚本定时kill超过某阈值的SQL。

3.2 临时限流

在应用层对可疑接口进行限流,例如使用Sentinel或Hystrix降低该接口的并发。

3.3 重启数据库? 不推荐

重启会清空buffer pool,可能导致启动后更慢。除非无法连接。

四、根治手段:优化SQL与索引

4.1 添加合适的索引

针对上述例子,我们分析:

  1. WHERE o.status = 'PAID' AND o.create_time > '2026-01-01',可以考虑联合索引(status, create_time)。
  2. 排序字段amount,看能否加到索引中避免filesort。

优化后:

ALTER TABLE orders ADD INDEX idx_status_time_amount (status, create_time, amount);

再次EXPLAIN,type变成range,rows大幅减少,Extra不再有Using filesort。

4.2 改写SQL

  1. 避免SELECT *,只取必要字段。
  2. 将LEFT JOIN改为INNER JOIN如果业务允许(可提前过滤掉空数据)。
  3. 使用子查询或临时表减少关联数据量。

4.3 拆分复杂查询

把一条多表JOIN拆成多条简单查询,在应用层组装(适合数据量不是特别大的场景)。

五、实战案例

原始SQL(订单表500万,用户表200万,商品表100万):

SELECT o.order_no, u.phone, p.name, o.amount
FROM orders o
LEFTJOINusers u ON o.user_id = u.id
LEFTJOIN products p ON o.product_id = p.id
WHERE o.status = 1
AND o.create_time BETWEEN'2026-04-01'AND'2026-04-30'
ORDERBY o.amount DESC
LIMIT100;

问题

  1. orders表只有单列索引status,导致只过滤了状态,但create_time没走索引,扫描了全部状态为1的历史订单。
  2. ORDER BY amount引发文件排序。

优化方案

  1. 创建联合索引:ALTER TABLE orders ADD INDEX idx_status_time_amount(status, create_time, amount);
  2. 改写SQL,将LEFT JOIN改为INNER JOIN(因为用户和商品一定有对应数据,且不要求展示空)。
  3. 分页优化:因为LIMIT 100,已经很好。

优化后执行计划:type=range,rows=2000,Extra中无Using filesort,查询时间从30秒下降到0.08秒。

效果:数据库CPU从85%降到15%,应用恢复正常。

六、优缺点与适用场景

image

七、如何预防慢SQL导致CPU飙升?

  1. 建立SQL审核机制:上线前必须通过EXPLAIN review,禁止全表扫描语句上线。
  2. 开启慢查询监控:配置阈值(如1秒),并接入告警系统。
  3. 定期分析索引使用情况:删除未使用的索引,优化重复索引。
  4. 压测:大促前对核心查询进行压力测试,观察CPU拐点。
  5. 限流与降级:在API网关或业务层配置限流,防止突发流量冲击数据库。
  6. 全链路压测平台:提前发现潜在慢SQL。

八、总结

线上慢SQL导致CPU飙升,本质上是一个“数据库资源被低效查询耗尽”的问题。

处理的核心流程可以概括为:

定位慢SQL → EXPLAIN分析 → 索引优化/SQL改写 → 验证效果 → 建立事前预防机制

在实际工作中,80%的CPU飙升问题都可以通过加索引或简单改写SQL解决。

但更重要的是,我们要有敬畏之心——每一行SQL都可能成为生产事故的导火索。

建立规范的开发流程和强有力的监控体系,才是长久之计。

最后修改:2026 年 06 月 06 日
如果觉得我的文章对你有用,请随意赞赏