简介:SQL Server Profiler是数据库管理员常用的图形化性能监控工具,这份配套文档系统讲解了其在SqlServer2000下的使用方法与优化技巧。文档面向需要诊断数据库性能瓶颈、排查死锁和慢查询的DBA与开发人员,内容按打开工具、创建跟踪、事件与数据列选择、过滤条件设置、查看分析跟踪及模板定制等章节逐步展开,并专门介绍了MSDN相关分析方法、Read80trace工具的Normalization功能,以及通过usp_GetAccessPattern存储过程分析Normalize后数据并定位HOT数据库的高级技巧,可直接用于生产环境性能调优。资源为单个doc格式文档,压缩包仅308KB,内容精炼但覆盖了从基础监视到深层分析的全流程。目前已有597人学习使用,适合作为SQL Server性能监控的速查与进阶参考资料,尤其对需要维护历史版本SQL Server的运维人员和开发人员有实用价值。
1. 一份《SqlServer2000性能工具Profiler.doc》旧文档,为什么今天还值得照着做
一份名为《SqlServer2000性能工具Profiler.doc》的旧文档放到现在,第一眼确实劝退:SqlServer2000 是二十多年前的产品,Profiler 也早被新版本里的 DMV、查询存储和扩展事件挤到了角落。但如果你手里正好有一台老库,或者要从一套历史遗留系统里揪出那个跑了好几年都说不清道不明的慢查询,这份文档描述的技术依然能直接落地。SqlServer2000 的 Profiler 本质上是一个事件捕获器,能把服务端正在执行的 SQL、存储过程调用、锁等待与死锁信息原样抓出来,并给出耗时、CPU、Reads/Writes 这些关键指标。下面按我做老库维护时实际用下来的方案,把这套工具从客户端追踪到服务端脚本、从筛选参数到结果分析完整拆开讲。
2. 为什么 SqlServer2000 的性能调优绕不开 Profiler:工具定位与选型理由
2.1 没有 DMV 也没有扩展事件:Profiler 是那个年代唯一能看见 SQL 的探针
SQL Server 2000 时代的性能诊断,和现在完全是两回事。现在遇到慢查询,第一反应是查 sys.dm_exec_query_stats、看等待统计、翻查询存储;而 2000 里这些一概没有,管理员手边能用的工具屈指可数:Windows 性能监视器看 CPU、内存、磁盘队列,DBCC 命令看碎片和缓冲,查询分析器看执行计划,剩下的就只有 Profiler。性能监视器能把压力说清楚,却说不清是哪条 SQL 在制造压力;DBCC 能看结构问题,也看不到“某一个时刻到底谁在跑”。恰恰是 Profiler 能在不中断业务的前提下,把服务端正在执行的语句一条条列出来,带 CPU、Reads、Writes、Duration 这些资源消耗字段。这就是它被放进“性能工具”里的根本原因。
我见过不少刚接手老项目的同事,面对一台 SQL Server 2000 实例一脸茫然,第一反应是去装新版本客户端来连,结果连不上。其实 2000 的 Profiler 不是独立安装包,它随 SQL Server 2000 客户端工具一起安装。中文版一般在开始菜单 Microsoft SQL Server 组里叫“事件探查器”,英文版叫 SQL Server Profiler。如果服务器上找不到这个入口,多半是只装了数据库服务器组件没装客户端工具,需要补装客户端组件才能看到。这个入口问题看起来基础,但实际排查时卡住半小时的案例我见过不止一次。
2.2 客户端追踪与服务端追踪:线上抓取直接选后者
Profiler 的“追踪”有两种完全不同的运行方式,文档里如果只字不提,容易在第一步就埋下隐患。第一种是客户端追踪,也就是启动 Profiler 图形界面,连上实例后新建一个跟踪,事件流实时汇入界面。这种方式直观、门槛低,适合开发环境或者临时盯一屏输出;缺点同样明显,事件要先经过客户端进程过滤和渲染,网络抖动、笔记本休眠、内存不足都会丢事件,而且图形界面本身对服务器也有额外压力。第二种是服务端追踪,用系统存储过程在服务端定义追踪,事件由 SQL Server 进程直接写入文件,客户端断开也不影响抓取,这也叫“脚本化追踪”。两种方式的核心差异可以看下面这张表。
| 对比项 | 客户端追踪 | 服务端追踪 |
|---|---|---|
| 追踪运行位置 | Profiler 客户端进程 | SQL Server 服务进程 |
| 断线表现 | 客户端断线即停,事件丢失 | 客户端断开,追踪继续 |
| 文件滚动 | 界面勾选,容易漏设 | @Options=2 自动切换新文件 |
| 对线上库额外压力 | 较高,界面渲染也占资源 | 较低,事件直接写文件 |
| 适用场景 | 开发环境、临时盯一屏 | 线上库、长时间抓取 |
我的习惯是:只要目标是生产库,或者打算抓超过十分钟的数据,一律走服务端追踪。早期我在笔记本上开客户端追踪抓线上问题,中午去吃饭笔记本休眠,回来发现追踪早就静默停了,一下午白干,从那以后就长了记性。
2.3 用 SqlServer2000 Profiler 前先对齐三个旧版认知
在动手之前,有三个容易被新版本经验误导的差异点,值得先说清楚。第一个是 Duration 的单位。SQL Server 2000 Profiler 里 Duration 数据列的单位是毫秒,而 SQL Server 2005 之后改成了微秒。同样的“Duration > 1000”,在 2000 里表示超过 1 秒,在 2005 里则表示超过 1 毫秒,套错单位会让筛选条件形同虚设。第二个是事件命名风格。2000 的事件类形如 SQL:BatchCompleted、RPC:Completed、Lock:Deadlock Chain,带冒号分层;新版本扩展事件的命名和组织方式完全不同,不能用新思路直接找。第三个是版本兼容性。2000 的 Profiler 客户端只能追踪 SQL Server 2000 实例,抓 2000 的库就得用装着 2000 客户端工具的机器,别的版本连不进去。这三个认知对齐了,后面的配置才不会来回返工。
3. 用 Profiler 建立线上追踪:模板、事件类、数据列与筛选器参数
3.1 新建追踪的四个关键选项:模板、保存位置、文件上限与滚动更新
客户端追踪的入口路径基本固定:打开 Profiler,文件菜单里选择新建跟踪,连接到目标实例,随后弹出“跟踪属性”窗口。第一次用的人往往盯着这个窗口发呆,因为里面的选项并不少。按优先级排,真正需要先定下来的只有四件事:模板、保存位置、单文件上限、是否滚动更新。
模板建议选 Standard,如果你主要查存储过程相关性能,可选 TSQL_SPs。2000 自带的模板数量很少,和后来版本里几十个预置模板没法比,选哪个都只是起点,最终事件类都要手动调整。保存位置方面,客户端追踪时文件默认保存在运行 Profiler 的这台机器上;服务端追踪时文件路径则要写在数据库服务器上。很多人第一次就把路径写错,抓了半天发现文件根本没生成,原因就在这层没分清。单文件上限我一般设 20MB,太小会频繁滚动生成一堆碎片文件,太大又不好收尾。最关键的是勾选“启用文件滚动更新”,不勾的话,文件写满 20MB 后追踪会自己停下来,界面没有任何提示,这是 2000 里最常见的静默翻车点。
设置完保存选项后,进入“事件类”标签页,左边是事件分类树,右边是数据列选择,底部还能打开筛选窗口。全部配好后点运行,追踪就开始工作了。需要提醒的是,2000 的追踪一旦启动,配置就不允许再改,想调整筛选条件只能停止后重新建一个追踪。所以在点“运行”之前,把模板、事件、数据列和筛选都检查一遍,比什么都重要。
3.2 事件类与数据列的取舍:Completed 系列是主力,Showplan 慎开
在“事件类”标签页里,最直观的诱惑是把“显示所有事件”勾上,这样能看到所有分类和事件名,但千万别全选。全选的结果是噪音淹没信号,抓出来的文件满是连接、断开、审计和错误信息,真正的慢 SQL 反而被淹没。实际使用中,我通常只勾下面这几类。
| 事件类 | 所在分类 | 作用 | 建议 |
|---|---|---|---|
| SQL:BatchCompleted | SQL | 一条批处理执行完成后的耗时与资源 | 默认勾选 |
| RPC:Completed | 存储过程 | 一次 RPC 调用(主要是存储过程调用)完成信息 | 默认勾选 |
| SQL:StmtCompleted | SQL | 批处理内部单条语句完成信息 | 分析单条语句时勾 |
| Showplan XML | 性能 | 输出执行计划,文本量巨大 | 默认不勾 |
| Lock:Deadlock / Lock:Deadlock Chain | 锁分类 | 死锁相关的进程与资源信息 | 排查锁问题时勾 |
为什么要选带 Completed 的事件而不选 Starting?因为 Starting 事件只有开始时刻,没有耗时、CPU、Reads 这些结果数据。性能定位要看的是“一条 SQL 跑完花了多少资源”,而不是“它开始了”。至于 Showplan XML,它会把执行计划完整写进 TextData,一条复杂查询的计划能膨胀到几百上千字符,抓一小时文件轻松超过几百 MB,除非你专门在调执行计划,否则不要勾。
数据列方面,TextData、EventClass、Duration、CPU、Reads、Writes、SPID、StartTime、DatabaseName、ApplicationName 这十列基本够用。TextData 是 SQL 文本,EventClass 用来区分事件类型,Duration/CPU/Reads 是排序和聚合的主要依据,DatabaseName 和 ApplicationName 用来过滤业务范围。数据列不是越多越好,每多一列,追踪输出和文件体积都会成比例上涨。
3.3 筛选器设置:排除 Profiler 自身,把 Duration 阈值卡在毫秒量级
筛选器是整个追踪配置里最值得花时间的地方。在跟踪属性窗口的事件类标签页底部,有一个“筛选”按钮,点开后左侧列出数据列,右侧是运算符和值。2000 的筛选器虽然简陋,但基础的等于、大于、小于、包含都支持。我固定会加三组条件:第一,ApplicationName 不等于“SQL Server Profiler”,排除追踪工具自身的查询;第二,DatabaseName 等于目标业务库名,避免多个库共用一个实例时抓到无关语句;第三,Duration 大于 1000,也就是只抓超过 1 秒的语句。如果目标是排查逻辑读压力,也可以再加一个 Reads 大于 500 的条件。
这里有三个 2000 特有的限制。筛选器是全局生效的,对所有已选事件统一过滤,你没法单独给 Lock:Deadlock 设一个阈值、再给 SQL:BatchCompleted 设另一个阈值。其次,筛选器不能实现“某个事件不参与过滤”这种细粒度控制。最后,追踪启动后筛选器不可修改,想调整只能停掉重建。所以在点运行前,把这三组条件反复核对一次,尤其是单位问题:2000 的 Duration 是毫秒,“大于 1000”就是大于 1 秒,不要拿着 2005 后的微秒习惯来设值。
4. 把 Profiler 追踪脚本化:sp_trace_create 建立服务端追踪与文件读取
4.1 为什么服务端追踪更可靠:断线、丢事件与文件滚动的差别
如果你已经按第 3 章的步骤配置了一个客户端追踪,并且成功抓到了数据,那下一步值得做的事情,是把这个追踪“搬到服务端”。客户端追踪最大的问题在于它依赖图形界面进程的稳定性。我经历过最典型的一次:下午两点开始抓,三点去看发现界面还在,但文件已经四个小时没写了,原因是当时连接会话被网络策略断开,Profiler 客户端进入重连状态,事件流全部丢失。服务端追踪不存在这个问题,它由 SQL Server 进程写文件,客户端只是下发了一个定义,之后哪怕关闭 Profiler,追踪照样在跑。
把当前客户端追踪配置转换成服务端脚本,操作上也有现成入口。在 Profiler 的文件菜单里找“导出”或“另存为”相关选项,选择生成 SQL 脚本,工具就会把当前事件类、数据列、筛选器的定义翻译成一组系统存储过程调用。2000 的菜单在不同语言版本里位置略有差异,但核心是“把跟踪定义保存为脚本”。生成出来的脚本一般很长,因为 sp_trace_setevent 会把每个事件与每个数据列的组合逐行展开,一个中等配置生成几百行非常正常,这是正常的,不要手工精简。
执行这份脚本需要 sysadmin 权限,并且脚本里的文件路径是数据库服务器本机的路径。执行前先把目录建好,确认 SQL Server 服务账户对该目录有写权限,否则追踪创建成功后一启动就报错,错误信息还藏在系统日志里,不容易发现。
4.2 用 sp_trace_create 定义追踪的最小脚本:参数说明与启动停止
从 Profiler 导出的脚本很长,不方便在文章里完整贴出来,但核心骨架就是下面这几段。第一次接触的人,看这个最小示例就能理解服务端追踪的运行逻辑。事件编号和数据列编号不要靠记忆写,以你自己机器上导出的脚本为准,下面代码只是演示。
-- 创建服务端追踪:输出到 D:\Trace\app_trace,单文件上限 20MB,启用滚动 DECLARE @TraceID int EXEC sp_trace_create @TraceID = @TraceID OUTPUT, @Options = 2, -- 2 表示文件滚动,写满自动生成新文件 @TraceFile = N'D:\Trace\app_trace', -- 不写扩展名,SQL Server 自动加 .trc @MaxFileSize = 20, -- 单文件上限,单位 MB @StopTime = NULL, -- 不设自动停止时间 @FileCount = 5 -- 最多保留 5 个滚动文件 GO -- 绑定事件与数据列。事件编号和数据列编号由 Profiler 导出脚本自动生成,这里只列常用组合 EXEC sp_trace_setevent @TraceID, 10, 1, 1 -- RPC:Completed -> TextData EXEC sp_trace_setevent @TraceID, 10, 11, 1 -- RPC:Completed -> Duration EXEC sp_trace_setevent @TraceID, 10, 13, 1 -- RPC:Completed -> CPU EXEC sp_trace_setevent @TraceID, 10, 16, 1 -- RPC:Completed -> Reads EXEC sp_trace_setevent @TraceID, 12, 1, 1 -- SQL:BatchCompleted -> TextData EXEC sp_trace_setevent @TraceID, 12, 11, 1 -- SQL:BatchCompleted -> Duration EXEC sp_trace_setevent @TraceID, 12, 13, 1 -- SQL:BatchCompleted -> CPU EXEC sp_trace_setevent @TraceID, 12, 16, 1 -- SQL:BatchCompleted -> Reads GO -- 启动追踪:状态 1=启动,0=停止,2=关闭并删除定义 EXEC sp_trace_setstatus @TraceID, 1 GO这段脚本的逻辑分成三步:先创建追踪得到一个追踪 ID,然后把需要的事件和数据列绑定到这个 ID 上,最后启动它。@Options 参数是滚动更新的开关,设成 2 时文件写满会自动切换新文件,这也是线上长时间抓取必须设置的参数。@TraceFile 参数注意不要带扩展名,SQL Server 会在第一个文件上自动加 .trc,滚动后的文件会变成 app_trace_1.trc、app_trace_2.trc 这种命名。@MaxFileSize 的单位是 MB,最小可以设 1,实际建议 20 到 50 之间。@FileCount 表示滚动文件数量上限,超过后最早的滚动文件会被覆盖,所以磁盘规划要按“单文件上限 × 文件数”再加余量来留。
停止服务端追踪时,正确的顺序是先停止再关闭定义。用 sp_trace_setstatus 传 0 停止追踪,追踪定义还留在服务端;再传 2 才能关闭并释放资源。如果抓完数据只停在停止状态,不清理定义,长时间挂机还是会占用服务端资源。养成抓完就三步走——停止、关闭、确认文件生成——的习惯,比什么都强。
-- 停止追踪并清理定义 EXEC sp_trace_setstatus @TraceID, 0 EXEC sp_trace_setstatus @TraceID, 2 GO4.3 用 ::fn_trace_gettable 回读追踪文件:下一步分析的入口
服务端追踪产生的 .trc 文件,除了可以在 Profiler 里直接打开,更实用的读取方式是使用系统函数 ::fn_trace_gettable。这个函数可以把追踪文件当表来查询,方便按事件编号、耗时、CPU 排序,也可以导出成 CSV 做进一步分析。下面是一段最常用的读取查询。
-- 读取追踪文件中的慢语句,按耗时倒序 SELECT TOP 100 EventClass, TextData, Duration, CPU, Reads, SPID, StartTime FROM ::fn_trace_gettable(N'D:\Trace\app_trace.trc', default) WHERE EventClass IN (10, 12) -- 10=RPC:Completed,12=SQL:BatchCompleted ORDER BY Duration DESC GO这里几个点需要解释一下:fn_trace_gettable 是 SQL Server 2000 的系统表值函数,调用时必须带双冒号前缀,这个写法在新版本里已经不常见了。第二个参数 default 表示读取文件本身;如果开了滚动更新,文件不止一个,这个函数在 2000 里对多文件的支持有限,常见做法是把主文件和滚动文件复制到同一个目录后按顺序改名读取,或者直接用 Profiler 图形界面去打开主文件,它会自动加载关联的滚动文件。EventClass 数字与事件名的对应关系,可以通过服务端目录视图或早期文档查,但更简单的办法是先用 Profiler 打开文件看一眼确认。TextData 列是 ntext 类型,排序和导出时如果需要完整文本,建议在 SELECT 里显式转成 nvarchar,否则不同工具处理起来容易出截断和乱码。
5. Profiler 避坑指南:5 个高频翻车点与排查方法
5.1 TextData 被截断:只看到 SQL 前半句,整段语句拼不齐
现象:抓回来的追踪里,很多 TextData 内容停在两三百个字符就断了,一条很长的 UPDATE 只能看到前半段,复制出来根本没法还原完整语句。
原因:客户端追踪在界面展示和保存时,为了控制内存与显示开销,对长文本列做了截断处理。这不是服务端数据本身的问题,而是 Profiler 客户端为了交互体验加的限制。
解决:改用第 4 章的服务端追踪,事件由服务端直接写文件,TextData 保存的是完整文本。已经用客户端抓出来的半截数据没有后悔药,只能重新抓。判断 TextData 是否完整有一个简单办法:看末尾有没有正常结束符,或者直接把 SQL 文本长度和 StmtCompleted 之类的配套信息对照一下。
5.2 死锁图在 SqlServer2000 里显示不出来:勾了事件仍一片空白
现象:为了排查死锁,跟踪里明明勾选了 Lock:Deadlock,死锁也确实发生了,但界面上看不到任何图形,只有一堆看不懂的进程编号和对象文本。
原因:SQL Server 2000 的死锁信息需要同时勾选 Lock:Deadlock 和 Lock:Deadlock Chain 两个事件类,只勾前者拿不到完整的锁等待链;而且 2000 的死锁展示机制很弱,不像 2005 之后有独立的死锁图页签。
解决:两个事件类一起勾上。抓到死锁后,在结果行上右键选择“提取事件数据”,把进程和资源信息转存为文本或 HTML 文件再慢慢看。分析时把两个 SPID、各自占用的对象、等待的资源对应起来,基本就能还原死锁环。
5.3 追踪文件写满自动停止:高峰时段数据断档,原因在滚动参数
现象:早上 8 点启动追踪,中午去看发现文件最后写入时间停在 8 点 10 分,之后没有任何数据,但数据库明显一直在忙。
原因:文件达到 20MB 上限后追踪自动停止,而且 2000 的界面不会弹提示。客户端追踪没勾“启用文件滚动更新”,服务端追踪的 @Options 不是 2,都会触发这个行为。
解决:客户端追踪在保存设置里勾上文件滚动更新;服务端追踪把 sp_trace_create 的 @Options 参数设成 2。同时磁盘空间要按预期抓取时长预留,20MB 的上限在忙碌系统里可能十分钟就写满,留足余量才能覆盖完整高峰期。
5.4 抓进大量 Profiler 自会话:筛选条件没生效,噪音刷屏
现象:追踪结果里混进大量来源为 SQL Server Profiler 的短小命令,比如一些系统维护语句,把业务 SQL 淹没了,根本没法看。
原因:筛选器里没有排除 ApplicationName 为 SQL Server Profiler 的会话,也没用 DatabaseName 或 LoginName 限定业务范围,导致工具自身的活动也被记录下来。
解决:在筛选器里加一条 ApplicationName 不等于“SQL Server Profiler”的条件,再按实际业务加 DatabaseName 等于目标库名。如果老应用的 ApplicationName 没有固定值,就改用 LoginName 或 HostName 来圈定业务范围。注意 2000 的筛选器对某些系统内部活动是挡不住的,尽量通过缩小事件类范围来减少噪音。
5.5 重放结果把线上数据写坏:回放只能指向一次性测试环境
现象:为了验证某个 UPDATE 是否会导致锁等待,有人把 Profiler 抓到的文件直接拿到测试库上点“重放”,结果测试库里的真实数据全被改了,更严重的是有人拿错了文件重放到生产环境。
原因:Profiler 的 Replay 功能是真实执行捕获的语句,不是只模拟执行计划,它会把 INSERT、UPDATE、DELETE 再原样跑一遍。
解决:重放只允许在一次性还原出来的专用测试副本上做,并且执行前确认服务器名、数据库名都和捕获环境完全不同。生产环境抓取的文件只做分析,不点重放。我现在的习惯是,抓到需要验证的语句后,把单条 SQL 手工抽取出来,在事务里跑并回滚,而不是整文件重放。
6. 从 Profiler 文件到结论:SQL 模板归一化与性能基线对比
追踪文件拿到手,最值钱的不是一条条看 SQL,而是把几百条、上千条语句汇总成少数几个“SQL 模板”,找到真正消耗资源的模式。这里我一般会把 fn_trace_gettable 的结果导出成 CSV,再用一段小脚本做归一化聚合,逻辑很简单:把语句里的数字字面量和字符串字面量统一替换成占位符,然后按归一化后的文本分组,累计次数、总耗时和总 CPU。
import re import csv from collections import defaultdict def normalize(sql): if not sql: return '' # 字符串字面量替换为占位符 s = re.sub(r"'\\.*?'", "'?'", sql) # 数字字面量替换为占位符 s = re.sub(r"\\b\\d+\\b", "? ", s) # 压缩空白,只保留前 200 字符用于分组 s = re.sub(r"\\s+", " ", s) return s[:200] agg = defaultdict(lambda: [0, 0, 0]) # [次数, 总耗时, 总CPU] with open('trace.csv', newline='', encoding='gbk') as f: reader = csv.DictReader(f) for row in reader: key = normalize(row['TextData']) if not key: continue agg[key][0] += 1 agg[key][1] += int(row['Duration'] or 0) agg[key][2] += int(row['CPU'] or 0) for sql, (cnt, dur, cpu) in sorted(agg.items(), key=lambda x: -x[1][1])[:20]: print(cnt, dur, cpu, sql)这段脚本做的事情就是把相同模板的语句归到一起。比如“SELECT * FROM orders WHERE order_id = 10001”和“SELECT * FROM orders WHERE order_id = 20002”,归一化后都是“SELECT * FROM orders WHERE order_id = ?”,会归到同一组。分组后按总耗时排序,排在最前面的那几条才是真正要优化的对象。CSV 导出时注意编码,SQL Server 2000 中文环境导出的文件用 GBK 读取比较稳,这一点在 Python 里已经通过 encoding 参数处理了。
归一化之后,下一步是性能基线对比。做法很简单:优化前在业务高峰期抓 15 分钟,记录前几个模板的累计 Duration、平均 Duration 和总 Reads;做完索引调整或 SQL 改写后,同样条件下再抓 15 分钟,对比同一组模板的指标变化。数量级差距一目了然,是能给业务方和开发看的硬数据。这套归一化加基线对比的流程,我在老库上用了很多次。以前排查一个订单系统的慢查询,第一次抓到一千二百行,归一化后只剩 14 个模板,前三个模板占了 80% 的 Duration;改完索引再抓一次,同样 15 分钟,前三个模板的总耗时从 87000 毫秒降到 13000 毫秒。如果你也接手了这样的老库,记得先建服务端追踪再去吃饭。开着客户端追踪在办公室里过夜这种事,断一次线,一下午就白抓了。希望帮到你。
本文还有配套的精品资源,点击获取