这类线上 SQL 性能突然劣化导致数据库 CPU 飙升的问题是 DBA 和开发同学最常遇到的紧急状况之一。它不像慢 SQL 优化那样有明确的长期目标而是要求你快速定位“变化点”在最短时间内恢复服务稳定。很多人一上来就去看执行计划、加索引这其实走偏了。真正的排查应该像急诊医生一样先问“今天和昨天有什么不同”再根据线索做针对性检查。这篇文章就围绕这个经典面试题拆解一套从现象到根因的实战排查路径。它适合所有需要与数据库打交道的后端开发、运维和 DBA核心价值不是背八股文而是建立一套可复现、可判断、有先后顺序的现场问题分析框架。下面我会按照“先锁定范围再深入细节”的原则把整个过程拆成四个递进的阶段。1. 第一步确认问题边界与收集现场快照遇到 CPU 90% 的告警第一反应不应该是登录服务器狂敲命令而是先花几分钟搞清楚问题的“边界”。这能避免你在错误的方向上浪费大量时间。1.1 界定问题范围是单条 SQL 还是系统性影响首先你需要确认这条“跑了5秒”的 SQL是导致 CPU 飙升的主要原因还是结果之一。这很重要。关键问题1这条 SQL 是独立出现的还是伴随其他慢查询一起出现如果监控只报警了这一条那它很可能是“元凶”。如果同时有大量其他慢查询那可能是数据库整体负载高导致这条原本就慢的 SQL 雪上加霜。关键问题2CPU 90% 是持续性的还是间歇性的是单个核心跑满还是所有核心都高通过top或htop命令Linux快速查看。如果只是单个或少数几个mysqld或oracle进程的 CPU 使用率高基本可以锁定是少数 SQL 的问题。如果所有核心都高且用户进程%us和系统进程%sy都高可能要怀疑锁竞争、刷脏页等系统级问题。关键问题3除了 CPU其他关键指标如 QPS、连接数、磁盘 IO、网络流量、内存使用率有没有同步异常这能帮你判断是计算密集型问题还是 IO 密集型问题。我的习惯是在动手前先拉取最近15分钟到1小时的监控大盘截图。包括CPU 使用率、数据库活跃线程数、每秒查询量QPS、每秒事务量TPS、InnoDB 缓冲池命中率、磁盘读写 IOPS。这些是后续对比的“基线”。1.2 抓取问题 SQL 的完整现场信息确定了是这条 SQL 的问题接下来就要像刑侦取证一样抓取它运行时的完整“现场信息”。光知道它跑了5秒没用要知道它“为什么”跑了5秒。获取精确的 SQL 文本与执行计划立刻从慢查询日志slow log或数据库监控工具如 Percona Monitoring and Management, Prometheus Grafana 等中找到这条 SQL 的完整语句。注意一定要拿到客户端发来的原始 SQL包括具体的参数值。SELECT * FROM user WHERE id 123和SELECT * FROM user WHERE id ?在优化器眼里可能是天壤之别。使用EXPLAINMySQL或EXPLAIN PLAN FOROracle命令获取它当前的执行计划。务必保存下来。-- MySQL 示例 EXPLAIN FORMATJSON SELECT * FROM orders WHERE user_id 10086 AND create_time 2023-10-01;重要动作如果条件允许立刻在测试环境或从库上用相同的参数重现一次并获取其执行计划。对比“昨天”的正常执行计划如果有存档和“今天”的问题执行计划差异点往往就是突破口。捕获实时运行状态在 MySQL 中使用SHOW PROCESSLIST;或查询information_schema.processlist来查看当前所有连接的状态。找到那条正在运行的慢 SQL记录它的Id,User,Host,db,Command,Time,State,Info。State字段尤其重要比如Sending data,Copying to tmp table,Sorting result都指向了不同的瓶颈。对于更深入的分析可以使用performance_schema或sys库。例如SELECT * FROM sys.session WHERE conn_id processlist_id;可以查看该会话的详细等待事件。在 Oracle 中使用v$session和v$sql等动态性能视图进行类似查询。这一步的目标是拿到两份核心资料一是问题发生时的系统全景监控图二是问题 SQL 此刻的“体检报告”SQL文本、执行计划、会话状态。没有这些后续分析就是盲人摸象。2. 第二步对比分析“变化点”——为什么是今天这是整个排查流程中最关键的一环。性能不会无缘无故劣化一定是环境或数据发生了变化。你需要系统地对比“昨天正常时”和“今天异常时”的不同。2.1 环境与配置变更排查首先排查人为或系统层面的变更这些往往是“快刀斩乱麻”的突破口。数据库层面参数变更检查my.cnf或spfile等配置文件最近是否被修改过是否有参数如innodb_buffer_pool_size,query_cache_size如果启用,tmp_table_size,sort_buffer_size被调整参数调整不当会立即影响优化器决策和内存使用。统计信息数据库表的统计信息是否过时在 MySQL 中ANALYZE TABLE命令可能被手动或自动执行如果统计信息采集不准确优化器可能会选择错误的索引。检查information_schema.tables中的UPDATE_TIME字段或直接对相关表执行SHOW TABLE STATUS LIKE ‘table_name’。索引变更相关表上的索引是否被添加、删除或修改如索引类型一个索引的删除或失效会直接导致全表扫描。应用层面代码发布是否刚刚进行了应用发布新的代码版本可能修改了 SQL 逻辑如增加了UNION,DISTINCT,GROUP BY或者改变了传入 SQL 的参数值。连接池配置应用连接池的配置如最大连接数、超时时间是否被修改连接泄漏或配置不当也会导致数据库负载异常。系统与硬件层面资源竞争同一台服务器上是否部署了新的、消耗资源的进程是否在进行备份、大数据导出等操作硬件问题虽然不常见但磁盘 IO 性能突然下降如 RAID 卡故障、SSD 寿命将至也会导致所有 IO 密集型 SQL 变慢。2.2 数据特征变化分析如果环境没有变化那么问题大概率出在“数据”本身。这是最隐蔽也最常见的原因。查询条件的数据分布倾斜昨天WHERE user_id 100可能只返回 10 行今天WHERE user_id 1000000可能返回 100 万行。你需要检查 SQL 中WHERE条件的传入值。是不是今天查询的是一个“热点”用户或一个“冷门”且数据量巨大的条件检查手段在从库或测试库用EXPLAIN查看执行计划中的rows列估算并与实际SELECT COUNT(*)的结果对比。如果估算严重不准就是统计信息问题如果估算就很巨大那就是数据量问题。表数据量突变相关表是否在夜间经历了大规模的数据导入、删除或更新特别是UPDATE和DELETE操作可能产生大量碎片影响索引效率。检查表大小SELECT table_name, data_length, index_length FROM information_schema.tables WHERE table_schema ‘your_db’;锁竞争今天是否有长时间未提交的事务锁住了该 SQL 需要访问的行或表在 MySQL 中可以使用SHOW ENGINE INNODB STATUS\G查看锁信息或查询information_schema.innodb_locks和innodb_lock_waits。一个常见的场景是凌晨跑批任务开启了事务但未提交阻塞了白天的高频查询。我通常会列一个简单的检查清单像过安检一样逐项核对[ ] SQL 传入参数值是否异常对比历史值[ ] 相关表统计信息是否最近更新是否准确[ ] 相关表索引是否完好SHOW INDEX FROM table_name查看Cardinality[ ] 数据库是否有未提交的长事务[ ] 应用端连接池和代码是否有变更3. 第三步深入执行计划与系统状态诊断如果第二步找到了明确的变更点比如统计信息刚更新那么修复它比如重新收集统计信息可能就解决了。如果没找到或者变更点是合理的比如数据量自然增长就需要深入分析 SQL 本身的执行效率。3.1 解码执行计划中的“危险信号”拿到EXPLAIN的输出后重点看以下几个字段它们是指向性能瓶颈的路标字段/现象可能的问题排查方向type ALL全表扫描。最致命的信号之一。检查WHERE条件是否没有用到索引或者索引失效。type index全索引扫描。虽然比 ALL 好但扫描整个索引也可能很慢。检查是否可以通过更合适的索引或改写 SQL 来避免扫描。key NULL优化器没有使用任何索引。确认索引是否存在WHERE条件字段是否匹配索引的最左前缀。rows 估算值巨大优化器认为需要处理的行数非常多。统计信息可能不准或者查询条件本身过滤性很差。Extra: Using filesort需要额外的排序操作无法利用索引排序。检查ORDER BY子句的字段是否与索引顺序一致。Extra: Using temporary需要创建临时表来处理查询常见于GROUP BY,DISTINCT,UNION。临时表可能在内存tmp_table_size或磁盘上创建磁盘临时表极慢。Extra: Using where存储引擎返回行后服务器层还需要进行过滤。WHERE条件中的部分字段可能不在索引中导致回表查询后过滤。针对我们“昨天快今天慢”的场景要特别关注索引选择变化对比昨天的执行计划今天是否用了一个不同的、更差的索引或者今天干脆没走索引出现了新的“危险信号”比如昨天Extra是空的今天多了Using filesort和Using temporary。3.2 利用性能模式Performance Schema进行深度剖析EXPLAIN是静态的预测而Performance Schema或 Oracle 的 ASH/AWR可以告诉你 SQL 运行时到底“卡”在了哪里。找到问题 SQL 的查询 ID-- MySQL 5.7/8.0 SELECT DIGEST_TEXT, SCHEMA_NAME, COUNT_STAR, SUM_TIMER_WAIT/1000000000 AS exec_time_sec FROM performance_schema.events_statements_summary_by_digest WHERE DIGEST_TEXT LIKE ‘%你的SQL关键词%’ ORDER BY SUM_TIMER_WAIT DESC LIMIT 5;通过DIGEST_TEXT标准化后的 SQL和SUM_TIMER_WAIT总耗时找到最可疑的 SQL 及其DIGEST。分析等待事件SQL 执行慢无非是花时间在“计算”CPU或“等待”IO、锁等上。通过以下查询可以查看该 SQL 各类等待事件的时间分布-- 需要先开启相关 instruments 和 consumers通常默认已开启部分 SELECT EVENT_NAME, COUNT_STAR, SUM_TIMER_WAIT/1000000000 AS wait_time_sec FROM performance_schema.events_waits_summary_by_thread_by_event_name WHERE THREAD_ID IN ( SELECT THREAD_ID FROM performance_schema.threads WHERE PROCESSLIST_ID 你的连接ID ) AND EVENT_NAME LIKE ‘wait/io/file%’ OR EVENT_NAME LIKE ‘wait/io/table%’ OR EVENT_NAME LIKE ‘wait/synch%’ ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;如果wait/io/file/innodb/innodb_data_file等待时间很长说明磁盘 IO 是瓶颈。如果wait/synch/mutex/innodb等锁等待时间长说明存在并发争用。如果wait/io/table/sql/handler时间长可能意味着大量回表或全表扫描。这一步的目的是将“慢”这个模糊的感觉量化成“在哪个环节花了多少时间”让优化目标变得极其清晰。4. 第四步制定应急与根治方案诊断完成后需要立刻行动。行动分为两层应急止血和根治优化。4.1 应急止血快速降低 CPU 负载当 CPU 持续在 90% 以上时首要任务是让数据库恢复服务能力而不是追求完美优化。查询限流或终止如果这条 SQL 来自某个非核心业务或报表系统最直接的办法是KILL掉当前正在运行的该查询线程使用SHOW PROCESSLIST找到的Id。注意对于 OLTP 核心业务 SQL慎用KILL需评估事务回滚的影响。在应用层或数据库代理层如 ProxySQL可以临时配置规则对该类 SQL 进行限流或返回降级结果。临时调整优化器“暗示”如果确定是优化器选错了索引可以在 SQL 中添加索引提示Index Hint强制其使用正确的索引。这是一个快速生效的“创可贴”。SELECT * FROM orders USE INDEX (idx_user_id) WHERE user_id 10086 AND create_time ‘2023-10-01’;警告这只是临时方案。索引提示会固化在代码中如果数据分布未来发生变化这个提示可能又会变成性能杀手。扩容与资源隔离在云数据库环境下可以临时提升实例规格CPU/内存。或者将这条消耗巨大的查询路由到只读从库上去执行避免影响主库的写入事务。4.2 根治优化从根上解决问题止血后必须分析根本原因防止问题复发。优化索引这是最根本的解决方案。根据执行计划和WHERE、ORDER BY、GROUP BY子句设计或调整索引。考虑联合索引对于WHERE user_id ? AND create_time ?联合索引(user_id, create_time)通常比单列索引(user_id)更高效。避免过度索引索引不是越多越好每个索引都会增加写操作的开销。改写 SQL避免SELECT *只查询需要的列减少回表开销和网络传输。拆分复杂查询将复杂的JOIN或子查询拆分成多个简单查询在应用层组合。有时这比数据库优化器更有效。优化分页对于深度分页LIMIT 100000, 20考虑使用“延迟关联”或记录上一页最后一条记录的 ID 进行查询。调整数据库配置根据Performance Schema的等待事件分析如果是 IO 瓶颈可以考虑升级磁盘或调整innodb_io_capacity。如果是内存不足导致临时表落盘可以适当增加tmp_table_size和max_heap_table_size。定期收集统计信息建立定时任务在业务低峰期对核心表进行ANALYZE TABLE。架构层面改进读写分离将报表类、分析类的重查询导向只读从库。引入缓存对于实时性要求不高的数据使用 Redis 等缓存中间件避免重复查询数据库。数据归档将历史冷数据迁移到归档库或对象存储减少主表数据量。最后一定要形成闭环将本次问题的根本原因、处理过程、优化方案记录到事故报告或知识库中。并考虑在监控系统中增加针对此类问题的告警规则例如“核心表统计信息超过7天未更新”、“全表扫描次数每分钟超过阈值”、“临时表磁盘写入率突增”。回到最初的问题一条 SQL 从 50 毫秒劣化到 5 秒核心排查思路就是“对比变化”。优先排查数据与环境的变更再通过执行计划和性能视图定位瓶颈。在实际工作中我建议把整个排查流程工具化、脚本化比如写一个 shell 脚本在接到告警时自动收集系统状态、慢查询、锁信息和关键表的统计信息这能为你节省大量黄金救援时间。记住快不是靠手速而是靠清晰的思路和准备好的工具。