多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

用C#复刻轻量级SQL Server Profiler:DMV轮询监控财务系统慢查询与死锁

用C#复刻轻量级SQL Server Profiler:DMV轮询监控财务系统慢查询与死锁 简介这是一套面向C#入门与进阶学习者的财务管理系统源码包适合希望掌握企业级桌面应用开发及SQL Server性能调优的读者。资源基于C#实现财务核心模块涵盖账务管理、报表生成、固定资产、成本核算、预算控制与权限管理等并针对SQL Server数据库提供索引、查询、内存等优化实践包含SQL Server Profiler相关代码。压缩包共116个文件约3.01MB以59个.cs源码文件为主配合解决方案与工程文件、配置文件、依赖库以及界面图标资源结构完整便于直接打开学习。已有85人学习浏览。通过阅读和运行源码可具体学习ADO.NET数据访问、Entity Framework应用、WinForms界面构建、设计模式与多线程处理同时可掌握利用Profiler监控数据库活动、定位性能瓶颈的方法提升系统优化能力。1. 免费的 C# SQL Profiler 与财务系统源码为什么值得自己做一套用 C# 写的财务管理系统上线才两周月末结账就开始卡会计点一次过账页面转圈 40 秒DBA 开官方 SQL Server Profiler 一看几十条 RPC:Completed 全是同一个存储过程锁等待和死锁占了一半。官方 Profiler 的底层 SQL Trace 接口已经被标记弃用新服务器上拿它做长期监控既担心兼容性又心疼性能开销free-sql-server-profiler 这类 C# 开源项目的思路就是用扩展事件和 DMV 轮询搭一套能天天开着的轻量监控面板。它的核心并不神秘轮询几个系统视图、解析死锁 XEL、把慢查询刷到 DataGridView 里。这篇文章给你两条线一条是看懂并复刻一个免费 SQL Server Profiler另一条是把它用在 C# 财务管理系统源码最常坏的位置——事务、批量导入与锁等待。适合开发兼运维、想在下一次月末结账前睡个安稳觉的人。2. 先弄清 Profiler 抓什么SQL Trace 事件与扩展事件的取舍财务系统是典型的「平时不慢、月底必卡」。做监控前得先知道事件类型对应什么故障否则工具再轻也白搭。先讲事件再讲两个数据来源SQL Trace 和 DMV。2.1 财务系统里的慢查询、死锁与锁等待对应 Profiler 的哪些事件财务系统最常见的性能故障有三个批量过账时凭证插入产生 Lock:Timeout月底报表把大表拉成全表扫描产生 RPC:Completed 慢事件两个会计同时做月末结账互踩对方锁定的范围而产生死锁。在 SQL Server Profiler 里这些分别对应不同事件类别。下面这张表是老 SQL Trace 事件 ID 和财务场景的对应关系适合新手建立「事件名—故障」的映射事件名SQL Trace 事件 ID财务系统里什么时候出现建议RPC:Completed10存储过程执行完成过账、结账过程打开只看耗时长的SQL:BatchCompleted12直接执行的 SQL 批报表查询、Dapper 查单打开Lock:Timeout47锁等待超时通常是报表和过账互相顶住打开Lock:Deadlock148结账任务互相持有锁打开单独存 XELSQL:BatchStarting11正在执行的批用来抓执行中的会话调试时开日常关掉新手常犯的错误是把这五类全开然后盯着每秒几百条事件发呆。通用的裁剪原则是日常监控只开 Completed 类事件并加上过滤条件比如数据库名、时长阈值、登录名只有排查死锁和阻塞时才去开 Lock 事件。free-sql-server-profiler 这类工具一般默认只做两件事轮询系统视图收集慢查询、解析死锁文件。它不是把官方 Profiler 的所有事件照搬过来因为搬过来的事件越多服务器端的开销越大监控本身反而成了负担。2.2 为什么 SQL Trace 能用于轻量工具从 sp_trace_create 到系统视图的监控轮询SQL Trace 是 SQL Server 内部的事件基础设施官方 SQL Server Profiler 图形工具只是它的一个前端。老一代 DBA 用 sp_trace_create、sp_trace_setevent 这些存储过程自己搭跟踪把结果写到文件或表里。它确实能抓到非常细的事件但有三个硬伤第一事件多时跟踪文件会很快膨胀写文件本身也抢 IO第二SQL Trace 在 SQL Server 2012 起被标记为弃用微软不再扩展新事件类型第三跟踪必须由某个长期会话持有不小心停了就静默丢数据。C# 实现免费替代品时我一般不会一上来就做 SQL Trace 前端而是先做 DMV 轮询。原因是 DMV 轮询不注册事件、不产生额外的事件流只是定期读几个系统视图对于「昨天夜里哪几条 SQL 最慢」这类追溯问题它比 Trace 更省事也不怕服务器重启丢跟踪。它的缺点同样明显只能看到 SQL Server 自己缓存的执行统计看不到正在执行的语句也拿不到死锁图。业界的常见做法是分层日常监控用 DMV 轮询出问题了再临时开扩展事件会话抓证据。2.3 数据抓手sys.dm_exec_query_stats 与 sys.dm_exec_requests 的快照查询最常用的慢查询脚本是查 sys.dm_exec_query_stats它可以作为慢查询基线每天早上看一遍SELECT TOP 10 SUBSTRING(st.text, (qs.statement_start_offset / 2) 1, ((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(st.text) ELSE qs.statement_end_offset END - qs.statement_start_offset) / 2) 1) AS [语句], qs.execution_count, qs.total_elapsed_time / 1000000.0 AS [总耗时(秒)], (qs.total_elapsed_time / qs.execution_count) / 1000000.0 AS [平均耗时(秒)], qs.total_logical_reads, qs.last_execution_time FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.plan_handle) st WHERE qs.execution_count 5 ORDER BY qs.total_elapsed_time DESC;这段代码从执行统计视图里取缓存计划的累计耗时按总耗时倒序取前 10 条。三个要点total_elapsed_time 的单位是微秒除以 1000000 才是秒statement_start_offset 和 statement_end_offset 是字节偏移量公式里的 /21 是为了跳过 Unicode 字符的半个字节过滤 execution_count 5 是为了滤掉只跑过一两次的冷 SQL避免基线被偶发大查询污染。但 sys.dm_exec_query_stats 有个盲区正在执行的 SQL 不在缓存统计里。要看「现在谁在跑、跑了多久」得查 sys.dm_exec_requests把两个视图的查询结果合并成一个监控面板一个页签看「已完成 TOP 10」另一个页签看「正在执行 TOP 10」SELECT TOP 10 r.session_id, r.status, r.start_time, r.total_elapsed_time / 1000 AS [已耗毫秒], SUBSTRING(t.text, (r.statement_start_offset / 2) 1, ((CASE r.statement_end_offset WHEN -1 THEN DATALENGTH(t.text) ELSE r.statement_end_offset END - r.statement_start_offset) / 2) 1) AS [当前语句] FROM sys.dm_exec_requests r CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t WHERE r.session_id 50 AND r.status IN (running, suspended) ORDER BY r.total_elapsed_time DESC;过滤 session_id 50 是为了跳过系统自己维护的会话防止把后台任务全部当成业务慢查询。status 为 suspended 的会话往往是锁等待配合 sys.dm_exec_waits 才能看到它在等哪把锁。监控脚本把这两个查询合起来出问题时永远有据可查。3. 用 C# 复刻一个轻量 ProfilerDMV 轮询 DataGridView 实时刷新的最小实现下面进入动手环节。目标是做一个能跑起来的 WinForm 面板后台线程每 5 秒查一次 DMV前台表格实时刷新。代码不多但线程和控件刷新的坑都在这。我把项目设计成三个类SqlWatchWorker 负责轮询和解析SlowSqlEvent 保存单条慢事件DeadlockEvent 先占好坑后面读 XEL 时直接复用。3.1 项目骨架与核心类SqlWatchWorker、SlowSqlEvent、DeadlockEventpublic class SlowSqlEvent { public DateTime EventTime { get; set; } public string SqlText { get; set; } public long ExecutionCount { get; set; } public double AvgElapsedSeconds { get; set; } public long LogicalReads { get; set; } public string DatabaseName { get; set; } } public class DeadlockEvent { public DateTime EventTime { get; set; } public string DeadlockGraphXml { get; set; } public string VictimProcess { get; set; } }这段代码没有业务逻辑但它决定了后续绑定 DataGridView 的方式直接用 List 而不是 DataTable因为 DataTable 列一多就要做列映射而实体类配合 BindingSource 更直观。参数上要注意 EventTime 用本地时间还是 UTC 得统一我习惯在查询时就转成本地时间避免跨时区排错时对着时间戳发呆。字段名用属性而不是公共字段是因为 DataGridView 绑定靠反射读属性名公共字段不会自动成为列。3.2 轮询线程与控件刷新BackgroundWorker、Timer 和 Task 怎么选WinForm 的控件句柄由主线程创建如果直接在后台线程里写 dataGridView1.DataSource list十有八九抛跨线程访问异常。最常见的方案是 BackgroundWorkerDoWork 里只做查询和解析严禁碰控件把结果放到 e.Result 里回主线程的 RunWorkerCompleted 里再更新表格。这里有个容易翻车的细节BackgroundWorker 默认不支持并发轮询间隔内上一次 DoWork 没结束RunWorkerAsync 会抛异常。所以我一般不用它做「一直跑」的轮询而是用 Task 配合 CancellationToken把查询任务排队。另一个经典坑是 System.Windows.Forms.Timer 的 Tick 事件直接在主线程执行在里面查 DMV 等于把 UI 卡死。把 SQL 查询放到独立线程UI 每 500 毫秒消费一次队列里的结果是更稳妥的做法。下面给一个最小轮询骨架用 BlockingCollection 缓冲事件private BlockingCollectionListSlowSqlEvent _queue new(); private void WorkerLoop(CancellationToken token) { using var conn new SqlConnection(_connStr); while (!token.IsCancellationRequested) { var list QuerySlowSql(conn); // 内部执行第 2.3 节的 DMV 查询 _queue.Add(list, token); Thread.Sleep(_intervalMs); } }这段代码把查询结果放进阻塞队列UI 线程再用一个定时器从队列取数据刷新表格。Thread.Sleep 在这里是简单可接受的方案追求精度可以改用 WaitHandle关键点是 CancellationToken 必须在循环条件和队列等待两处都传进去否则关闭窗体时线程还在等队列进程退不掉。热词里那些「C# timer 访问控件」「C# 线程」的求助帖十有八九就是把查询和刷新写在了同一个线程里本质问题不是 Timer是职责没拆分。3.3 最小可跑代码连接串、轮询间隔、只输出慢查询的 C# 片段把骨架填完整放进一个空白 WinForm 项目就能跑。先在设计器里放一个 DataGridView关掉 AutoGenerateColumns再在 Form_Load 里启动任务private async void Form1_Load(object sender, EventArgs e) { _connStr Server.;DatabaseFinance;Integrated Securitytrue; Application NameSqlWatch;Connect Timeout3;; _intervalMs 5000; _cts new CancellationTokenSource(); _ Task.Run(() WorkerLoop(_cts.Token), _cts.Token); var refreshTimer new System.Windows.Forms.Timer { Interval 1000 }; refreshTimer.Tick (s, ev) { while (_queue.TryTake(out var list)) { dataGridView1.DataSource list; } }; refreshTimer.Start(); } private void Form1_FormClosing(object sender, FormClosingEventArgs e) { _cts.Cancel(); }注意FormClosing 里只调 Cancel 还不够最好接着用 Task.WhenAny 等 WorkerLoop 超时退出否则连接还开着SQL Server 端会残留会话。这段代码做了什么后台任务每隔 5 秒查一次 DMVUI 定时器每秒把新到的结果刷到表格。三个参数值得说明intervalMs 设 5000 是因为低于 5 秒的轮询对服务器压力明显上升实测 1 秒轮询能让跑批时间多出两成Application Name 一定要写否则监控面板里没法区分连接来自哪个业务系统Connect Timeout3 是避免监控连接把网络超时也算进业务延迟里。数据量涨到上千行以后每次 DataSource 赋值都会触发重排WinForm 界面卡顿就从这里来。改法有两个一是用 BindingList 并手动配置关键列二是在赋值前后调用 SuspendLayout 和 ResumeLayout再补一句 dataGridView1.ClearSelection() 防止选中行跳动。凡是 WinForm 控件多、数据刷新快的界面卡顿排查的优先级一定排在这几个位置。4. 财务管理系统源码里最该被监控的代码点事务、批量导入与锁监控工具搭好该拿它对着财务系统源码看。财务系统最值得被监控的四个位置凭证过账、批量导入、报表查询、操作日志它们恰恰是 SQL Server Profiler 里出事件最多的地方。逐段对一遍能少挨好几顿运维投诉。4.1 凭证过账与月末结账必须用事务SqlTransaction 的最小记账模板凭证过账的规则是「有借必有贷借贷必相等」。如果在代码里先插借方、再插贷方中间任何一步失败数据库里就躺着一条只有借没有贷的脏凭证。所以源码层面过账方法必须以 SqlTransaction 包住整段操作最后校验借贷平衡再提交。下面是我常用的最小模板using var conn new SqlConnection(_connStr); conn.Open(); using var tx conn.BeginTransaction(VoucherPost); try { // 1. 写入凭证主表 var cmd conn.CreateCommand(); cmd.Transaction tx; cmd.CommandText INSERT INTO Voucher(VoucherNo, VoucherDate, State) VALUES(no, date, POSTED); cmd.Parameters.Add(no, SqlDbType.NVarChar, 32).Value voucherNo; cmd.Parameters.Add(date, SqlDbType.DateTime).Value voucherDate; cmd.ExecuteNonQuery(); // 2. 写入分录表并累计借贷金额 decimal debitTotal 0, creditTotal 0; foreach (var entry in entries) { // 每一条分录 INSERT debitTotal entry.DebitAmount; creditTotal entry.CreditAmount; } // 3. 借贷平衡校验不平就抛异常回滚 if (debitTotal ! creditTotal) throw new InvalidOperationException($凭证{voucherNo}借贷不平衡); tx.Commit(); } catch { tx.Rollback(); throw; }这段代码把凭证主表、全部分录的插入纳入同一个事务最后校验借贷平衡。两个细节值得注意第一AddWithValue 对 decimal 和日期类型会造成隐式类型转换导致索引用不上所以这里用显式的 SqlParameter 写法第二事务内部绝不弹 MessageBox、不调 WebService、不做 Thread.Sleep这些动作会把数据库锁的持有时间从毫秒级拉长到秒级月末结账时锁等待的根源常常就在这里。源码里还要关注凭证的记账状态机。常见做法是给凭证表加 State 字段取值 Draft、Posted、Voided过账只允许从 Draft 到 Posted红冲凭证只允许从 Posted 到 Voided。如果状态转换条件写漏同一个凭证被过账两次Profiler 里看到的就是同一批插入语句重复执行对不上账。把这个状态机放进固定流程里校验比事后看监控更省事。4.2 批量导入初始余额/Excel 凭证SqlBulkCopy 的性能与对表变动的影响财务系统上线第一天往往要导初始余额之后每个月可能还要从 Excel 导银行流水或凭证。逐条 INSERT 太慢SqlBulkCopy 是常用做法但它对表的影响经常被忽略。最典型的是目标表上的触发器会被触发索引会重建维护这些维护操作会阻塞正在读同一批数据的报表会话。也就是说你在后台导数据前台报表突然卡住回过去看 Profiler全是 LCK_M_IX 等待。using var bulk new SqlBulkCopy(conn, SqlBulkCopyOptions.KeepIdentity, null); bulk.DestinationTableName dbo.VoucherDetail; bulk.BatchSize 5000; bulk.BulkCopyTimeout 300; bulk.ColumnMappings.Add(Id, Id); bulk.ColumnMappings.Add(VoucherId, VoucherId); bulk.ColumnMappings.Add(AccountCode, AccountCode); bulk.ColumnMappings.Add(DebitAmount, DebitAmount); bulk.ColumnMappings.Add(CreditAmount, CreditAmount); bulk.WriteToServer(dataTable);注意 KeepIdentity 这个选项。批量导入初始余额时如果 VoucherId 是自增列且业务上需要和源 Excel 里的编号保持一致就得保持标识值否则两张表对不上账。BatchSize5000 是一次提交的行数改大不一定更快因为每批都会触发日志写盘BulkCopyTimeout 设 300 是给大表一个合理的超时上限。但 WriteToServer 超时会断开连接之前已提交的批次不会自动回滚所以导入前要先把临时区数据清掉失败后整体清理重导也就是先清后导、失败重导。陈年 Excel 的读取也是这套流程里的常见痛点。我一般用 ExcelDataReader 流式读而不是 Excel Interop——Interop 会真的拉起一个 Excel 进程服务器上没人点开也会残留几十个 EXCEL.EXE32 位和 64 位问题更是没完没了。读出来之后直接塞进 DataTable再丢给 SqlBulkCopy整条链路里最值得调的是批量大小和事务边界不是读取库。4.3 财务查询为什么会把锁拉高NOLOCK、WITH(INDEX)、统计信息过期财务人员写报表最喜欢在 WHERE 条件里套函数比如 datediff(month, VoucherDate, getdate())0。这个条件让 SQL Server 无法对 VoucherDate 做 seek只能扫整张凭证表扫描过程遇到正在过账的事务就得等锁释放。Profiler 里表现成一条长时间运行且伴随 Lock:Timeout 的完成事件。源码评审时第一个要盯的就是这种写法。处理办法按严重程度排序第一步把函数条件改写为范围条件例如 VoucherDate 2024-01-01 00:00:00 AND VoucherDate 2024-02-01 00:00:00让索引能走 seek第二步确认报表是否真的需要实时一致如果只是月度统计可以接受带脏读就在 SELECT 语句里加 NOLOCK 提示。但明细账、科目余额表这类要出正式报表的查询绝对不能用 NOLOCK否则统计口径和余额表对不上时很难解释第三步检查统计信息是否过期特别是每季度新增大量凭证后自动更新的阈值不一定触发常见做法是在维护计划里对凭证大表执行 UPDATE STATISTICS WITH FULLSCAN每周一次即可。还有一个和统计信息相关的玄学现象同一个表上管理员重建了聚集索引但没更新统计信息Profiler 里会看到两条执行次数差不多但平均耗时差好几倍的同类 SQL。这时候不是代码问题是优化器手里的黑匣子没刷新。先跑一遍 UPDATE STATISTICS再回头看执行计划通常就正常了。排查手段用 DBCC SHOW_STATISTICS 看 updated 列距离上次更新的时间越久越值得怀疑。4.4 权限与操作日志从 Profiler 视角反查「谁删了凭证」财务系统被审计时问得最多的问题是谁在什么时候删了凭证SQL Server Profiler 默认只能看到 SQL 登录名而整个系统的连接池共用一个 SQL 登录所以光靠服务器端 trace 查不出是哪个业务用户。常见做法是双轨业务代码在写操作时把 OperatorId 和操作类型写进审计表同时把关键操作的日志写到应用日志文件。lock (_logLock) { File.AppendAllText(_logPath, ${DateTime.Now:yyyy-MM-dd HH:mm:ss}|{operatorId}|{action}|{detail}{Environment.NewLine}); }这段日志代码解决了进程内多线程写文件的覆盖问题。lock 是给进程内多个线程串行的但如果系统是多进程部署比如多个服务实例跨进程还要用文件锁或换统一日志库。注意输出格式要固定成分隔符分隔后面用 SQL 或脚本解析时不用做正则清洗C# 财务管理系统源码里我一般把日志路径放进配置项并且按天滚动避免单个文件涨到几百兆。反过来如果审计表漏记而服务器上又必须留证据另一个手段是在连接串里带上 Application NameSettlement; Workstation IDHQ-PC01然后配合扩展事件按 app 过滤。它虽然不能精确定位到业务用户但至少能把范围缩小到具体工作站和时间窗口。这也是上一章强调连接串要写 Application Name 的原因监控工具的价值不只是抓到慢 SQL还包括在出审计问题时划定责任边界。5. 挖坑与排查SQL Server Profiler 替代工具上线后的 5 个真问题轻量 Profiler 看着简单真上线到生产环境问题通常出在轮询开销、死锁抓取、连接标识、旧版本兼容和看不到正在执行的语句这五个地方。下面每条都按「现象 → 原因 → 解决」记录。5.1 轮询开销1 秒轮询比跑批还累现象开着监控跑月末结账原本 20 分钟的批跑到 28 分钟监控和业务在同一条服务器上时尤其明显。原因监控查询每秒来一次每次都要扫 sys.dm_exec_query_stats 并关联计划句柄缓存里冷热数据全被触发一遍。解决把轮询间隔调到 5 秒以上并且只采集最近 10 秒内有执行的新数据WHERE qs.last_execution_time DATEADD(second, -10, GETDATE())加上这个过滤后dm_exec_query_stats 的扫描范围大大缩小监控自身的 CPU 占用可以降到 1% 以下。还有一种现象是轮询没抓到任何数据面板空着此时不要缩短间隔而是去查 sys.dm_exec_requests 看是否有正在跑的长 SQL那个视图比快照统计更直接。5.2 死锁图不是拿不到扩展事件 XEL 的读取方法现象监控面板记录了 Lock:Deadlock 事件但只显示一行摘要没有死锁图形。原因死锁图 XML 只写在扩展事件的 file target 里DMV 里没有完整图形。解决先创建一个扩展事件会话CREATE EVENT SESSION xe_deadlock ON SERVER ADD EVENT sqlserver.lock_deadlock (ACTION(sqlserver.sql_text, sqlserver.session_id)) ADD TARGET package0.event_file (SET filename ND:\XEL\deadlock.xel) WITH (MAX_MEMORY 4 MB, EVENT_RETENTION_MODE ALLOW_SINGLE_EVENT_LOSS); ALTER EVENT SESSION xe_deadlock ON SERVER STATE START;C# 里读取这份 XEL 文件常见做法是借助 sys.fn_xe_file_target_read_file 打开SELECT CAST(event_data AS XML) FROM sys.fn_xe_file_target_read_file(ND:\XEL\deadlock*.xel, NULL, NULL, NULL)拿到 XML 再解析 deadlock_graph 节点。注意这个函数支持路径通配但按文件名前缀匹配路径写错时它不报错返回空表新手很容易在这上面空转半小时。5.3 Application Name 没设置监控面板里全是 .Net SqlClient Data Provider现象想过滤财务系统的查询发现来源列里全是同一个名字 .Net SqlClient Data Provider没法区分是哪套业务。原因连接串默认的 Application Name 是客户端库自带的名字所有 C# 程序都一样。解决在每个项目的连接串里固定加上 Application NameFinanceSettlement; Workstation IDServer01; 然后监控查询里按这个字段过滤。这是一个成本几乎为零但排错收益很大的习惯尤其配合 SQLBulkCopy 做批量导入时它能帮你一眼分辨哪些等待来自导入线程、哪些来自报表线程。5.4 SQL Trace 已弃用老服务器上 sp_trace 还能不能写现象把以前保存的 Profiler 模板导入新环境发现部分事件类型消失团队里还有人提议用 sp_trace_create 做一个开机自启跟踪。原因SQL Server 2012 起 SQL Trace 被标记为弃用2016 之后事件引擎的重心全部转向扩展事件旧接口能跑但不再维护。解决新项目一律走扩展事件只有 SQL Server 2008 R2 这种老环境才用 sp_trace并且只开必要事件设置 max_file_size 限制跟踪文件大小定期用下面的语句检查状态SELECT id, status, path, max_file_size, event_count FROM sys.traces WHERE id ! 1;id 为 1 的那一行是系统默认跟踪它的状态不代表自定义跟踪自定义跟踪 status 变为 0 时说明已经停止文件不落盘就是白抓。不要用默认跟踪替代专业监控它只适合应急抓不出需要业务上下文的慢查询。5.5 Profiler 没抓到那条慢 SQL它还在执行中或者跑在别的库现象同事说报表接口超时了打开监控面板一条慢查询都没有。原因看错视图。sys.dm_exec_query_stats 只统计已经执行完且还在缓存里的语句接口超时说明语句正在执行所以它不会出现在已完成统计里。另一种可能是这条语句的连接指定了别的 Initial Catalog而监控脚本按 DatabaseName 过滤了。解决监控面板同时具备两个查询一个查已完成一个查正在执行正在执行的查询用 sys.dm_exec_requests 关联 sys.dm_exec_sql_text并且不按数据库名过滤只在展示时标注库名。这样无论语句处于哪个阶段都有对应证据不会再被一句「你是不是漏抓了」问住。6. 落地技巧早上 30 秒跑完的 SQL Server 体检脚本自研 Profiler 解决的是「实时看」还缺一个「每天看」的静态体检。我的习惯是写一个存储过程把核心指标汇总成一张表等待统计、最慢十条、阻塞会话、死锁计数。每天早上到工位先跑一遍跟昨天的数字对比比打开监控面板四处翻快得多。CREATE PROCEDURE dbo.usp_SqlHealthDaily AS BEGIN SET NOCOUNT ON; INSERT INTO dbo.SqlHealthHistory(CheckDate, ItemName, ValueText) SELECT GETDATE(), WaitStats, wait_type : CAST(wait_time_ms AS varchar(20)) FROM sys.dm_os_wait_stats WHERE wait_type NOT LIKE %SLEEP% AND wait_time_ms 5000 ORDER BY wait_time_ms DESC; INSERT INTO dbo.SqlHealthHistory(CheckDate, ItemName, ValueText) SELECT GETDATE(), TopSlow, CAST(total_elapsed_time / 1000000.0 AS varchar(20)) : s.text FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.plan_handle) s WHERE qs.execution_count 5 ORDER BY total_elapsed_time DESC OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY; END说明两点sys.dm_os_wait_stats 是实例重启以来的累计值不是当天瞬时值所以对比时看增量而不是绝对值我会在脚本里同时记录前一天的值并算出差值TopSlow 的 OFFSET FETCH 在 SQL Server 2012 以上可用老环境用 TOP 10 加子查询替换。体检脚本解决的是「监控面板会不会漏」的兜底问题把每天的数据存进 SqlHealthHistory一周导出一份 CSV 归档和当月结账记录放一起。时间久了你就有了属于自己系统的性能基线下次再有人问「是不是数据库变慢了」先查体检表再说结论。希望帮到你。本文还有配套的精品资源点击获取
返回列表