简介:围绕SQL Server数据库活动跟踪与结构梳理的实用工具包,面向数据库管理员、后端开发者和正在学习SQL性能排查的读者,可用于理解数据库读写行为、定位慢查询与资源消耗点,并辅助生成表、字段、索引及存储过程等结构说明。压缩包共28个文件、约71KB,以C#源码、资源与窗体文件、可执行程序为主,同时包含解决方案、项目配置、清单及调试符号,便于直接查看示例工程的组织方式与运行逻辑。已有1123人学习下载。读者可结合包内示例理解SQL Server Profiler与Extended Events的跟踪思路,熟悉查询、插入、更新、删除等操作的记录方式,并延伸到性能瓶颈分析、数据一致性排查和安全审计场景;对第三方监控工具的可视化报告与告警设置也能建立基本认识,适合作为数据库跟踪入门的参考材料。
1. 凌晨三点被告警叫醒,我用一份事件会话把慢查询按住了
某公司一套跑批业务的库,白天看着风平浪静,一到凌晨批量任务就各种超时,监控上只有一条干巴巴的“CPU 使用率 90%”曲线,谁在跑、跑了什么、慢在哪一步,全是黑匣子。这种时候能救场的,就是 SQL Server 数据库跟踪能力:把语句完成事件、RPC 调用、死锁报告按谓词过滤后落到文件里,再离线拆开看。这套东西包括 Extended Events 事件会话、服务器端跟踪(sp_trace_*)和老牌的图形化 Profiler 三条路径。它解决的是“知道慢了但不知道谁慢”的问题,适合后端开发、应用运维和刚接手数据库的 DBA。下面按选型、落地、排错、进阶的顺序,把我自己反复用过的流程拆开讲。
2. 三代跟踪方案怎么选:Profiler、服务器端跟踪与事件会话的取舍
SQL Server 的跟踪能力不是一代,而是三代并存。很多团队翻车,不是不会用,而是一上来就在生产库上开了最重的那一种。选型这步做对,后面省一半力气。
2.1 三种机制的边界对比
先把三者的能力摊在桌面上。Profiler 是图形化客户端,本质还是让服务器端开一个 trace;服务器端跟踪是 sp_trace_create 系列存储过程加文件目标;Extended Events 是事件引擎,用事件会话(EVENT SESSION)组织事件、动作、谓词和目标。三者在“能不能过滤”“开销多大”“能不能持久化”上差别很大。
| 维度 | Profiler 图形化 | 服务器端跟踪 sp_trace_* | Extended Events 事件会话 |
|---|---|---|---|
| 入口 | 客户端工具连库 | T-SQL 存储过程 | T-SQL / 客户端图形界面 |
| 谓词过滤 | 有,但过滤在客户端侧更重 | 有列过滤,粒度一般 | 事件级谓词,可在采集前丢弃 |
| 事件粒度 | 语句级为主 | 语句级、锁、死锁 | 语句、RPC、等待、锁、死锁、查询计划 |
| 典型开销 | 高,容易线上出事 | 中到高 | 低,可只采关心的字段 |
| 持久化 | 存表或存文件,需要配置 | 文件或表 | 文件目标(.xel),可滚动 |
| 现状定位 | 应急看一眼 | 老系统遗留 | 默认首选 |
图形化工具的便利性毋庸置疑,但它把“采集”和“展示”绑在一起,采集端稍微开宽一点,实例的调度压力就上来了。服务器端跟踪的问题类似,而且脚本可读性差,事件和列的编号全是数字,交接的时候没人看得懂。Extended Events 的优势在于谓词在事件产生时就判断,不满足条件的根本不会进入缓冲区,字段也能裁到只剩需要的几个。
2.2 为什么我把默认方案压在 Extended Events 上
理由很实在。第一,开销可控:可以只挂sql_statement_completed和rpc_completed两个事件,只取数据库名、会话 ID、客户端程序名和语句文本四个动作,谓词卡在 duration 上。第二,可持久:文件目标写 .xel,支持按大小滚动,实例重启还能用STARTUP_STATE = ON自动拉起。第三,可离线:把 .xel 拷回本地,用 T-SQL 或图形界面慢慢拆,完全不碰生产库。
提示:线上排查慢查询,采集端只做“记录”,不做“分析”。分析全部挪到离线,这是降低风险最有效的一条纪律。
常见做法是先建一个只开必要事件的会话,跑 10 到 30 分钟,拿到样本就停。别指望一次会话解决所有问题,也别把它当常驻监控——常驻监控该交给专门的采集体系,事件会话是手术刀,不是体温计。
2.3 十分钟建一个能落盘的最小会话
下面这段是我常用的骨架,改数据库 ID 和文件路径就能用。谓词里duration的单位是微秒,200000 就是 200 毫秒,这个换算关系踩过坑的人都懂。
-- 建一个只抓慢语句、慢 RPC 和死锁报告的事件会话 CREATE EVENT SESSION [slow_watch] ON SERVER ADD EVENT sqlserver.sql_statement_completed( ACTION( sqlserver.database_id, sqlserver.sql_text, sqlserver.client_app_name, sqlserver.client_hostname, sqlserver.session_id, sqlserver.username ) -- 只保留执行超过 200ms 且属于目标库(示例库 ID 为 7)的语句 WHERE ([duration] >= 200000 AND [sqlserver].[database_id] = 7) ), ADD EVENT sqlserver.rpc_completed( ACTION( sqlserver.sql_text, sqlserver.client_app_name, sqlserver.session_id ) WHERE ([duration] >= 200000) ), ADD EVENT sqlserver.xml_deadlock_report -- 死锁图,免费送的,别省 ADD TARGET package0.event_file( SET filename = N'D:\XELogs\slow_watch.xel', max_file_size = 256, -- 单文件 256MB max_rollover_files = 4 -- 最多保留 4 个滚动文件 ) WITH ( MAX_DISPATCH_LATENCY = 5 SECONDS, -- 缓冲区 5 秒刷一次盘 EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS, MAX_EVENT_SIZE = 0 KB, -- 0 表示用默认上限 MEMORY_PARTITION_MODE = NONE, TRACK_CAUSALITY = OFF, STARTUP_STATE = ON -- 实例重启后自动启动 ); GO ALTER EVENT SESSION [slow_watch] ON SERVER STATE = START; GO先说事件。sql_statement_completed覆盖普通 T-SQL 批里的每条语句,rpc_completed覆盖存储过程调用和参数化调用,这两个组合起来能覆盖绝大多数业务路径。漏掉rpc_completed是很常见的失误,表现为“明明有慢调用但文件里空空如也”。
再说动作。sql_text会把整条语句带出来,代价是事件体积变大;如果只关心哪类语句慢,可以先不取sql_text,用statement字段配合sqlserver.database_name定位。client_app_name是我必留的字段,它能直接告诉你慢的是报表服务、定时任务还是某个后台线程,比看会话 ID 猜人快得多。
目标文件参数上,max_file_size单位是 MB,max_rollover_files决定滚动保留几个。总量估算很简单:单文件大小乘以文件数就是磁盘上限,256 × 4 约 1GB,普通排查完全够用。MAX_DISPATCH_LATENCY设 5 秒是个折中,设太大会丢尾部事件,设太小会增加刷盘频率。
EVENT_RETENTION_MODE我一般选ALLOW_SINGLE_EVENT_LOSS,原因是排查场景下丢个别事件无所谓,丢整个缓冲区才是灾难。STARTUP_STATE = ON意味着实例重启后会话自动恢复,好处是没人盯着也不会断,坏处是忘了关就一直在写盘,这点要有意识。
执行完记得确认状态:
-- 确认会话已启动,并查看目标文件当前大小 SELECT s.name, s.startup_state, t.target_name, CAST(t.target_data AS XML).value('(EventFileTarget/File/@name)[1]', 'nvarchar(260)') AS current_file FROM sys.dm_xe_sessions AS s JOIN sys.dm_xe_session_targets AS t ON t.event_session_address = s.address WHERE s.name = 'slow_watch';这条 DMV 查询返回的是会话运行时状态,current_file就是当前正在写的那个文件。如果这里查不到记录,说明会话压根没起来,先回去看 CREATE 那一步有没有报错,别急着怀疑谓词。
3. 抓慢查询:事件、谓词和字段裁剪的三个关键动作
会话建起来只是开了个口子,真正决定能不能抓到有用数据的,是事件选得对不对、谓词卡得准不准、字段裁得够不够。这三步任何一步松了,要么抓不到,要么抓回来一堆噪音。
3.1 事件选型:语句完成和 RPC 完成到底差在哪
sql_statement_completed记录的是批处理内部每条语句执行完成时的情况,一个批里有五条语句就产生五条事件;rpc_completed记录的是远程过程调用完成,典型场景是应用程序通过参数化方式调用存储过程。两者不是替代关系,是互补关系。
实际排查里我遇到过两种典型漏采。一种是应用层全部走存储过程,只开了sql_statement_completed,结果抓到的是存储过程内部的语句,入口参数完全看不到,只能靠猜。另一种是 ORM 框架把语句包装成 sp_executesql 调用,只开rpc_completed,能看到调用但看不到内部展开的语句文本。两种都开,才不至于来回补采。
还有一类事件值得按需加:sqlserver.sql_batch_completed适合整批统计耗时,sqlserver.lock_deadlock系列负责死锁,sqlserver.wait_info能看等待类型但对量级敏感,非必要不加。字段上,sql_statement_completed的duration是语句自身耗时,不含网络传输;rpc_completed的duration含过程内部所有语句累计,两者对比能快速判断耗时是在过程内部还是调用链路上。
3.2 谓词怎么写才不误伤
谓词写在事件后面,语法上是一个布尔表达式,可用字段就是该事件带的 data 和 action。最常见的错误是拿错字段名——比如把duration写成elapsed_time,创建时不报错,运行时谓词永远为假,表现就是“会话正常但一条数据都没有”。
几个我常用的谓词写法:
-- 写法一:按库 + 耗时双条件,最常用 WHERE ([duration] >= 500000 AND [sqlserver].[database_id] = 7) -- 写法二:额外排除某个后台程序,避免定时任务刷屏 WHERE ([duration] >= 500000 AND [sqlserver].[client_app_name] <> N'ReportScheduler') -- 写法三:只在语句文本里出现特定表名时才采,适合精准蹲点 WHERE ([duration] >= 100000 AND [sqlserver].[sql_text] LIKE N'%OrderDetail%')注意:
sql_text上的 LIKE 过滤会带来额外字符串匹配开销,只在明确知道表名或关键字时用,别写成%SELECT%这种全表扫描式的谓词。
耗时阈值的设置有个经验值:先设 200 毫秒跑十分钟看样本量,如果文件涨得飞快就加到 500 毫秒或 1 秒,如果一条都没有就降到 50 毫秒。别一上来就设 10 毫秒,那是给自己找罪受,采集端压力和后续分析成本都会翻倍。数据库 ID 用DB_ID(N'库名')查一次记下来,比在谓词里写库名可靠。
3.3 目标文件参数:滚动、上限与刷盘节奏
文件目标有三个参数需要心里有数。max_file_size控制单文件上限,单位 MB;max_rollover_files控制滚动保留数量;MAX_DISPATCH_LATENCY控制缓冲区刷盘间隔。三者共同决定磁盘占用和事件完整度。
磁盘估算的公式是「单文件大小 × 滚动数」,再加上少量元数据文件。举例,256MB × 4 大约 1GB,这个量级放在任何一台生产机上都不算负担。但如果把max_file_size设成 0 或者不设上限,单个文件会一直涨,最后把数据盘写满,这是真实发生过的翻车现场。
另一个容易忽略的是文件路径。路径必须是数据库服务账户有写权限的目录,不是当前登录用户的目录。用 T-SQL 建会话时用的是服务账户身份,很多“创建成功但文件不存在”的问题都出在这。稳妥做法是放在实例默认备份目录同级的独立子目录,事先用服务账户验证一次写入。
刷盘间隔上,5 秒适合排查场景;如果只想抓极短时间窗口(比如复现一个偶发问题),可以设 1 秒,代价是刷盘频繁。设为 0 表示实时派发,开销会明显上升,生产环境不建议。
3.4 读 .xel:T-SQL 和图形界面两条路
文件落地之后就是读。图形界面适合快速浏览,把 .xel 拖进去就能看到时间轴和字段表,筛选排序都方便。但要做聚合统计,还是得靠 T-SQL,因为事件数据存的是 XML,得用 XQuery 拆。
-- 读取 .xel 文件,把关键字段拆成列 SELECT evt.value('(event/@name)[1]', 'sysname') AS event_name, evt.value('(event/@timestamp)[1]', 'datetime2') AS event_time, evt.value('(event/data[@name="duration"]/value)[1]', 'bigint') / 1000.0 AS duration_ms, evt.value('(event/action[@name="session_id"]/value)[1]', 'int') AS session_id, evt.value('(event/action[@name="database_id"]/value)[1]', 'int') AS database_id, evt.value('(event/action[@name="client_app_name"]/value)[1]', 'nvarchar(128)') AS app_name, evt.value('(event/data[@name="statement"]/value)[1]', 'nvarchar(max)') AS statement_text FROM ( SELECT CAST(event_data AS XML) AS evt FROM sys.fn_xe_file_target_read_file( N'D:\XELogs\slow_watch*.xel', -- 通配符可一次读入所有滚动文件 NULL, -- 元数据文件,通常传 NULL NULL, -- 起始文件名,NULL 表示从头读 NULL -- 起始偏移,NULL 表示从文件开头 ) ) AS t ORDER BY event_time DESC;sys.fn_xe_file_target_read_file的四个参数依次是路径、元数据文件、起始文件名、起始偏移。日常用法就是第一个参数给通配符路径,后三个传 NULL。返回的event_data是 XML 字符串,需要用CAST(... AS XML)转换后再取值。
字段路径有个规律:事件自带的属性放在event/data[@name="..."]/value,通过 ACTION 附加的字段放在event/action[@name="..."]/value。duration是 data,session_id、database_id、client_app_name是 action,路径写错会返回 NULL,不报错,所以取出来是空的时候先检查路径。
duration的原始单位是微秒,除以 1000 得到毫秒。这一步很多人忘记,看到 250000 以为是 250 秒,实际是 250 毫秒,误判之后方向就跑偏了。
4. 接手遗留的服务器端跟踪:识别、迁移、干净下线
不少存量系统里还挂着 sp_trace_* 建的跟踪,运行了好几年没人管。这类跟踪往往开得很宽,文件一直涨,磁盘告警了才有人想起来。接手时不能直接删,得先摸清楚它在采什么、谁在用、能不能用事件会话等价替换。
4.1 先把现有跟踪摸清楚
第一步是列出实例上所有非默认跟踪。sys.traces是查这个的入口,is_default = 1的是系统默认跟踪,别动它。
-- 列出所有自定义跟踪及其文件路径、大小上限 SELECT t.id, t.status, -- 1 = 运行中,0 = 已停止 t.path, -- 跟踪文件所在目录 t.maxfilesize, -- 单文件上限(MB) t.stopat, -- 计划停止时间 t.buffer_journal -- 是否记录到缓冲区日志 FROM sys.traces AS t WHERE t.is_default = 0;拿到 traceid 之后,看它到底采集了哪些事件和列。事件和列在这里都是数字编号,得关联系统视图翻译成人话。
-- 把 traceid 为 2 的跟踪翻译成事件名 + 列名 SELECT ti.traceid, e.name AS event_name, c.name AS column_name FROM ::fn_trace_geteventinfo(2) AS ti JOIN sys.trace_events AS e ON e.trace_event_id = ti.eventid JOIN sys.trace_columns AS c ON c.trace_column_id = ti.columnid ORDER BY e.name, c.name;这一步的输出很关键。如果只看到SQL:BatchCompleted和SQL:StmtCompleted两个事件,加TextData、Duration、ApplicationName几列,那迁移到事件会话就是十几行代码的事;如果还带了Lock:Deadlock、Performance系列,就得逐个对照。
4.2 事件与列的迁移对照
服务器端跟踪的事件名和 Extended Events 的事件名不是一一对应,列的语义也得重新映射。下面这张表覆盖日常最常用的几组,照着改基本不会错。
| 服务器端跟踪事件/列 | Extended Events 对应 | 说明 |
|---|---|---|
| SQL:BatchCompleted | sqlserver.sql_batch_completed | 整批完成,不含内部语句拆分 |
| SQL:StmtCompleted | sqlserver.sql_statement_completed | 单条语句完成,排查首选 |
| RPC:Completed | sqlserver.rpc_completed | 过程调用完成,含参数 |
| SP:StmtCompleted | sqlserver.sql_statement_completed(sp 内部语句) | 迁移时一般并入语句事件 |
| Lock:Deadlock / Lock:Deadlock Chain | sqlserver.xml_deadlock_report | 死锁图,XML 格式 |
| 列 TextData | 动作 sqlserver.sql_text 或字段 statement | 前者完整语句,后者受谓词裁剪 |
| 列 Duration | 字段 duration(微秒) | 单位一致,无需换算 |
| 列 ApplicationName | 动作 sqlserver.client_app_name | 语义等价 |
| 列 LoginName | 动作 sqlserver.username | 语义等价 |
| 列 SPID | 动作 sqlserver.session_id | 语义等价 |
迁移时有个细节要注意:服务器端跟踪的Duration和事件会话的duration都是微秒,这一点不用换算;但sp_trace的TextData对超长语句会截断,事件会话默认保留更完整,样本体积会变大,所以迁移时最好同时把谓词加上,别原样照搬全量采集。
下面是等价迁移的示例,把上面查出来的那套“批完成 + 语句完成 + 文本 + 耗时 + 应用名”搬过去:
-- 等价迁移:从 sp_trace 的两事件三列,改造成事件会话 CREATE EVENT SESSION [legacy_trace_migrated] ON SERVER ADD EVENT sqlserver.sql_statement_completed( ACTION( sqlserver.sql_text, sqlserver.client_app_name, sqlserver.session_id, sqlserver.database_id ) WHERE ([duration] >= 1000000) -- 原跟踪只关心 1 秒以上,原样保留 ), ADD EVENT sqlserver.sql_batch_completed( ACTION( sqlserver.sql_text, sqlserver.client_app_name ) WHERE ([duration] >= 1000000) ) ADD TARGET package0.event_file( SET filename = N'D:\XELogs\legacy_migrated.xel', max_file_size = 128, max_rollover_files = 8 ) WITH ( MAX_DISPATCH_LATENCY = 10 SECONDS, STARTUP_STATE = OFF -- 先不自动启动,观察一轮再决定 );STARTUP_STATE这里故意设成 OFF。迁移过程中最怕的是新会话一上线就出问题还没人知道,先手动启动、观察一天事件量,确认没问题再改成 ON。这算是我吃过亏之后固定下来的习惯。
4.3 停止与清理,别留半截
新会话验证通过后,老跟踪要干净地停掉并删除。顺序是先停状态,再删除定义,两步都要做,只做第一步的话跟踪定义还在,重启实例可能又活过来。
-- 先停止(status = 0),再删除定义(status = 2) EXEC sp_trace_setstatus @traceid = 2, @status = 0; EXEC sp_trace_setstatus @traceid = 2, @status = 2;@traceid换成实际编号。如果跟踪是通过图形化工具挂在某个客户端上的,还要确认那个客户端已经断开,否则可能出现“删了又回来”的情况。删除之后回查sys.traces,确认记录消失。
历史文件别急着删。先挪到归档目录放一周,等业务方确认报表、审计都没受影响,再清理。这条不是技术问题,是流程问题,但翻车往往出在流程上。
4.4 写文件的权限与路径约束
不管走哪条路,最终都要往磁盘写。服务账户对目标目录没有写权限时,创建会话不会报错,启动也可能显示成功,但文件就是不出。排查方法很简单:看目标目录有没有文件生成,没有就换目录。
路径选择上,我倾向放在数据盘之外的独立目录,避免跟踪文件把数据文件所在卷写满。目录名带上会话名和日期,方便后续清理。另外,路径里不要出现中文和空格,某些版本的处理会有意外,用纯英文路径最稳。
5. 避坑与排查:五条踩出来的记录
这类工具用起来不难,难在出问题时现象不明显。下面五条都是我自己或身边人真实遇到过的,按“现象 → 原因 → 解决”写清楚,遇到了直接对号入座。
5.1 会话建好了,文件里一条数据都没有
现象:sys.dm_xe_sessions里能看到会话,状态是 started,但目标目录下没有 .xel 文件,或者文件是 0 字节。
原因通常有三个。一是谓词永远为假,比如字段名写错,或者数据库 ID 用了另一个环境的值。二是服务账户对目录没有写权限,创建阶段不校验,运行阶段才失败。三是流量本身就没达到阈值,200 毫秒的标准在小库上可能十分钟都碰不到一条。
解决顺序是:先把谓词整段去掉,重建一个无过滤的会话跑两分钟,看有没有数据;有数据说明是谓词问题,逐步加回条件定位;没数据就换目录,用实例备份目录试一次,验证权限。
5.2 打开跟踪之后 CPU 反而更高了
现象:本来只是想看看谁慢,结果会话一开,实例 CPU 从 70% 冲到 95%,业务方直接来问是不是在压测。
原因一般是事件开太多、动作字段太全、谓词太宽三者叠加。特别是把sql_text和query_plan同时开,再不加耗时过滤,等于让实例把每条语句的完整文本和执行计划都序列化一遍,开销非常可观。
解决办法是把采集窗口压到 5 到 10 分钟,用ALTER EVENT SESSION ... STATE = STOP及时收手;事件只留sql_statement_completed和rpc_completed;动作字段只留database_id、client_app_name、session_id、sql_text四个;谓词阈值先高后低。这套组合下来,多数业务库的开销可以压到可接受范围。
5.3 死锁图抓到了,但看不懂
现象:xml_deadlock_report采到了,打开一看是一大段 XML,进程节点、资源节点、等待关系全在里面,但不知道谁锁了谁。
原因是不熟悉死锁图的节点结构。XML 里每个<process>是一个参与者,带spid、lockMode、inputbuf;每个<resource>是争用对象;<deadlock-list>下的<victim>标记被回滚的那个。看懂这三个就够定位了。
快速定位的办法是把 XML 里的inputbuf全部提取出来,按 spid 排列:
-- 从死锁 XML 里提取每个参与进程的语句 SELECT p.value('@spid', 'int') AS spid, p.value('@lockMode', 'varchar(20)') AS lock_mode, p.value('(inputbuf)[1]', 'nvarchar(max)') AS input_buffer FROM ( SELECT CAST(event_data AS XML) AS evt FROM sys.fn_xe_file_target_read_file(N'D:\XELogs\slow_watch*.xel', NULL, NULL, NULL) ) AS t CROSS APPLY evt.nodes('//deadlock-list/deadlock/process-list/process') AS x(p) WHERE evt.value('(event/@name)[1]', 'sysname') = 'xml_deadlock_report';拿到两条语句之后,对照看加锁顺序是否相反,基本一眼就能判断是哪个事务先拿了不该拿的锁。死锁多数不是数据库的问题,是访问顺序不一致,改代码比调参数有用。
5.4 实例重启一次,会话就没了
现象:会话建好跑得好好的,某次实例维护重启之后,文件不再增长,查 DMV 也查不到。
原因是创建时STARTUP_STATE用了默认值 OFF。默认不随实例启动,重启即失效,定义还在但状态是停止。
解决办法是改成 ON 并重新启动会话:
ALTER EVENT SESSION [slow_watch] ON SERVER WITH (STARTUP_STATE = ON); ALTER EVENT SESSION [slow_watch] ON SERVER STATE = START;改之前先想清楚:这个会话是要长期跑还是临时排查。临时排查的会话改 ON 反而危险,忘了关就一直写盘。我的习惯是排查类会话一律 OFF,跑完即停;只有明确要长期采集的才开 ON。
5.5 读文件时报占用或者权限错误
现象:用 T-SQL 读 .xel 时报“拒绝访问”或者“文件正在使用”,换成图形界面也打不开。
原因有两个。一是文件正被会话写入,某些工具不允许同时读;二是执行读取的登录账号没有文件系统权限,fn_xe_file_target_read_file是以 SQL Server 服务账户身份访问文件的。
解决办法是先把会话停掉再读,或者读滚动出来的历史文件而不是当前写入的那个。权限问题就找运维加目录读取权限,或者干脆把文件拷到临时目录再用本地实例读——后者在跨环境分析时特别顺手,也避免了在生产实例上跑解析查询。
提示:拷 .xel 到本地分析时,注意本机实例的版本要能识别文件格式,直接连生产读反而更省事。
6. 进阶:把散落的事件压成一张能直接看的报表
原始事件读出来是流水账,一条一条看效率极低。真正省时间的是聚合:按语句文本分组,算执行次数、总耗时、平均耗时、最大耗时,排序取前若干条。下面这段是我每次分析必跑的,直接从 .xel 出 TOP 榜。
-- 按语句文本聚合,找出“总耗时最高”和“单次最慢”的两类问题语句 WITH raw AS ( SELECT CAST(event_data AS XML) AS evt FROM sys.fn_xe_file_target_read_file(N'D:\XELogs\slow_watch*.xel', NULL, NULL, NULL) ), parsed AS ( SELECT evt.value('(event/data[@name="statement"]/value)[1]', 'nvarchar(max)') AS stmt, evt.value('(event/data[@name="duration"]/value)[1]', 'bigint') / 1000.0 AS duration_ms, evt.value('(event/action[@name="client_app_name"]/value)[1]', 'nvarchar(128)') AS app_name, evt.value('(event/@timestamp)[1]', 'datetime2') AS ts FROM raw WHERE evt.value('(event/@name)[1]', 'sysname') IN ('sql_statement_completed', 'rpc_completed') ) SELECT TOP 20 app_name, stmt, COUNT(*) AS exec_count, SUM(duration_ms) AS total_ms, AVG(duration_ms) AS avg_ms, MAX(duration_ms) AS max_ms FROM parsed GROUP BY app_name, stmt ORDER BY total_ms DESC;这段查询的逻辑是两段式:先把 XML 拆成关系型字段,再做分组聚合。statement取语句文本,duration除 1000 换成毫秒,client_app_name用来区分来源。IN条件把两类事件合并到一张结果里,避免分开看时漏掉 RPC 调用。
参数上有两点可以按需调整。一是TOP 20,样本多的时候改成 50 更有覆盖度,但结果会更长。二是分组键,按stmt分组会把参数不同但结构相同的语句分开,如果想合并同类项,可以把stmt换成语句的前 100 个字符,或者按app_name先粗分再细看。
聚合结果里有两列值得特别关注。total_ms高说明这类语句累计拖慢了整体,通常出现在高频小查询上;max_ms高说明存在偶发尖刺,往往和锁等待、参数嗅探或统计信息过期有关。两种问题的处理方向完全不同,前者优化索引或减少调用次数,后者要去看执行计划和等待类型。
跑完聚合,我还会加一步交叉验证:拿total_ms最高的两三条语句,回原始事件里看它们的执行时间分布。如果集中在某个时间窗,多半是批处理或定时任务叠加;如果均匀分布一整天,那就是常态化的性能问题,得从索引和语句本身下手。
最后说个习惯。从那以后我每次开事件会话,都会强制走一遍固定动作:先确认服务账户对目录有写权限,再确认谓词字段名拼对,然后无过滤跑两分钟验证通路,最后才加阈值正式采。这套动作花不到五分钟,能挡掉八成的“开了但没数据”。希望帮到你。
本文还有配套的精品资源,点击获取