简介:这份文档面向 SQL Server 数据库管理员与开发人员,聚焦 SQL Server 2000 环境下性能监视工具 Profiler 的实战应用,帮助排查死锁、致命错误、慢查询与存储过程效率等典型问题。资源包内含 1 个 doc 文件,压缩包约 308KB,篇幅紧凑,适合作为随查随用的工具手册。内容从打开 Profiler、新建跟踪、设置事件与数据列、配置过滤条件讲起,逐步延伸到跟踪结果的查看与分析,并覆盖跟踪模板的创建与复用。文档还整理了 MSDN 相关介绍,包括 Profiler trace 文件性能分析的传统方法及其局限、Read80trace 工具的 Normalization 功能、利用存储过程分析规范化数据、usp_GetAccessPattern 的使用技巧,以及如何判断 HOT 数据库。读者可据此掌握实时监视、事件跟踪、数据存储与性能瓶颈定位的完整思路,并借助模板与工具链提升 SQL 语句和存储过程的执行效率。目前已有 597 人学习下载。
1. 从一次 CPU 打满的故障说起:这份 Profiler 文档能帮你做什么
接手一套跑了七八年的业务系统,应用端反馈“页面转圈”,登录服务器一看,8 个 CPU 全在 90% 以上波动,内存和磁盘反倒很闲。这种时候最怕凭感觉猜,猜错方向改半天索引,问题还在。我当时的做法是挂上 SQL Server Profiler 采样两小时,再用文档里提到的 Read80trace 做标准化,最后跑一遍usp_GetAccessPattern,报表出来一眼就看到某个存储过程两小时内只跑了 75 次,却吃掉了 90.8% 的 CPU。定位到这一条,优化方向就清楚了。
这份《SqlServer2000性能工具Profiler.doc》讲的正是这套从抓取到分析的完整链路。它不是单纯教你怎么点开 Profiler 菜单,而是把“抓 Trace → 标准化 → 按访问模式归类统计 → 找热点语句和热点库”串成了一条可复现的路径。适合两类人:一是还在维护 SQL Server 2000/2005 老系统的 DBA,二是需要从海量 Trace 里快速找出关键 SQL 的后端工程师。如果你手上正好有性能问题要查,这份文档里的存储过程和命令行参数可以直接抄来用。
2. Profiler 抓取与跟踪配置:事件、数据列和过滤条件怎么定
2.1 打开 Profiler 并建立第一条跟踪
文档第 1 章给的入口路径是企业管理器里tool → SQLServer Profiler → New Trace,连上目标服务器后直接 Run。这个流程在 SQL Server 2000 的 Enterprise Manager 里是通的,如果你用的是 SSMS,入口变成Tools → SQL Server Profiler,逻辑一样。新建跟踪时会弹出一个 Trace Properties 对话框,里面几个关键选项直接决定后面分析顺不顺手。
跟踪名称和服务器不用多说,重点是输出方式。文档里列了Capture to file和Capture to table两种。我的血泪经验是:生产环境一律选 Capture to file,把.trc落到本地磁盘。Capture to table 会把每一行事件写进数据库表,跟踪本身就在给数据库加负载,很容易把一次性能排查变成二次故障。文件方式内存开销小,后面用 Read80trace 处理也更方便。
-- 如果你不想开图形界面,也可以用扩展存储过程建跟踪 -- 下面这段是常见做法,先建一个输出到文件的跟踪 DECLARE @rc int DECLARE @TraceID int DECLARE @maxfilesize bigint SET @maxfilesize = 50 -- 单位 MB,单个文件到 50MB 就滚动 EXEC @rc = sp_trace_create @TraceID OUTPUT, 0, N'D:\trace\perf_trace', @maxfilesize, NULL SELECT @rc AS rc, @TraceID AS TraceID这段代码里sp_trace_create的第二个参数 0 表示不启用文件滚动之外的特殊选项,第三个参数是文件路径前缀,SQL Server 会自动加.trc后缀。@maxfilesize设 50MB 是为了防止单个文件太大不好处理,Read80trace 能顺序读取perf_trace.trc、perf_trace_1.trc这一系列文件。建完跟踪后还需要用sp_trace_setevent逐个添加事件和列,再用sp_trace_setstatus启动,步骤比图形界面繁琐,但适合做成脚本反复用。
2.2 事件选择:只抓你真正要看的
文档 2.1 节列了一串值得监视的事件:登录连接的成功失败断开、DELETE/INSERT/UPDATE、RPC 状态、存储过程开始结束、错误日志写入、游标打开、锁的添加释放。同时明确说了一句“不监视过多的事件”,因为每多一个事件类,跟踪文件就大一圈,系统负担也重。
我一般分两轮抓。第一轮用 Standard 模板跑,拿到整体轮廓;第二轮针对可疑模块,只留SQL:BatchCompleted、RPC:Completed、SP:StmtCompleted这几个,把噪音压下去。文档第 3 章的模板表里,TSQL_Duration模板专门按执行时间分组,适合找慢查询;TSQL_SPs模板把存储过程的每一步都抓出来,怀疑过程重编译时再加SP:Recompile事件。模板不是拿来就用,得根据你当前要回答的问题选。
2.3 数据列与过滤条件:把 Reads、CPU、Duration 留下来
Data Columns 标签页决定每一行事件记录哪些字段。文档说“通常选择那些我们比较关心的列”,这句话背后有个硬性要求:如果你后面要用usp_GetAccessPattern做统计,reads、cpu、duration、writes这四列必须勾上,否则存储过程里sum(cpu)、sum(reads)全是空。另外TextData列也要留,标准化后的语句文本就靠它。
Filters 标签页是控制跟踪体积的关键。常见做法是按Duration过滤,比如只抓大于 1000 毫秒的语句;或者按DatabaseName过滤,只盯某个库。但要注意,过滤条件设得太狠,可能把最频繁的那类短查询全滤掉了。文档 4.5 节专门提醒过:运行最多最密的语句未必有超过 1000 毫秒的执行时间,带参数运行的结果有可能不包括最频繁语句。所以第一轮抓取时我建议不加 Duration 过滤,先把全量拿到手,分析阶段再用@duration_filter参数去切。
3. Read80trace 标准化与 usp_GetAccessPattern 统计:从 GB 级 Trace 到访问模式报表
3.1 为什么传统分析方法在 GB 级 Trace 面前会翻车
文档 4.2 节把传统方法说得很透:一种是用 Profiler 自带的 Filter 和排序,另一种是把 Trace 导入数据库用 T-SQL 统计。Trace 小的时候这两种都行,但文档举了个例子——4 个 500MB 以上的 trace 文件,这两种方法就非常吃力。原因有两个:一是文件太大,你很容易被某几条耗时语句吸引,把精力耗在不是真正关键的地方;二是大量语句模式类似、只是参数不同,没有简单办法把它们归到一起统计。
这个“模式类似、参数不同”的问题,就是标准化的切入点。比如下面三条语句:
select * from authors where au_lname = 'white' select * from authors where au_lname = 'green' select * from authors where au_lname = 'carson'在访问模式层面它们是同一类查询,但直接 group by 文本会得到三行。Read80trace 的 Normalization 功能把它们统一成select * from authors where au_lname = {str},这样统计才有意义。
3.2 Read80trace 命令行参数与执行
文档给的命令是:
Read80trace –f –dmydb –Imytrace.trc三个开关的含义:-f表示不生成 RML 文件,因为不需要重放功能,RML 文件体积很大,不需要就关掉;-d指定处理结果写入的数据库名,后面创建的存储过程就是访问这个库里的表;-I指定要分析的 trace 文件。Read80trace 会自动识别同目录下的mytrace.trc、mytrace1.trc、mytrace2.trc并顺序处理。另外-i开关可以直接从 zip 或 CAB 文件里读 trace,不用手动解压。
执行完之后,mydb库里会生成两张关键表:tblBatches存放所有语句,tblUniqueBatches存放标准化后的语句,两张表通过HashId关联。文档提到在 512MB 内存的老机器上分析几个 GB 的 trace 文件不到一小时,这个性能在当年算相当能打。我自己的习惯是先把 trace 文件按时间段分目录,每个目录单独跑一次 Read80trace,避免一次处理太多文件时中途出错要重来。
3.3 usp_GetAccessPattern 存储过程拆解
文档 4.4 节给出了完整的存储过程定义。它的核心逻辑分两步:先算出所有语句的 CPU、Reads、Writes、Duration 总和,再按标准化文本 group by,算出每一类语句的占比。
Create procedure usp_GetAccessPattern @duration_filter int=-1 as begin declare @sum_total float,@sum_cpu float,@sum_reads float, @sum_duration float,@sum_writes float select @sum_total=count(*)*0.01, @sum_cpu=sum(cpu)*0.01, @sum_reads=sum(reads)*0.01, @sum_writes=sum(writes)*0.01, @sum_duration=sum(duration)*0.01 from tblBatches where duration>=@duration_filter Select ltrim(str(count(*))) exec_stats, ''+ str(count(*)/@sum_total,4,1)+'%' ExecRatio, ltrim(str(sum(cpu)))+' : '+ltrim(str(avg(cpu))) cpu_stats, ''+str(sum(cpu)/@sum_cpu,4,1)+'%' CpuRatio, ltrim(str(sum(reads)))+' : '+ltrim(str(avg(reads))) reads_stats, ''+str(sum(reads)/@sum_reads,4,1)+'%' ReadsRatio, ltrim(str(sum(duration)))+' : '+ltrim(str(avg(duration))) duration_stats, ''+str(sum(duration)/@sum_duration,4,1)+'%' DurRatio, textdata, count(*)/@sum_total tp, sum(cpu)/@sum_cpu cp, sum(reads)/@sum_reads rp, sum(duration)/@sum_duration dp into #queries_staticstics from (select reads,cpu,duration,writes, convert(varchar(2000),NormText)textdata from tblBatches inner join tblUniqueBatches on tblBatches.HashId=tblUniqueBatches.hashid where duration>@duration_filter) B group by textdata print 'Top 10 order by cpu+reads+duration' select top 10 * from #queries_staticstics order by cp+rp+dp desc print 'Top 10 order by cpu' select top 10 * from #queries_staticstics order by cp desc print 'Top 10 order by reads' select top 10 * from #queries_staticstics order by rp desc print 'Top 10 order by duration' select top 10 * from #queries_staticstics order by dp desc print 'Top 10 order by batches' select top 10 * from #queries_staticstics order by tp desc End几个参数和字段要说明白。@duration_filter以毫秒为单位,默认 -1 表示不过滤,传入 1000 就只看执行时间超过 1 秒的语句。@sum_total等变量乘 0.01 是为了后面算百分比时直接除,省一次乘法。#queries_staticstics临时表里tp、cp、rp、dp分别是执行次数、CPU、Reads、Duration 的占比,排序时用它们比用原始 sum 值更直观。存储过程把 writes 去掉了,因为大部分系统以 Reads 为主,需要的话把注释掉的那行放开就行。
调用方式:
Use mydb Exec usp_GetAccessPattern -- 或者带过滤参数 Exec usp_GetAccessPattern 30003.4 从报表里读出关键语句
文档给了一个真实采样:某大型公司业务系统,8 个 CPU 都在 90%~100% 波动。跑完usp_GetAccessPattern后,DBO.x_DEDUP_PROC两小时内运行 75 次,占用 90.8% CPU、94.6% Reads。平均 CPU 时间 681961 毫秒,约 11 分钟。8 个 CPU 两小时内最多跑 80 条左右,实际跑了 75 条,说明这个存储过程一直在连续运行。优化它,问题基本就解决了。
另一个采样里,关键语句是SELECT COUNT(*) FROM GTBL7MS和SELECT CaseNO FROM PATIENTDATA_sum WHERE MRN = @P1,后续检查发现相关表没有有效索引,加上索引后性能整体提升。这两个例子说明报表的价值不在于数字多漂亮,而在于帮你把“哪类语句最耗资源”这件事从猜测变成可量化的事实。
4. 避坑与排查:Profiler 分析里最容易翻车的五个地方
4.1 跟踪文件暴涨把磁盘写满
现象:挂上 Profiler 跑了一晚上,第二天发现磁盘红了,.trc文件几十个 GB。
原因:事件类选太多,或者没设文件滚动大小,跟踪一直往一个文件里追加。
解决:建跟踪时设@maxfilesize,比如 50MB 或 100MB,让文件自动滚动;事件类只留必要的几个;生产环境尽量用Capture to file而不是Capture to table。
4.2 int 溢出导致存储过程报错
现象:执行usp_GetAccessPattern时报“将 expression 转换为数据类型 int 时出现算术溢出错误”。
原因:tblBatches里的reads、cpu、writes字段是 int 类型,大 trace 文件里这些值的 sum 超过 int 上限。
解决:文档 4.5 节给了修正语句:
alter table tblBatches alter column reads bigint alter table tblBatches alter column cpu bigint alter table tblBatches alter column writes bigint改完再跑存储过程就不会溢出了。这个坑在 trace 文件超过 1GB 时几乎必现。
4.3 过滤参数设太小,漏掉最频繁的语句
现象:用usp_GetAccessPattern 5000跑出来的结果里,找不到应用端反馈的那条高频查询。
原因:那条查询单次执行时间不到 5 秒,被@duration_filter滤掉了。
解决:文档 4.5 节建议参数不要小于 1000,用 1000 的倍数如 3000、5000。同时交叉分析四个结果:不带参数的、1000、3000、5000 各跑一次,重叠出现的语句通常正是需要关注的。运行最多最密的语句未必有超过 1000 毫秒的执行时间,所以不带参数的那次不能省。
4.4 拿压力测试的 Trace 当生产数据用
现象:分析压力测试产生的 trace,优化了一堆语句,上线后性能问题依旧。
原因:很多压力测试工具不能真正模拟现实环境,甚至是循环执行自己写的语句,访问模式和真实生产差很远。
解决:文档 4.3 节明确建议分析从生产环境捕获的 Trace 文件,压力测试的 trace 只作参考。如果非要用,至少确认测试脚本覆盖了真实业务的主要路径。
4.5 只盯最频繁语句,忽略高资源占比的“少数派”
现象:报表里最频繁的语句是一条环境配置语句,没有参考价值,真正耗资源的存储过程只跑了 75 次,差点被忽略。
原因:执行次数多不等于资源占用高。一条执行 11 分钟的存储过程,跑 75 次就能吃满 8 个 CPU。
解决:看报表时同时关注ExecRatio、CpuRatio、ReadsRatio、DurRatio四个占比,按cp+rp+dp排序的那张表往往最能暴露问题。文档 4.4 节的例子就是靠这个排序抓到x_DEDUP_PROC的。
5. 进阶技巧:用 dbid 分组找出 HOT 数据库与交叉分析习惯
5.1 按 dbid 分组定位最耗资源的库
一台 SQL Server 上跑多个库时,光知道哪类语句耗资源还不够,还得知道哪个库最“热”。文档 4.6 节给了一段按dbid分组的统计语句,思路和usp_GetAccessPattern一样,只是 group by 的维度从textdata换成dbid。
print 'group by dbid' declare @sum_total float,@sum_cpu float,@sum_reads float, @sum_duration float,@sum_writes float select @sum_total=count(*)*0.01, @sum_cpu=sum(cpu)*0.01, @sum_reads=sum(reads)*0.01, @sum_writes=sum(writes)*0.01, @sum_duration=sum(duration)*0.01 from tblBatches select dbid, ltrim(str(count(*))) exec_stats, ''+ str(count(*)/@sum_total,4,1)+'%' ExecRatio, ltrim(str(sum(cpu)))+' : '+ltrim(str(avg(cpu))) cpu_stats, ''+str(sum(cpu)/@sum_cpu,4,1)+'%' CpuRatio, ltrim(str(sum(reads)))+' : '+ltrim(str(avg(reads))) reads_stats, ''+str(sum(reads)/@sum_reads,4,1)+'%' ReadsRatio, ltrim(str(sum(duration)))+' : '+ltrim(str(avg(duration))) duration_stats, ''+str(sum(duration)/@sum_duration,4,1)+'%' DurRatio, count(*)/@sum_total tp, sum(cpu)/@sum_cpu cp, sum(reads)/@sum_reads rp, sum(duration)/@sum_duration dp into #queries_staticstics_groupbydb from (select reads,cpu,duration,writes, convert(varchar(2000),NormText)textdata,dbid from tblBatches inner join tblUniqueBatches on tblBatches.HashId=tblUniqueBatches.hashid) b group by dbid order by sum(reads) desc select dbid,ExecRatio batches,CPURatio CPU, ReadsRatio Reads,DurRatio Duration from #queries_staticstics_groupbydb这段代码和usp_GetAccessPattern的区别在于子查询里多取了dbid,group by 换成dbid,最后按sum(reads)降序排列。输出结果里dbid对应sysdatabases里的数据库 ID,用select name from sysdatabases where dbid = ?就能查出库名。文档给了一个输出样例,dbid为 37 的库在 batches、CPU、Reads、Duration 四个维度上都排第一,那它就是当前最需要关注的 HOT 库。
5.2 交叉分析四个结果,建立访问模式直觉
文档 4.5 节提到的交叉分析方法,是我自己用得最多的一条技巧。具体做法是:同一份 trace 数据,分别跑不带参数、1000、3000、5000 四次usp_GetAccessPattern,把四张报表并排看。不带参数的那次告诉你“谁跑得最多”,带参数的那几次告诉你“谁跑得最重”。重叠出现的语句,就是既频繁又耗资源的关键语句,优先处理。
我一般还会把order by cp+rp+dp desc的那张 Top 10 单独存下来,和上一次优化的结果对比。如果某类语句的占比从 40% 降到 5%,说明优化生效了;如果没降反升,可能是索引改了但统计信息没更新,或者应用端调用模式变了。性能调优是个长期过程,文档里那句话我印象很深:一开始可以解决 80% 的问题,但后面 20% 的问题却需要另外 80% 的时间。
从那以后我每次做 Profiler 分析,都强制走一遍“抓全量 → Read80trace 标准化 → 四个参数交叉跑 → 按 dbid 看热点库”的流程,不再凭感觉猜哪条 SQL 有问题。这套文档里的存储过程和命令,改改库名和文件路径就能直接用,希望帮到你。
本文还有配套的精品资源,点击获取