应使用 sp_statement_completed 事件并启用 collect_cpu_time=1,可精准捕获存储过程内部每条语句的 cpu 消耗(单位微秒),其快照值比 sys.dm_exec_requests.cpu_time 更准确,避免会话级累计干扰。

SQL Server 扩展事件怎么捕获存储过程的 CPU 消耗
扩展事件(XEvents)是 SQL Server 中最轻量、最灵活的性能跟踪机制,比 SQL Trace 更低开销,也比 Profiler 更适合长期运行的监控。要捕获存储过程的 CPU 消耗,核心是启用 query_post_execution_showplan 或 sp_statement_completed 事件,并确保收集 cpu_time 字段。
关键点在于:不能只靠 rpc_completed——它只记录调用完成,不带语句级 CPU;必须用能穿透到执行计划或语句粒度的事件。
-
sp_statement_completed事件可精确到存储过程内部每一行语句的cpu_time、duration、logical_reads,但需在创建会话时显式勾选collect_cpu_time = 1 -
query_post_execution_showplan能拿到实际执行计划 XML,其中每个<relop></relop>节点含ActualCPUms,但仅适用于 SQL Server 2016+,且默认不启用,需加collect_database_name = 1和collect_statement = 1才能关联到具体 SP - 避免用
sql_batch_completed:它只覆盖整个批处理,无法区分 CALL 后面的具体 SP 内部行为 - 建议过滤条件写成
WHERE [object_name] LIKE '%your_proc_name%'或用[database_name] = 'YourDB',否则日志爆炸
为什么 sp_statement_completed 的 cpu_time 比 sys.dm_exec_requests.cpu_time 更准
sys.dm_exec_requests.cpu_time 是会话级累计值,只要连接没断,它就一直累加——哪怕 SP 已执行完、正卡在 WAITFOR 或事务未提交,这个值还在涨。而 sp_statement_completed.cpu_time 是单条语句执行结束瞬间的快照,和实际调度器消耗严格对齐。
典型误判场景:
- 一个 SP 里有
WAITFOR DELAY '00:01:00',sys.dm_exec_requests.cpu_time可能只有 2ms,但duration显示 60s——这时你查sp_statement_completed就能看到前几条 INSERT/UPDATE 的真实 CPU 占用,后面那 60s 在事件里根本不会产生新记录 - SP 内部嵌套调用另一个 SP,
sys.dm_exec_requests只显示外层 EXEC 的总时间,而sp_statement_completed能按调用栈逐层展开,每条语句都带独立cpu_time - 如果 SP 启用了
SET STATISTICS XML ON,执行计划里的ActualCPUms和 XEvent 的cpu_time值基本一致(误差
如何避免 XEvent 会话本身拖慢系统
XEvent 开销低,但配置不当照样翻车。最容易被忽略的是事件缓冲区和目标选择。
- 别用
event_file目标写本地磁盘——尤其当磁盘 I/O 已紧张时,写入延迟会反压到执行线程。改用ring_buffer(内存中滚动缓存)做实时诊断,或histogram聚合统计(比如按object_name统计平均cpu_time) - 务必设置
MAX_DISPATCH_LATENCY = 5 SECONDS,否则事件可能堆积数分钟才刷出,失去实时性 - 禁止开启
collect_statement = 1同时又不限制statement长度——超长动态 SQL 会导致事件体膨胀数倍,内存占用飙升。加statement_max_length = 4000是底线 - 高并发 OLTP 环境下,不要为所有 SP 开启跟踪。先用
sys.dm_exec_procedure_stats查total_worker_time / execution_count前 5 名,只盯这些热点过程
从 XEvent 数据里快速定位高 CPU 语句
导出 ring_buffer 或 event_file 后,别手动翻 XML。直接用 T-SQL 解析:
SELECT
event_data.value('(/event/@timestamp)[1]', 'datetime2') AS [timestamp],
event_data.value('(/event/data[@name="object_name"]/value)[1]', 'sysname') AS [proc_name],
event_data.value('(/event/data[@name="cpu_time"]/value)[1]', 'bigint') AS [cpu_time_ms],
event_data.value('(/event/data[@name="statement"]/value)[1]', 'nvarchar(4000)') AS [statement]
FROM (
SELECT CAST(target_data AS XML) AS target_data
FROM sys.dm_xe_session_targets t
JOIN sys.dm_xe_sessions s ON s.address = t.event_session_address
WHERE s.name = 'capture_sp_cpu'
AND t.target_name = 'ring_buffer'
) AS tab
CROSS APPLY target_data.nodes('//event') AS q(event_data)
WHERE event_data.value('(/event/@name)[1]', 'varchar(50)') = 'sp_statement_completed'
AND event_data.value('(/event/data[@name="cpu_time"]/value)[1]', 'bigint') > 100;
注意:cpu_time 单位是微秒,上面示例里 > 100 是指 > 0.1ms——太低的阈值会淹没噪音;生产环境建议从 1000(1ms)起调。
真正难的是区分“合理高 CPU”和“异常高 CPU”:一条聚合报表 SP 跑 500ms CPU 是正常的,但同一条语句在 OLTP 场景下反复出现 200ms 就得查执行计划是否走了全表扫描——XEvent 只给数据,不替你做判断。










