直接查sys.dm_exec_requests可实时定位正在运行的高CPU存储过程,通过过滤status为running/suspended且cpu_time>10000毫秒的会话,并用statement_start_offset精准截取当前执行语句;对高频低效问题则需结合sys.dm_exec_procedure_stats分析累计CPU和执行次数,再提取执行计划XML搜索Compute Scalar、Table Spool等性能雷区。

直接查 sys.dm_exec_requests 定位正在跑的高CPU存储过程
别等它执行完再分析——很多存储过程一跑就是几分钟,CPU早被吃光了。关键是在它还“活着”的时候,从内存里捞出当前正在执行的那条语句,而不是只看到最外层的 EXEC MyProc。
核心逻辑是:过滤出 status 为 running 或 suspended、且 cpu_time > 10000(即已耗 CPU 超 10 秒)的会话,再用 sql_handle 关联真实 SQL 文本:
SELECT
r.session_id,
s.login_name,
s.host_name,
r.cpu_time / 1000.0 AS cpu_sec,
r.logical_reads,
r.wait_type,
SUBSTRING(t.text, (r.statement_start_offset/2) + 1,
(CASE WHEN r.statement_end_offset = -1
THEN DATALENGTH(t.text)
ELSE r.statement_end_offset END - r.statement_start_offset) / 2 + 1) AS current_statement,
t.text AS full_sql
FROM sys.dm_exec_requests r
JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.status IN ('running', 'suspended')
AND r.cpu_time > 10000
AND s.is_user_process = 1;
-
current_statement是真正卡住的那行(比如一个嵌套循环里的UPDATE),不是整个存储过程体 - 注意
r.statement_start_offset和r.statement_end_offset才能精准截取当前执行片段,直接读t.text容易误判 - 如果
command列是EXECUTE或RPC,说明这行就是存储过程调用入口,但真正耗资源的往往在它里面
用 sys.dm_exec_procedure_stats 看累计指标,识别“慢性消耗”
有些存储过程单次不慢,但每分钟被调几百次,加起来 CPU 就爆了。这类问题在 sys.dm_exec_requests 里很难抓到,得看历史累计数据。
sys.dm_exec_procedure_stats 按存储过程对象聚合统计,total_worker_time 是总 CPU 时间(微秒),除以 execution_count 就是平均值:
SELECT DB_NAME(database_id) AS db_name, OBJECT_SCHEMA_NAME(object_id, database_id) + '.' + OBJECT_NAME(object_id, database_id) AS proc_name, total_worker_time / 1000000.0 AS total_cpu_sec, execution_count, (total_worker_time / 1000000.0) / NULLIF(execution_count, 0) AS avg_cpu_sec, total_logical_reads / NULLIF(execution_count, 0) AS avg_logical_reads FROM sys.dm_exec_procedure_stats WHERE database_id > 4 -- 排除系统库 AND total_worker_time > 10000000 -- 总 CPU 超 10 秒 ORDER BY total_worker_time DESC;
- 重点看
avg_cpu_sec高 +execution_count也高的组合,这是“高频+低效”的典型 - 如果
plan_generation_num > 1,说明该存储过程经常重编译,可能是参数嗅探或拼字符串导致的计划缓存失效 - 这个视图只包含“至少执行过一次且计划还在缓存中”的存储过程,刚重启后数据为空
提取执行计划 XML,搜 这类性能雷区
找到目标存储过程后,光看 SQL 文本不够——真正吃 CPU 的往往是执行计划里的隐藏结构。必须把计划 XML 拿出来人工扫一眼。
先用 sys.dm_exec_query_plan 拿计划,再用 sys.dm_exec_sql_text 对应上 SQL:
SELECT
p.query_plan,
t.text AS sql_text
FROM sys.dm_exec_procedure_stats s
CROSS APPLY sys.dm_exec_query_plan(s.plan_handle) p
CROSS APPLY sys.dm_exec_sql_text(s.sql_handle) t
WHERE s.object_id = OBJECT_ID('YourProcName');
拿到 XML 后,在浏览器或编辑器里搜索这些关键词:
-
:标量函数调用,每行都执行一次,无法并行 -
或:内存密集型操作,常因缺少索引或ORDER BY引发 -
嵌套很深且外层行数多:可能该换哈希连接或加索引 - 大量
:说明有隐式转换,类型不匹配导致索引失效
临时表没建索引就 JOIN,是 CPU 占用最高的常见原因
开发写存储过程时习惯性用 #temp,但忘了加索引。SQL Server 在对几万行的临时表做 JOIN 或 ORDER BY 时,会默默触发大内存排序和哈希构建,CPU 直接拉满。
验证方法很简单:在目标存储过程中,找所有 CREATE TABLE #xxx 或 SELECT ... INTO #xxx,然后检查后续是否跟了 CREATE INDEX。没有?基本就是它了。
- 尤其警惕
SELECT * INTO #t FROM ...后直接JOIN #t的模式 - 临时表字段若参与
WHERE、JOIN、ORDER BY,必须建索引;主键列默认有聚集索引,但非主键筛选字段需要显式创建 - 用
sys.dm_exec_cached_plans+sys.dm_exec_query_plan查计划里是否有Hash Match或Sort节点,再反推是哪张临时表没索引
真正难定位的不是某条语句慢,而是存储过程里那些“看起来很短、执行很快、但被高频调用+带标量计算+临时表无索引”的组合。它们不会出现在 top 10 耗时列表里,却能把 CPU 稳定压在 95% 以上。











