简介:本资源是一款面向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 Events(XEvents)才是那个能真正“贴身监听”每条语句执行生命周期的轻量级黑匣子——它不改应用一行代码,不依赖客户端驱动,不触发额外锁,甚至能在 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:Completed+SQL: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_completed+wait_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.Package,sql_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 = N'C:\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)与其内部执行的多条 SQL(Batch)关联起来,看清“到底是哪个子查询拖慢了整个 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 = N'C:\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 = N'C:\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清洗id:
SELECT xed.value('(data[@name="xml_report"]/value/deadlock/victim-list/victimProcess/@id)[1]', 'varchar(50)') AS victim_id, xed.value('(data[@name="xml_report"]/value/deadlock/process-list/process/@id)[1]', 'varchar(50)') AS process_id, REPLACE(xed.value('(data[@name="xml_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) = N'C:\XEvents\xe_basic_capture_*.xel'; -- 步骤2:CTE 解析事件,提取关键字段 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[@name="client_app_name"]/value)[1]', 'varchar(128)') AS client_app, event_data.value('(event/action[@name="client_hostname"]/value)[1]', 'varchar(128)') AS client_host, event_data.value('(event/action[@name="database_name"]/value)[1]', 'varchar(128)') AS database_name, event_data.value('(event/action[@name="sql_text"]/value)[1]', 'nvarchar(max)') AS sql_text, event_data.value('(event/data[@name="duration"]/value)[1]', 'bigint') AS duration_microsec, event_data.value('(event/data[@name="cpu_time"]/value)[1]', 'bigint') AS cpu_microsec, event_data.value('(event/data[@name="logical_reads"]/value)[1]', 'bigint') AS logical_reads, event_data.value('(event/data[@name="physical_reads"]/value)[1]', 'bigint') AS physical_reads, event_data.value('(event/data[@name="writes"]/value)[1]', 'bigint') AS writes, event_data.value('(event/data[@name="row_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[@name="duration"]/value)[1]', 'bigint') AS duration_microsec, event_data.value('(event/data[@name="query_hash"]/value)[1]', 'binary(8)') AS query_hash, event_data.value('(event/data[@name="query_plan_hash"]/value)[1]', 'binary(8)') AS query_plan_hash, event_data.value('(event/action[@name="sql_text"]/value)[1]', 'nvarchar(max)') AS sql_text FROM sys.fn_xe_file_target_read_file(N'C:\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[@name="query_hash"]/value)[1]', 'binary(8)') AS query_hash, event_data.value('(event/data[@name="query_plan_hash"]/value)[1]', 'binary(8)') AS query_plan_hash, event_data.value('(event/action[@name="sql_text"]/value)[1]', 'nvarchar(max)') AS sql_text, CAST(event_data.value('(event/data[@name="showplan"]/value)[1]', 'varchar(max)') AS XML) AS query_plan_xml FROM sys.fn_xe_file_target_read_file(N'C:\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' ) -- 步骤2:LEFT 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过滤掉偶然执行,聚焦高频问题 SQL;plan_consistency字段直观标出“参数化失效”的嫌疑点。
5.3 导出技巧:一键生成 Excel 可读的 CSV(含 BOM 头)
SQL Server Management Studio(SSMS)导出结果为 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 Security=True;" $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) # 写入 BOM(EF 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[@name="sql_text"]/value)[1]', 'nvarchar(max)') AS sql_text, event_data.value('(event/action[@name="client_app_name"]/value)[1]', 'nvarchar(128)') AS client_app, event_data.value('(event/action[@name="database_name"]/value)[1]', 'nvarchar(128)') AS database_name, event_data.value('(event/data[@name="duration"]/value)[1]', 'bigint') / 1000.0 AS duration_ms, event_data.value('(event/data[@name="cpu_time"]/value)[1]', 'bigint') / 1000.0 AS cpu_ms, event_data.value('(event/data[@name="logical_reads"]/value)[1]', 'bigint') AS logical_reads, event_data.value('(event/data[@name="row_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[@name="duration"]/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
本文还有配套的精品资源,点击获取