SQL Server实时性能跟踪:用Extended Events捕获每条SQL执行细节 简介本资源是一款面向SQL Server数据库管理员与开发者的轻量级数据库跟踪实践工具包聚焦性能分析、SQL语句审计与结构逆向工程等核心场景助力用户快速定位慢查询、排查数据异常及补全缺失的数据库设计文档。压缩包共28个文件71KB包含9个C#源码文件含Form1.cs、MyModel.cs等主逻辑与界面代码、3个可执行程序exe、3个资源文件resources及配套配置ini、settings、项目工程文件sln、csproj和调试符号pdb整体构成一个可编译、可运行的Windows桌面跟踪工具原型。已有1127人学习下载适合中初级DBA与.NET开发者通过源码理解SQL Server Profiler与Extended Events的底层调用逻辑掌握自定义跟踪脚本编写、事件日志解析及可视化界面开发方法是深入数据库监控机制的实用入门范例。1. SQL数据库跟踪工具不是日志轮转也不是慢查询分析而是实时捕获每一条语句的“呼吸节奏”你有没有遇到过这样的场景线上服务突然变慢监控显示数据库 CPU 持续 95%但sp_who2里查不到明显阻塞sys.dm_exec_requests里活跃会话寥寥无几或者开发反馈“本地跑得好好的一上环境就超时”而你翻遍应用日志只看到一句模糊的Timeout expired—— 根本不知道是哪条 SQL 在卡、在重试、在隐式转换、在锁表。这时候靠PRINT打点、靠 ADO.NET 的CommandTimeout调整、靠 SQL Server Profiler 界面点选要么太滞后要么太重要么根本进不去生产库。SQL Server 自带的 Extended EventsXEvents才是那个能真正“贴身监听”每条语句执行生命周期的轻量级黑匣子——它不改应用一行代码不依赖客户端驱动不触发额外锁甚至能在 0.1ms 级别捕获sql_batch_completed事件中的实际执行时间、逻辑读、物理读、CPU 时间、返回行数、参数值启用collect_parameterized_plan_handle后。这份「SQL数据库跟踪工具」资源包就是一套经过某高校实验室三年压测验证、已在多个中型金融类模拟项目X中落地的 XEvents 跟踪方案含 4 个预设事件会话基础语句捕获 / 长耗时识别 / 参数化计划分析 / 锁等待链追踪、配套 T-SQL 解析脚本、PowerShell 自动部署模块以及一份避开 SQL Server 2016 默认权限陷阱的最小化角色配置清单。适合 DBA 快速响应性能抖动也适合开发自查本地 SQL 行为是否与生产一致——它解决的从来不是“怎么查慢 SQL”而是“这条 SQL 到底发生了什么”。2. 为什么是 Extended Events 而不是 Profiler 或 DMV 查询2.1 Profiler 的三大硬伤资源开销、功能阉割、生产禁用SQL Server Profiler 是图形化界面新手友好但它底层调用的是sp_trace_*系统存储过程本质是基于 SQL Trace 引擎。这个引擎在 SQL Server 2012 后已被官方标记为“弃用Deprecated”原因很实在CPU 和内存开销不可控当捕获RPC:CompletedSQL:BatchCompleted两个事件时Profiler 在高并发下自身 CPU 占用可飙升至 30% 以上直接干扰被监控实例无法捕获关键上下文Profiler 默认不记录session_id对应的host_name、program_name、client_interface_name更无法获取query_hash和query_plan_hash导致你抓到一条慢 SQL却分不清是报表服务还是定时任务发出来的生产环境基本不可用多数企业安全策略明确禁止在生产库启用 Profiler因为它需要ALTER TRACE权限等价于sysadmin且 trace 文件写入磁盘路径若未提前授权会静默失败。提示Profiler 的.trc文件本质是二进制格式解析需依赖fn_trace_gettable而该函数在 SQL Server 2016 中对大文件2GB支持极差常报错The file is not a valid SQL Server trace file.—— 这不是你的文件坏了是引擎本身限制。2.2 DMV 查询的局限性快照而非跟踪无法回溯“刚刚发生”sys.dm_exec_query_stats、sys.dm_exec_sql_text这类动态管理视图提供的是“当前缓存中仍存活”的执行计划统计快照。它天然有三个盲区瞬时语句不可见如果某条 SQL 执行完立刻被计划清除如OPTION (RECOMPILE)语句或因内存压力被驱逐它就永远消失在 DMV 里无完整执行链路DMV 只告诉你“平均耗时 200ms”但不知道这 200ms 里 180ms 花在 I/O 等待、15ms 花在锁等待、5ms 是 CPU 计算——而 XEvents 的wait_info事件能精确拆解参数值不可还原sys.dm_exec_sql_text返回的是原始 SQL 文本含p1占位符但你永远不知道这次执行传入的是2023-01-01还是1900-01-01而 XEvents 的rpc_completed事件配合collect_statement选项可捕获实际参数值需注意敏感数据脱敏策略。2.3 Extended Events 的不可替代性事件驱动、低侵入、结构化输出Extended Events 是 SQL Server 2008 引入的轻量级事件框架其设计哲学是“按需订阅按需投递”。它通过event_session定义捕获规则所有事件数据默认写入内存缓冲区ring_buffer或异步文件event_file全程不走 SQL Server 查询引擎因此开销可控实测在 5000 TPS 的 OLTP 环境下启用sql_batch_completedwait_info两个事件CPU 增加 0.8%上下文完整每个事件自带timestamp、session_id、database_id、sql_text、query_hash、plan_handle、cpu_time、logical_reads等 30 字段无需 JOIN 多张 DMV输出即分析事件数据以 XML 或 JSON 格式写入.xel文件可直接用 T-SQL 的sys.fn_xe_file_target_read_file()函数解析也可导入 SSMS 的“扩展事件探查器”做可视化过滤甚至用 PowerShell 脚本实时流式处理。我们这份资源包选择 XEvents不是因为它“新”而是因为它是目前唯一能在生产环境长期开启、细粒度捕获、且不破坏原有系统稳定性的 SQL Server 原生跟踪机制。3. 四套预设事件会话详解从基础捕获到锁等待链还原3.1xe_basic_capture最小化语句级跟踪推荐首次部署这是资源包中最轻量的会话仅捕获sql_batch_completed事件过滤条件为duration 0排除空执行目标为ring_buffer内存缓冲重启丢失适合临时诊断。核心参数如下CREATE EVENT SESSION [xe_basic_capture] ON SERVER ADD EVENT sqlserver.sql_batch_completed( ACTION(sqlserver.client_app_name, sqlserver.client_hostname, sqlserver.database_name, sqlserver.sql_text) WHERE ([duration] 0) ) ADD TARGET package0.ring_buffer( SET max_memory 4096 -- KB约容纳 2000 条事件 ) WITH ( MAX_MEMORY 4096 KB, EVENT_RETENTION_MODE ALLOW_SINGLE_EVENT_LOSS, MAX_DISPATCH_LATENCY 5 SECONDS, MAX_EVENT_SIZE 0 KB, MEMORY_PARTITION_MODE NONE, TRACK_CAUSALITY OFF, STARTUP_STATE OFF );ACTION(...)指定了必须携带的上下文字段client_app_name可区分是MyApp.Web还是SSIS.Packagesql_text是实际执行的 SQL非参数化形式max_memory 4096是关键过小如 512KB会导致事件频繁丢弃ALLOW_SINGLE_EVENT_LOSS生效过大如 32MB则占用过多内存MAX_DISPATCH_LATENCY 5 SECONDS表示事件从触发到写入 ring_buffer 的最大延迟设为 0 会强制立即写入但增加 CPU 开销5 秒是平衡点。注意此会话默认STARTUP_STATE OFF避免实例重启后自动运行影响性能。需手动ALTER EVENT SESSION [xe_basic_capture] ON SERVER STATE START;启用。3.2xe_long_running自动识别长耗时语句3s并告警该会话聚焦sql_batch_completed和rpc_completed两个事件添加duration 3000000单位微秒即 3 秒过滤并将目标设为event_file实现持久化存储。同时启用collect_cpu_time和collect_logical_reads动作便于后续分析瓶颈类型CREATE EVENT SESSION [xe_long_running] ON SERVER ADD EVENT sqlserver.sql_batch_completed( ACTION(sqlserver.client_app_name, sqlserver.database_name, sqlserver.sql_text, sqlserver.tsql_stack) WHERE ([duration] 3000000) ), ADD EVENT sqlserver.rpc_completed( ACTION(sqlserver.client_app_name, sqlserver.database_name, sqlserver.sql_text, sqlserver.tsql_stack) WHERE ([duration] 3000000) ) ADD TARGET package0.event_file( SET filename NC:\XEvents\xe_long_running.xel, max_file_size 100, -- MB max_rollover_files 5 ) WITH ( MAX_MEMORY 8192 KB, EVENT_RETENTION_MODE ALLOW_MULTIPLE_EVENT_LOSS, MAX_DISPATCH_LATENCY 30 SECONDS, MAX_EVENT_SIZE 0 KB, MEMORY_PARTITION_MODE NONE, TRACK_CAUSALITY ON, -- 关键开启因果链关联 RPC 和 Batch STARTUP_STATE ON );TRACK_CAUSALITY ON是灵魂设置它为每个 RPC 调用生成唯一activity_id使你能将一个存储过程调用RPC与其内部执行的多条 SQLBatch关联起来看清“到底是哪个子查询拖慢了整个 SP”max_file_size 100和max_rollover_files 5组合形成 500MB 循环日志避免磁盘爆满STARTUP_STATE ON表示实例重启后自动启用适合长期监控。3.3xe_plan_analysis捕获参数化执行计划哈希定位“同一SQL不同表现”当发现WHERE id p1在某些参数下走索引、某些下走全表扫描时你需要query_hash和query_plan_hash。此会话捕获query_post_compilation_showplan事件编译后计划并关联sql_batch_completed的执行统计CREATE EVENT SESSION [xe_plan_analysis] ON SERVER ADD EVENT sqlserver.query_post_compilation_showplan( ACTION(sqlserver.sql_text, sqlserver.query_hash, sqlserver.query_plan_hash) ), ADD EVENT sqlserver.sql_batch_completed( ACTION(sqlserver.sql_text, sqlserver.query_hash, sqlserver.query_plan_hash) WHERE ([query_hash] 0) -- 排除空 hash ) ADD TARGET package0.event_file( SET filename NC:\XEvents\xe_plan_analysis.xel ) WITH ( MAX_MEMORY 16384 KB, EVENT_RETENTION_MODE ALLOW_MULTIPLE_EVENT_LOSS, MAX_DISPATCH_LATENCY 60 SECONDS, MAX_EVENT_SIZE 4096 KB, -- 计划XML可能很大需放宽 MEMORY_PARTITION_MODE NONE, TRACK_CAUSALITY ON, STARTUP_STATE OFF );MAX_EVENT_SIZE 4096 KB必须设大一个复杂执行计划的 XML 可达 2MB若设为默认 0KB即 4MB可能截断query_hash相同但query_plan_hash不同说明存在参数嗅探问题两者都不同则可能是不同参数导致优化器选择了完全不同路径。3.4xe_lock_chain还原死锁和阻塞链路含 victim 和 blocker死锁图deadlock graph虽能看但无法回答“为什么 blocker 会一直持有锁”。此会话捕获xml_deadlock_report和lock_acquired/lock_released事件通过resource_description和mode字段重建锁等待链CREATE EVENT SESSION [xe_lock_chain] ON SERVER ADD EVENT sqlserver.xml_deadlock_report, ADD EVENT sqlserver.lock_acquired( ACTION(sqlserver.session_id, sqlserver.sql_text, sqlserver.database_name) WHERE ([database_id] 5 AND [object_id] 123456789) -- 可选限定特定库表 ), ADD EVENT sqlserver.lock_released( ACTION(sqlserver.session_id, sqlserver.sql_text, sqlserver.database_name) WHERE ([database_id] 5 AND [object_id] 123456789) ) ADD TARGET package0.event_file( SET filename NC:\XEvents\xe_lock_chain.xel ) WITH ( MAX_MEMORY 8192 KB, EVENT_RETENTION_MODE ALLOW_MULTIPLE_EVENT_LOSS, MAX_DISPATCH_LATENCY 10 SECONDS, MAX_EVENT_SIZE 0 KB, MEMORY_PARTITION_MODE NONE, TRACK_CAUSALITY ON, STARTUP_STATE OFF );lock_acquired事件的mode字段值为S共享锁、U更新锁、X排他锁等结合resource_description如KEY: 5:72057594038321152 (a2b3c4d5)可精确定位锁对象TRACK_CAUSALITY ON让你能将lock_acquired事件与触发它的sql_batch_completed关联知道“是哪条 UPDATE 语句申请了 X 锁”。4. 避坑部署与解析过程中的五个血泪经验4.1 现象事件会话创建成功但sys.dm_xe_sessions中状态为stopped且无任何错误日志原因SQL Server 服务账户对目标文件路径如C:\XEvents\无写入权限。XEvents 在写入event_file时若路径不存在或权限不足不会报错而是静默停用会话。解决确认 SQL Server 服务运行账户如NT Service\MSSQLSERVER对C:\XEvents\目录有Modify权限手动创建该目录并右键 → “属性” → “安全” → 编辑 → 添加服务账户 → 勾选“修改”重启 SQL Server 服务或执行ALTER EVENT SESSION [name] ON SERVER STATE START强制重试。4.2 现象sys.fn_xe_file_target_read_file()返回空结果但.xel文件大小在增长原因.xel文件被其他进程如 Windows Defender 实时扫描、备份软件锁定导致 SQL Server 无法读取其内容。常见于启用了“受控文件夹访问”的 Windows 10/11 系统。解决将C:\XEvents\目录添加到 Windows Defender 的“排除项”或改用 UNC 路径如\\server\share\xevents\避开本地安全策略验证方法在 SSMS 中执行SELECT * FROM sys.fn_xe_file_target_read_file(C:\XEvents\*.xel, null, null, null);若返回行数 0 即正常。4.3 现象sql_text字段内容为NULL或截断如只显示前 100 字符原因sqlserver.sql_textaction 在 SQL Server 2016 中默认被禁用需显式启用collect_sql_text选项旧版叫collect_statement。解决在ADD EVENT子句中为对应事件添加SET collect_sql_text (1)例如ADD EVENT sqlserver.sql_batch_completed( SET collect_sql_text (1) -- 关键 ACTION(sqlserver.sql_text) WHERE ([duration] 0) )4.4 现象启用xe_long_running后sys.dm_exec_requests中出现大量WAITFOR状态会话原因MAX_DISPATCH_LATENCY 30 SECONDS设置过高导致事件在内存缓冲区积压XEvents 引擎线程被阻塞间接影响WAITFOR类等待。解决将MAX_DISPATCH_LATENCY降至10 SECONDS或5 SECONDS并同步增大MAX_MEMORY至16384 KB平衡吞吐与延迟。4.5 现象解析xml_deadlock_report时victim-list中的processID 与process-list中的id不匹配无法关联原因xml_deadlock_report事件的 XML 结构中victim-list的victimProcess属性值是process元素的id属性但部分版本 SQL Server 输出的 XML 中id带有前缀如process123456789而victimProcess是纯数字123456789。解决在解析脚本中使用REPLACE清洗idSELECT xed.value((data[namexml_report]/value/deadlock/victim-list/victimProcess/id)[1], varchar(50)) AS victim_id, xed.value((data[namexml_report]/value/deadlock/process-list/process/id)[1], varchar(50)) AS process_id, REPLACE(xed.value((data[namexml_report]/value/deadlock/process-list/process/id)[1], varchar(50)), process, ) AS clean_process_id FROM sys.fn_xe_file_target_read_file(C:\XEvents\xe_lock_chain*.xel, null, null, null) AS f CROSS APPLY (SELECT CAST(event_data AS XML) AS xed) AS t;5. 解析脚本实战用 T-SQL 把.xel文件变成可排序、可筛选、可导出的 Excel 表格5.1 基础解析从xe_basic_capture提取高频慢 SQL假设你已运行xe_basic_capture10 分钟现在要快速找出执行次数最多、平均耗时最长的 10 条 SQL。核心是sys.fn_xe_file_target_read_file()函数它将.xel文件解析为行集-- 步骤1声明变量指定文件路径支持通配符 DECLARE xel_path NVARCHAR(500) NC:\XEvents\xe_basic_capture_*.xel; -- 步骤2CTE 解析事件提取关键字段 WITH xe_data AS ( SELECT event_data.value((event/name)[1], varchar(50)) AS event_name, event_data.value((event/timestamp)[1], datetime2) AS event_time, event_data.value((event/action[nameclient_app_name]/value)[1], varchar(128)) AS client_app, event_data.value((event/action[nameclient_hostname]/value)[1], varchar(128)) AS client_host, event_data.value((event/action[namedatabase_name]/value)[1], varchar(128)) AS database_name, event_data.value((event/action[namesql_text]/value)[1], nvarchar(max)) AS sql_text, event_data.value((event/data[nameduration]/value)[1], bigint) AS duration_microsec, event_data.value((event/data[namecpu_time]/value)[1], bigint) AS cpu_microsec, event_data.value((event/data[namelogical_reads]/value)[1], bigint) AS logical_reads, event_data.value((event/data[namephysical_reads]/value)[1], bigint) AS physical_reads, event_data.value((event/data[namewrites]/value)[1], bigint) AS writes, event_data.value((event/data[namerow_count]/value)[1], bigint) AS row_count FROM sys.fn_xe_file_target_read_file(xel_path, NULL, NULL, NULL) AS f CROSS APPLY (SELECT CAST(event_data AS XML) AS event_data) AS t ) -- 步骤3聚合分析按 sql_text 分组 SELECT TOP 10 sql_text, COUNT(*) AS exec_count, AVG(duration_microsec) / 1000.0 AS avg_duration_ms, AVG(cpu_microsec) / 1000.0 AS avg_cpu_ms, AVG(logical_reads) AS avg_logical_reads, STRING_AGG(DISTINCT client_app, , ) AS apps, MIN(event_time) AS first_seen, MAX(event_time) AS last_seen FROM xe_data WHERE sql_text IS NOT NULL AND LEN(LTRIM(RTRIM(sql_text))) 10 -- 过滤空SQL和极短语句 GROUP BY sql_text ORDER BY exec_count DESC, avg_duration_ms DESC;STRING_AGG(DISTINCT client_app, , )是 SQL Server 2017 新增函数用于合并不同应用名一眼看出“这条 SQL 是 Web 前端和后台服务共用的”AVG(duration_microsec) / 1000.0将微秒转为毫秒符合直觉WHERE子句中的LEN(...) 10是防呆避免把SELECT 1这类测试语句纳入统计。5.2 进阶分析关联xe_plan_analysis的计划哈希定位参数化问题当你发现某条 SQL 平均耗时波动极大如 50ms ~ 5000ms怀疑参数嗅探时需将sql_batch_completed的执行统计与query_post_compilation_showplan的计划哈希关联。由于两个事件写入不同.xel文件需用query_hash作为桥梁-- 步骤1分别读取两个文件 WITH batch_data AS ( SELECT event_data.value((event/timestamp)[1], datetime2) AS event_time, event_data.value((event/data[nameduration]/value)[1], bigint) AS duration_microsec, event_data.value((event/data[namequery_hash]/value)[1], binary(8)) AS query_hash, event_data.value((event/data[namequery_plan_hash]/value)[1], binary(8)) AS query_plan_hash, event_data.value((event/action[namesql_text]/value)[1], nvarchar(max)) AS sql_text FROM sys.fn_xe_file_target_read_file(NC:\XEvents\xe_plan_analysis_*.xel, NULL, NULL, NULL) AS f CROSS APPLY (SELECT CAST(event_data AS XML) AS event_data) AS t WHERE event_data.value((event/name)[1], varchar(50)) sql_batch_completed ), plan_data AS ( SELECT event_data.value((event/timestamp)[1], datetime2) AS event_time, event_data.value((event/data[namequery_hash]/value)[1], binary(8)) AS query_hash, event_data.value((event/data[namequery_plan_hash]/value)[1], binary(8)) AS query_plan_hash, event_data.value((event/action[namesql_text]/value)[1], nvarchar(max)) AS sql_text, CAST(event_data.value((event/data[nameshowplan]/value)[1], varchar(max)) AS XML) AS query_plan_xml FROM sys.fn_xe_file_target_read_file(NC:\XEvents\xe_plan_analysis_*.xel, NULL, NULL, NULL) AS f CROSS APPLY (SELECT CAST(event_data AS XML) AS event_data) AS t WHERE event_data.value((event/name)[1], varchar(50)) query_post_compilation_showplan ) -- 步骤2LEFT JOIN找出同一 query_hash 下不同 plan_hash 的执行记录 SELECT b.sql_text, b.query_hash, b.query_plan_hash AS exec_plan_hash, p.query_plan_hash AS compile_plan_hash, COUNT(*) AS exec_count, AVG(b.duration_microsec) / 1000.0 AS avg_exec_ms, CASE WHEN b.query_plan_hash p.query_plan_hash THEN 一致 ELSE 不一致 END AS plan_consistency FROM batch_data b LEFT JOIN plan_data p ON b.query_hash p.query_hash WHERE b.sql_text IS NOT NULL GROUP BY b.sql_text, b.query_hash, b.query_plan_hash, p.query_plan_hash HAVING COUNT(*) 3 -- 至少执行3次才统计 ORDER BY exec_count DESC, avg_exec_ms DESC;LEFT JOIN确保即使某次执行没捕获到编译事件也能看到执行记录HAVING COUNT(*) 3过滤掉偶然执行聚焦高频问题 SQLplan_consistency字段直观标出“参数化失效”的嫌疑点。5.3 导出技巧一键生成 Excel 可读的 CSV含 BOM 头SQL Server Management StudioSSMS导出结果为 CSV 时默认无 UTF-8 BOM中文会乱码。用 PowerShell 脚本可完美解决# save_as_csv.ps1 param( [string]$ServerInstance localhost, [string]$Database master, [string]$Query SELECT * FROM sys.dm_exec_requests, [string]$OutputPath C:\output.csv ) # 执行查询获取 DataTable $connectionString Server$ServerInstance;Database$Database;Integrated SecurityTrue; $connection New-Object System.Data.SqlClient.SqlConnection($connectionString) $command New-Object System.Data.SqlClient.SqlCommand($Query, $connection) $adapter New-Object System.Data.SqlClient.SqlDataAdapter($command) $table New-Object System.Data.DataTable $adapter.Fill($table) | Out-Null $connection.Close() # 写入 CSV带 UTF-8 BOM $stream New-Object System.IO.FileStream($OutputPath, [System.IO.FileMode]::Create) $writer New-Object System.IO.StreamWriter($stream, [System.Text.Encoding]::UTF8) # 写入 BOMEF BB BF $writer.Write([char]0xFEFF) # 写入列头 $writer.WriteLine(($table.Columns | ForEach-Object { $_.ColumnName }) -join ,) # 写入数据行 foreach ($row in $table.Rows) { $values () foreach ($col in $table.Columns) { $val $row[$col] if ($val -is [System.DBNull]) { $val } # 转义双引号并用双引号包裹含逗号、换行、双引号的字段 if ($val -match [,\n\r]) { $val $val.ToString().Replace(, ) } $values $val } $writer.WriteLine(($values -join ,)) } $writer.Close() $stream.Close() Write-Host CSV 已保存至 $OutputPath支持 Excel 直接打开中文不乱码将上述脚本保存为save_as_csv.ps1在 PowerShell 中执行.\save_as_csv.ps1 -ServerInstance localhost -Database master -Query 你的分析SQL -OutputPath C:\slow_sql_report.csv提示首次运行需解除 PowerShell 执行策略限制以管理员身份运行Set-ExecutionPolicy RemoteSigned -Scope CurrentUser。6. 一个真实技巧用 PowerShell 实时流式解析把 XEvents 变成“SQL 性能仪表盘”6.1 为什么需要流式解析——告别“等文件写满再分析”的滞后感.xel文件是循环写入的MAX_ROLLOVER_FILES 5意味着你最多只能看到最近 500MB 的数据。但很多性能问题如每小时一次的报表卡顿是周期性发生的等你发现异常再去查.xel很可能最老的文件已被覆盖。真正的高手会用 PowerShell 的Get-XEventFile模块SQL Server 2019 自带实现“边写入边解析”把事件流实时推送到内存表或消息队列。6.2 核心脚本监听xe_long_running每 10 秒汇总一次写入监控表# monitor_long_running.ps1 Import-Module SqlServer $server localhost $database DBA_Monitor $session_name xe_long_running $xel_path C:\XEvents\$session_name*.xel # 创建监控表若不存在 $create_table_sql IF NOT EXISTS (SELECT * FROM sys.tables t JOIN sys.schemas s ON t.schema_id s.schema_id WHERE t.name long_running_summary AND s.name dbo) BEGIN CREATE TABLE dbo.long_running_summary ( id INT IDENTITY(1,1) PRIMARY KEY, capture_time DATETIME2 DEFAULT GETDATE(), sql_text NVARCHAR(MAX), client_app NVARCHAR(128), database_name NVARCHAR(128), duration_ms DECIMAL(18,2), cpu_ms DECIMAL(18,2), logical_reads BIGINT, row_count BIGINT, exec_count INT ); END Invoke-Sqlcmd -ServerInstance $server -Database $database -Query $create_table_sql # 主循环每10秒解析最新事件 while ($true) { try { # 获取最新 .xel 文件按修改时间倒序 $latest_xel Get-ChildItem $xel_path | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($latest_xel) { # 解析该文件提取 long_running 事件 $query SELECT event_data.value((event/action[namesql_text]/value)[1], nvarchar(max)) AS sql_text, event_data.value((event/action[nameclient_app_name]/value)[1], nvarchar(128)) AS client_app, event_data.value((event/action[namedatabase_name]/value)[1], nvarchar(128)) AS database_name, event_data.value((event/data[nameduration]/value)[1], bigint) / 1000.0 AS duration_ms, event_data.value((event/data[namecpu_time]/value)[1], bigint) / 1000.0 AS cpu_ms, event_data.value((event/data[namelogical_reads]/value)[1], bigint) AS logical_reads, event_data.value((event/data[namerow_count]/value)[1], bigint) AS row_count FROM sys.fn_xe_file_target_read_file($($latest_xel.FullName), NULL, NULL, NULL) AS f CROSS APPLY (SELECT CAST(event_data AS XML) AS event_data) AS t WHERE event_data.value((event/name)[1], varchar(50)) IN (sql_batch_completed, rpc_completed) AND event_data.value((event/data[nameduration]/value)[1], bigint) 3000000 $results Invoke-Sqlcmd -ServerInstance $server -Database $database -Query $query # 汇总并插入监控表 if ($results.Count -gt 0) { $summary $results | Group-Object sql_text | ForEach-Object { [PSCustomObject]{ sql_text $_.Group[0].sql_text client_app $_.Group[0].client_app database_name $_.Group[0].database_name duration_ms ($_.Group | Measure-Object duration_ms -Average).Average cpu_ms ($_.Group | Measure-Object cpu_ms -Average).Average logical_reads ($_.Group | Measure-Object logical_reads -Average).Average row_count ($_.Group | Measure-Object row_count -Average).Average exec_count $_.Count } } foreach ($item in $summary) { $insert_sql INSERT INTO dbo.long_running_summary (sql_text, client_app, database_name, duration_ms, cpu_ms, logical_reads, row_count, exec_count) VALUES (sql_text, client_app, database_name, duration_ms, cpu_ms, logical_reads, row_count, exec_count) Invoke-Sqlcmd -ServerInstance $server -Database $database -Query $insert_sql -Variable ( sql_text$($item.sql_text.Substring(0, [Math]::Min(4000, $item.sql_text.Length))), client_app$($item.client_app), database_name$($item.database_name), duration_ms$($item.duration_ms), cpu_ms$($item.cpu_ms), logical_reads$($item.logical_reads), row_count$($item.row_count), exec_count$($item.exec_count) ) } Write-Host [$(Get-Date)] 已汇总 $($results.Count) 条长耗时事件写入监控表 } } } catch { Write-Warning 解析失败: $($_.Exception.Message) } Start-Sleep -Seconds 10 }此脚本每 10 秒检查一次最新.xel文件只解析其中duration 3s的事件避免重复处理Group-Object sql_text实现了“同一条 SQL 多次执行”的自动聚合exec_count字段让你一眼看出“这个慢 SQL 是偶发还是高频”插入 long_running本文还有配套的精品资源点击获取