
1. 先确认问题是SQL本身还是环境或数据变了面试官问“昨天50毫秒今天5秒CPU飙到90%”这其实是在考察你面对线上突发性能问题的第一反应和排查思路。很多人一上来就埋头看SQL执行计划、加索引这很可能走错方向。我的经验是性能突然劣化90%的原因不是SQL本身逻辑突然变“笨”了而是它的执行环境或输入数据发生了你没预料到的变化。CPU飙高是结果不是原因。所以正确的第一步不是优化而是定位。你需要立刻回答出清晰的排查层次从外到内从宏观到微观确认影响范围是这一条SQL慢还是整个数据库实例都慢是单个应用节点还是所有节点确认环境变化昨天到今天数据库、服务器、应用有没有做过变更发布、配置更新、数据迁移确认数据变化这条SQL处理的数据量、数据分布统计信息有没有剧变如果只有这一条SQL变慢而数据库整体负载正常那问题大概率就出在这条SQL的执行计划上。如果整个实例CPU都高那可能是资源争抢、锁等待、或大量并发执行了低效查询。核心思路把“SQL变慢”这个现象拆解成“执行计划变了”或“资源被抢了”两个大方向去查。2. 锁定罪魁祸首找到正在消耗CPU的查询和会话确定了是SQL执行计划问题后下一步就是精准定位。你不能靠猜必须用数据说话。在SQL Server里有一系列动态管理视图DMV是你的“手术刀”。2.1 实时查看谁正在“烧”CPU当CPU飙到90%时第一时间连接上数据库通常用SSMS或Azure Data Studio运行以下查询。它能告诉你此时此刻哪些会话正在疯狂消耗CPU。SELECT TOP 10 s.session_id, r.status, r.cpu_time AS CPU时间(ms), r.logical_reads AS 逻辑读, r.reads AS 物理读, r.writes AS 写, r.total_elapsed_time / 1000 AS 总耗时(ms), SUBSTRING(st.text, (r.statement_start_offset/2) 1, ((CASE r.statement_end_offset WHEN -1 THEN DATALENGTH(st.text) ELSE r.statement_end_offset END - r.statement_start_offset)/2) 1) AS 正在执行的语句, DB_NAME(st.dbid) AS 数据库名, OBJECT_NAME(st.objectid, st.dbid) AS 对象名, s.login_name, s.host_name, s.program_name FROM sys.dm_exec_sessions AS s JOIN sys.dm_exec_requests AS r ON r.session_id s.session_id CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS st WHERE r.session_id ! SPID -- 排除当前查询自己的会话 ORDER BY r.cpu_time DESC;关键看这几列cpu_time这个会话已经用了多少CPU时间。找到数值最大的那个。逻辑读如果这个值异常高通常意味着大量全表/索引扫描是CPU高的常见原因。正在执行的语句这里就能看到是不是那条“问题SQL”。注意如果SQL是来自应用层的参数化查询这里可能显示的是带参数的语句你需要结合对象名和应用日志来确认。如果这个查询返回空或者消耗CPU的会话已经结束那就需要查历史记录。2.2 历史分析谁曾经是“CPU大户”SQL Server会把执行过的查询计划及其性能数据缓存起来。通过查询sys.dm_exec_query_stats我们可以找到累计消耗CPU最多的那些查询。SELECT TOP 10 qs.last_execution_time AS 最后执行时间, SUBSTRING(st.text, (qs.statement_start_offset/2) 1, ((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(st.text) ELSE qs.statement_end_offset END - qs.statement_start_offset)/2) 1) AS 语句文本, (qs.total_worker_time / 1000) / qs.execution_count AS 平均CPU时间(ms), qs.total_worker_time / 1000 AS 总CPU时间(ms), qs.execution_count AS 执行次数, qs.total_logical_reads / qs.execution_count AS 平均逻辑读, qp.query_plan AS 执行计划 FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) AS qp -- 可以按总CPU时间或平均CPU时间排序 ORDER BY qs.total_worker_time DESC; -- ORDER BY (qs.total_worker_time / qs.execution_count) DESC; -- 按平均CPU排序这个查询的价值在于平均CPU时间对比昨天和今天的记录。如果同一条SQL平均CPU时间从50ms暴涨到5000ms那问题就锁定了。执行计划点击query_plan列生成的XML链接可以直接查看图形化执行计划这是分析的关键。执行次数结合总CPU时间看是单次执行变慢还是执行次数暴增导致的总体CPU高。拿到问题SQL的完整语句和它的执行计划你的排查就成功了一半。3. 剖析执行计划为什么今天“跑偏”了找到了消耗CPU的SQL和它的执行计划现在要像侦探一样对比“昨天”和“今天”的计划差异。理想情况下你有性能基线和历史计划缓存。如果没有就靠推理。在SSMS里选中那条SQL点击“显示估计的执行计划”或“包括实际执行计划”后执行重点关注以下几点3.1 最可能的原因统计信息过时这是导致执行计划突变的头号杀手。优化器依赖统计信息来估算成本、选择索引。如果表的数据量分布发生了巨大变化例如某字段突然新增了上千万条相同值的数据而统计信息没更新优化器就会基于错误的信息选择一个“愚蠢”的计划。如何判断在执行计划中将鼠标悬停在每个操作符如表扫描、索引查找上查看“估计行数”和“实际行数”。如果两者相差巨大比如估计100行实际返回100万行那几乎可以断定是统计信息问题。如何解决更新统计信息。但要注意在大型表上更新统计信息本身是资源密集型操作最好在低峰期进行。-- 更新特定表的统计信息 UPDATE STATISTICS YourTableName WITH FULLSCAN; -- 更新当前数据库所有用户表的统计信息谨慎使用特别是生产环境 EXEC sp_updatestats;建议对于核心大表建立定期的统计信息更新维护任务而不是等出了问题再手动更新。3.2 第二个常见原因参数嗅探Parameter Sniffing这个问题非常隐蔽。简单说就是SQL Server为存储过程或参数化查询第一次编译生成执行计划时“嗅探”到了传入的参数值并基于这个值生成了一个它认为最优的计划。但这个计划对于后续传入的其他参数值可能极其糟糕。典型场景一个根据Status字段查询订单的存储过程。第一次执行时传入Status1已完成数据量很少生成了一个使用索引查找的高效计划。这个计划被缓存。第二天应用传入Status0进行中数据量巨大但SQL Server依然重用缓存中那个为少量数据生成的计划可能选择了错误的索引或连接策略导致全表扫描和CPU飙升。如何排查对比不同参数值下的执行计划。用OPTION (RECOMPILE)提示强制每次执行都重新编译看性能是否恢复正常。EXEC YourStoredProc Status 0 WITH RECOMPILE;查询计划缓存看同一条SQL或存储过程是否对应了多个不同的执行计划。SELECT cp.plan_handle, cp.objtype, cp.usecounts, st.text FROM sys.dm_exec_cached_plans cp CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) st WHERE st.text LIKE %YourStoredProcName%;如何解决使用OPTION (RECOMPILE)查询提示适用于执行不频繁但每次参数差异大的查询。优点是总能获得最适合当前参数的计划缺点是每次编译消耗CPU。使用OPTION (OPTIMIZE FOR UNKNOWN)或OPTION (OPTIMIZE FOR (variable value))让优化器使用一个“平均”或指定的值来生成计划避免被极端参数值带偏。使用本地变量在存储过程内部先将参数赋值给一个本地变量再用这个变量进行查询。这会阻止参数嗅探但可能导致优化器无法使用参数值进行优化。清除特定查询的计划缓存临时措施-- 先找到问题计划的handle SELECT plan_handle, st.text FROM sys.dm_exec_cached_plans cp CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) st WHERE st.text LIKE %YourProblemSQL%; -- 然后用找到的plan_handle清除它 DBCC FREEPROCCACHE (0x05000600B5207A...); -- 替换为实际的plan_handle3.3 第三个原因缺失索引如果执行计划显示大量的“键查找”Key Lookup或“表扫描”Table Scan并且有绿色的“缺失索引”建议那么缺失索引可能是性能瓶颈。注意不要盲目创建所有建议的索引。索引本身也有维护成本写操作变慢。需要评估improvement_measure值是否足够高这个值综合了影响度、使用次数等。建议的索引是否与现有索引重复或冲突创建索引对磁盘空间和写性能的影响。可以使用以下查询查看缺失索引建议SELECT migs.avg_total_user_cost * (migs.avg_user_impact / 100.0) * (migs.user_seeks migs.user_scans) AS improvement_measure, CREATE INDEX [IX_ CONVERT(VARCHAR, mig.index_group_handle) _ CONVERT(VARCHAR, mid.index_handle) ] ON mid.statement ( ISNULL(mid.equality_columns, ) CASE WHEN mid.equality_columns IS NOT NULL AND mid.inequality_columns IS NOT NULL THEN , ELSE END ISNULL(mid.inequality_columns, ) ) ISNULL( INCLUDE ( mid.included_columns ), ) AS create_index_statement, migs.*, mid.database_id, mid.[object_id] FROM sys.dm_db_missing_index_groups mig INNER JOIN sys.dm_db_missing_index_group_stats migs ON migs.group_handle mig.index_group_handle INNER JOIN sys.dm_db_missing_index_details mid ON mig.index_handle mid.index_handle WHERE migs.avg_total_user_cost * (migs.avg_user_impact / 100.0) * (migs.user_seeks migs.user_scans) 10 -- 可根据情况调整阈值 ORDER BY improvement_measure DESC;3.4 其他执行计划问题SARGability问题查询条件写法导致无法使用索引。例如WHERE SUBSTRING(ProductNumber, 1, 2) AB或WHERE Amount * 1.1 100。优化器无法在索引列上应用函数或计算会导致全表扫描。应重写为WHERE ProductNumber LIKE AB%和WHERE Amount 100 / 1.1。隐式类型转换WHERE varchar_column 123会导致列上的索引失效。确保比较时数据类型一致。4. 排查外部因素当SQL本身“无罪”时如果经过上述分析SQL的执行计划本身合理数据量也没变但CPU还是高那就要把目光投向数据库实例和服务器环境。4.1 资源争抢与阻塞运行以下查询检查是否有大量的阻塞Blocking。SELECT session_id, blocking_session_id, wait_type, wait_time, wait_resource, t.text AS SQL文本 FROM sys.dm_exec_requests r CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t WHERE blocking_session_id 0;大量的阻塞会导致会话堆积每个被阻塞的会话都在占用CPU等待资源从整体上看就是CPU利用率高。解决阻塞需要分析锁资源、事务隔离级别和业务逻辑。4.2 服务器和虚拟机配置电源计划在Windows服务器上确保电源选项设置为“高性能”。“平衡”模式可能会动态降低CPU频率导致SQL Server需要更长时间完成相同工作从而表现出更高的CPU占用率。虚拟机配置如果SQL Server运行在虚拟机上如VMware确保为虚拟机分配了固定的CPU资源并且没有过度分配over-commit。检查虚拟化层的CPU就绪时间CPU Ready Time是否过高。CPU亲和性在极端情况下可以尝试设置SQL Server进程的CPU亲和性将其绑定到特定的NUMA节点或核心减少上下文切换开销。但这通常是最后的手段需要谨慎测试。4.3 其他高CPU消耗源扩展事件XEvents或SQL Trace过于激进或配置不当的跟踪会话会产生大量开销。检查是否有不必要的跟踪在运行。自动增长如果数据库文件或日志文件频繁自动增长且每次增长量很小会导致磁盘I/O瓶颈间接使得CPU在等待I/O时堆积任务。第三方软件防病毒软件实时扫描数据库文件、备份软件等都可能与SQL Server争抢CPU和I/O资源。5. 总结一套可复用的排查清单面对“SQL突然变慢CPU飙升”的问题你可以按照以下清单快速响应【定性】快速登录服务器使用任务管理器或perfmon确认是否是sqlservr.exe进程导致CPU高。【定位】使用sys.dm_exec_requests和sys.dm_exec_query_stats定位到具体的消耗CPU的SQL语句和会话。【剖析】获取该SQL的当前执行计划。对比对比历史计划或不同参数下的计划。看估算/实际行数差异巨大 -更新统计信息。看参数嗅探同一存储过程不同参数性能差异极大 - 考虑参数嗅探问题使用RECOMPILE或OPTIMIZE FOR提示。看扫描操作大量表扫描/键查找 - 检查缺失索引和SARGability。【验效】如果怀疑是统计信息或计划缓存问题尝试在测试环境或低峰期使用UPDATE STATISTICS或DBCC FREEPROCCACHE(plan_handle)进行针对性清理观察是否恢复。【外因】如果SQL本身计划无问题检查阻塞链、服务器电源设置、虚拟机配置以及是否有外部监控工具如XEvents造成开销。记住临时修复如清空计划缓存可以快速止血但根本解决需要找到原因并实施长效方案比如更新统计信息作业、优化索引、重写问题查询、调整数据库参数等。把这条排查路径讲清楚并能在每个环节说出关键的系统视图和命令面试官想要的实战经验你就已经展示出来了。