恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
C#财务系统免费用SQL Trace与扩展事件抓取慢SQL分析
首页
资讯中心
/
C#财务系统免费用SQL Trace与扩展事件抓取慢SQL分析
C#财务系统免费用SQL Trace与扩展事件抓取慢SQL分析
发布时间:2026/10/2 3:19:32
简介这是一份面向C#学习者和财务软件初学者的完整练习源码包围绕财务管理系统演示账务、报表、固定资产、成本核算、预算控制与用户权限等核心模块并将SQL Server Profiler性能监控与索引、查询、分区、内存调优策略融入实战。资源共116个文件压缩后3.01MB以59个C#源文件为主辅以DLL依赖、界面GIF演示、资源文件、项目配置与解决方案文件便于直接打开工程查看逻辑。目前已有84人学习下载。通过研读源码可掌握ADO.NET与Entity Framework的数据访问方式、WinForms界面组织、MVC式分层设计、多线程与异常处理结合Profiler工具可学习定位慢查询、优化索引和数据库结构整体适合作为课设参考或入门企业级系统开发的实践样板。1. SQL Server Profiler 收费后C# 财务系统开发者还能怎么抓慢 SQLSQL Server Profiler 几乎是 SQL Server 开发者的肌肉记忆但很多 C# 财务系统项目现在并不想为它单独掏一笔企业版授权。free-sql-server-profiler-master 这类项目名背后是一个很现实的诉求用 C# 重新实现一套免费可用的 SQL Server Profiler替换掉原来靠 SSMS 那个黑匣子的操作方式。再加上 C# 财务管理系统源码这种重事务、重准确性、慢一点就要被人追着问的业务系统问题就变成一个具体工程问题没有 Profiler 授权时怎么定位慢查询、死锁、长事务这套方案适合 SQL Server 使用中低版本或 Express 环境、手里有 C# 财务系统源码、又不愿意被图形界面限制住的开发者。它解决的不只是“看什么”而是“怎么自动化地抓、怎么和财务模块对上号”。2. 先用 C# 的理解拆掉 Profiler 的黑匣子Trace 与 Extended Events2.1 Profiler 到底在抓什么事件流不是日志抓取SQL Server Profiler 并不是抓网络包也不是去翻日志文件。它本质上是一个事件流接收器SQL Server 引擎在执行 SQL 语句、发生锁等待、事务提交回滚时会把事件写入内部的 trace 队列Profiler 再把队列消费出来展示。对写 C# 财务系统的人来说这个区别很重要你想抓“慢查询”不是去轮询sys.dm_exec_requests看当前在跑什么那样只能看到眼前一瞬间的状态而是订阅SQL:BatchCompleted、RPC:Completed这类事件拿到的是“已经执行完的 SQL 和它的累计耗时”。这个设计直接影响代码怎么写。如果你只是用 ADO.NET 的 SqlCommand 去查sys.dm_exec_query_stats你会拿到累计统计但是拿不到每一次执行的细节比如是哪个登录名、哪个客户端、哪条 SQL 文本、耗时多少微秒。而 Profiler 的 Trace 事件流里每一行就是一次执行记录。财务系统里最讨厌的场景——用户说“点查询要 3 秒”普通 DMV 只能告诉你平均 1 秒而 Trace 能告诉你是 14:32:05 那一次凭证查询跑了 2.8 秒参数是某个具体科目代码。2.2 C# 财务系统里最常见的三类事件BatchCompleted、RPC:Completed、Lock:Deadlock我一般建议先只开三类事件不要一次全抓。第一类是SQL:BatchCompleted它对应的是 ADO.NET 直接执行的命令比如SELECT * FROM Voucher WHERE Period202404第二类是RPC:Completed对应的是存储过程调用财务系统里大量月末结转、凭证过账都写在存储过程里你得靠它看过程级耗时第三类是Lock:Deadlock和Lock:Timeout财务系统并发不高但一旦发生锁等待往往就是同一个人同时打开了两个界面或者一个事务里查完账没提交就去打开另一个窗体。C# 财务管理系统源码里最典型的坏味道是在同一个数据库连接里先做一次 SELECT再开事务做 UPDATE中间隔了很久。BatchCompleted 只能看到每一条语句的耗时看不到间隔而锁事件能帮你把“两个事务互相等锁”的因果链找出来。所以我的习惯是默认模板只开 BatchCompleted 和 RPC:Completed等出现锁问题再加 Lock 事件避免事件量太大把 SQL Server 本身拖慢。2.3 免费方案怎么选SQL Trace、Extended Events 还是 SMO TraceReader现在要做免费的 Profiler 替代品有三条路。第一条是继续用 SQL Trace 的存储过程sp_trace_create、sp_trace_setevent建 trace然后用fn_trace_gettable读文件。这条路兼容性最好SQL Server 2008 到 2019 都能跑用法网上资料最多但微软已经标记为 deprecated未来版本不见得还保留。第二条是用 Extended Events也就是 XE。SQL Server 2012 以后官方推荐走 XE它更轻量、事件字段更干净而且可以定义过滤条件比如duration 1000000直接在服务器端过滤文件里只留下超过阈值的慢 SQL。缺点是 XE 的 xel 文件不能直接用fn_trace_gettable读C# 这边解析比较麻烦需要自己解析 XML 流或者用官方提供的Microsoft.SqlServer.Xe.Core相关组件。第三条是直接用 SMO 里的TraceReader类和Server.Trace集合。这是很多开源 free-sql-server-profiler 方案的内核它封装了底层 trace 的创建和读取C# 开发效率最高。但 SMO 体积大、版本要和 SQL Server 匹配部署到瘦客户端会比较痛苦。我自己的偏好是工具类项目用 SMO生产服务端采集用 XE团队内部快速诊断用原始sp_trace_create因为它在任何环境都能手工执行不依赖额外 DLL。3. 用 C# 写一个 free-sql-server-profiler 的最小实现3.1 创建 SQL Trace最小 SQL 模板和过滤参数先说明一点下面的脚本是在数据库引擎上直接执行不是在本机 Profiler 界面上点按钮。它的作用是在服务器端创建一个后台 trace把事件写入指定路径的文件。最小模板如下DECLARE TraceID INT; DECLARE MaxFileSize BIGINT 20; -- 参数说明0 表示不启用 rollover推荐改成 2 EXEC sp_trace_create TraceID OUTPUT, 2, NC:\TraceFiles\finance_trace, MaxFileSize, NULL; -- 监听 SQL:BatchCompleted事件 ID12 EXEC sp_trace_setevent TraceID, 12, 1, ON; -- TextData EXEC sp_trace_setevent TraceID, 12, 10, ON; -- ApplicationName EXEC sp_trace_setevent TraceID, 12, 11, ON; -- LoginName EXEC sp_trace_setevent TraceID, 12, 14, ON; -- EndTime EXEC sp_trace_setevent TraceID, 12, 13, ON; -- StartTime EXEC sp_trace_setevent TraceID, 12, 16, ON; -- CPU EXEC sp_trace_setevent TraceID, 12, 4, ON; -- TransactionID -- 只保留耗时超过 100 万微秒也就是 1 秒的 SQL EXEC sp_trace_setfilter TraceID, 13, 0, 6, 1000000; EXEC sp_trace_setstatus TraceID, 1;这里sp_trace_setevent的参数分别是 TraceID、事件号、列号、开关。事件号 12 是 SQL:BatchCompleted列号 1 是 TextData10 是 ApplicationName11 是 LoginName16 是 CPU。如果记不清列号可以查sys.trace_columns视图。sp_trace_setfilter的第 4 个参数是操作符6 代表“大于等于”数字表示从第几列开始比较这里 13 对应 StartTime注意我一般不用列号直接过滤时间而是过滤 Duration。不同版本列号有微小差异所以生产环境里我更建议用 Extended Events 的过滤语法而不是靠这一串数字。如果要停止 trace执行下面这段清理脚本EXEC sp_trace_setstatus TraceID, 0; GO EXEC sp_trace_setstatus TraceID, 2;第一个 0 是停止第二个 2 是关闭并删除定义。停止后SQL Server 才保证把所有缓冲写入文件这时候再去读.trc文件最稳妥。如果不停止直接读文件尾部可能缺最后几秒数据。3.2 用 C# 读 Trace 文件并转成事件回调委托和事件的经典用法跟踪文件生成后剩下的工作就是用 C# 把它读出来。所有.trc文件都可以用系统函数fn_trace_gettable按表查询这是一个巨大的隐藏接口。我只需要传文件路径SQL Server 就会返回一张包含所有事件列的表。下面这段代码定义了一个事件类并用 C# 委托和事件把读取过程解耦。public sealed class TraceEvent { public int EventClass { get; set; } public string TextData { get; set; } string.Empty; public string ApplicationName { get; set; } string.Empty; public string LoginName { get; set; } string.Empty; public long Duration { get; set; } public DateTime StartTime { get; set; } } public sealed class TraceFileReader { private readonly string _connectionString; public event ActionTraceEvent? TraceReceived; public TraceFileReader(string connectionString) { _connectionString connectionString; } public async Task ReadAsync(string trcPath, CancellationToken ct) { await using var conn new SqlConnection(_connectionString); using var cmd new SqlCommand( SELECT EventClass, TextData, ApplicationName, LoginName, Duration, StartTime FROM fn_trace_gettable(path, DEFAULT), conn); cmd.Parameters.AddWithValue(path, trcPath); await conn.OpenAsync(ct); using var reader await cmd.ExecuteReaderAsync(ct); while (await reader.ReadAsync(ct)) { var evt new TraceEvent { EventClass reader.GetInt32(0), TextData reader.IsDBNull(1) ? string.Empty : reader.GetString(1), ApplicationName reader.IsDBNull(2) ? string.Empty : reader.GetString(2), LoginName reader.IsDBNull(3) ? string.Empty : reader.GetString(3), Duration reader.IsDBNull(4) ? 0 : reader.GetInt64(4), StartTime reader.GetDateTime(5) }; TraceReceived?.Invoke(evt); } } }逻辑说明fn_trace_gettable的第二个参数传入 DEFAULT 表示读取所有事件。这段代码把读取和业务处理拆开订阅方只管自己的逻辑比如筛选慢 SQL、按模块统计。这样当你想把数据接到日志中心时不需要改读取逻辑只需要多挂一个事件处理器。实际使用中.trc里可能混着很多事件类型我通常会在 SQL 查询里加上WHERE EventClass IN (10, 12)只留下 RPC:Completed 和 SQL:BatchCompleted减少数据传输量。另外fn_trace_gettable读取大文件时很慢如果文件超过几百 MB最好在服务器端先做一次过滤而不是全读回客户端。3.3 用 Task 做后台轮询与文件轮转别把 UI 线程卡死实时监听不像读单个文件那么简单。sp_trace_create开启了 rollover 之后SQL Server 会在文件到达上限时自动创建新文件。一个免费 Profiler 要做的就是每隔几秒扫一次 trace 目录把新出现的文件交给上面的读取器。这个循环必须扔到后台线程用 C# Task 而不是在 UI 事件里同步跑。public async Task MonitorTraceLoopAsync(TimeSpan interval, CancellationToken ct) { while (!ct.IsCancellationRequested) { var files Directory.GetFiles(_traceDir, *.trc) .OrderBy(f f) .ToList(); foreach (var file in files) { if (_consumed.Contains(file)) continue; try { await _reader.ReadAsync(file, ct); _consumed.Add(file); } catch (IOException) { // 文件可能正被 SQL Server 写入下次再读 } } try { await Task.Delay(interval, ct); } catch (OperationCanceledException) { break; } } }这里的关键是_consumed集合。如果一次只读一个文件读完后立刻标记为已消费不会重复解析。文件在被 SQL Server 写入时读往往会拿到不完整的尾部行所以我保留了 IOException 重试逻辑。实际项目里我用的是ConcurrentDictionarystring, byte记录文件名避免多线程重复消费。轮询间隔一般设 3 到 5 秒太频繁容易被文件锁干扰太慢则实时性差。还要注意一个坑trace 文件路径不能放在数据库的数据文件目录里也不能放在 C# 程序安装目录否则监控进程被杀或磁盘满时数据库和采集同时翻车。我一般单独建一个D:\TraceFiles目录并设置配额。3.4 连接字符串里的 ApplicationName让财务模块从源头可分免费 Profiler 能不能用起来很大程度取决于你分不分得清一条 SQL 是哪个模块发出来的。如果财务系统所有连接都用默认的.NET SqlClient Data Provider那抓回来的 ApplicationName 全部相同你只能靠 TextData 去查表名猜模块。正确做法是在 C# 财务系统的连接字符串里注入模块名。var builder new SqlConnectionStringBuilder { DataSource finance-db, InitialCatalog FinanceDB, UserID finance_app, Password ******, ApplicationName FinanceGL_Posting }; using var conn new SqlConnection(builder.ConnectionString);这样 Profiler 的 ApplicationName 列就会显示FinanceGL_Posting。你就可以在监控端只关心这个模块的 SQL。如果项目里用的是 EF Core可以在 DbContext 的连接工厂里统一设置optionsBuilder.UseSqlServer(connectionString);但要注意EF Core 的迁移命令、后台任务、报表导出都要区分开否则统计时混在一起。我习惯在程序入口统一封装一个FinanceConnectionFactory所有页面都从它拿连接串不允许各窗体自己拼字符串。这是 C# 财务管理系统源码改造里性价比最高的一步。4. 在 C# 财务管理系统源码里跑通 Profiler慢凭证查询的完整定位过程4.1 复现慢查询给凭证查询加上过滤条件和参数化一个典型的 C# WinForms 财务系统凭证查询页面长这样用户选择期间、科目、凭证号点查询DataGridView 加载结果。问题是只要期间跨月查询就卡到超过 2 秒。要复现先开启我们上一章那个 trace然后手工点一次查询等结果回来后停止 trace读回文件。你会发现一条 SQL:BatchCompletedTextData 类似这样exec sp_executesql NSELECT TOP (50) * FROM dbo.Voucher AS v LEFT JOIN dbo.VoucherDetail AS vd ON v.VoucherID vd.VoucherID WHERE v.Period period AND v.AccountCode accountCode ORDER BY v.VoucherID DESC,Nperiod int,accountCode nvarchar(10),period202404,accountCodeN1001注意看最后两个参数类型。accountCode声明成nvarchar(10)而财务系统表结构里AccountCode列是char(10)。SQL Server 为了比较不得不把 char 列隐式转换成 nvarchar导致 AccountCode 上的索引失效。这就是慢查询的根因它不会在 Ssms 里直接“EXPLAIN”给你看但 Profiler 的 Duration、Reads、CPU 三列会把问题暴露得很清楚。4.2 用 Profiler 抓到的字段反推 DAL 层源码问题看到上面这条 SQL 后回到财务系统源码里搜AccountCode参数的定义。如果用的是 ADO.NET你大概率会看到这样的代码cmd.Parameters.AddWithValue(accountCode, accountCodeTextBox.Text);AddWithValue最大的坑就是默认把字符串当nvarchar传。而 SQL Server 列是char或varchar时就会发生隐式转换。修复方式是把参数类型显式指定为SqlDbType.Char并设置长度。cmd.Parameters.Add( new SqlParameter(accountCode, SqlDbType.Char, 10) { Value accountCodeTextBox.Text.Trim() });参数说明SqlDbType.Char对应数据库的 char长度为 10值超过时会截断如果科目编码里可能有 Unicode 字符才改成NChar。财务系统的科目编码表字段长度是固定的这种修改不会影响数据却能让查询走索引。EF Core 里类似的问题也很常见。如果你发现抓回来的 SQL 是WHERE AccountCode __accountCode_0且参数是nvarchar就在实体配置里强制这一列固定长度modelBuilder.EntityVoucher() .Property(v v.AccountCode) .HasMaxLength(10) .IsFixedLength();改了之后重新跑一次Duration 会从 200 万微秒掉到几十万微秒。这里的关键不是“快了多少倍”而是你能从 Profiler 的 Reads 列看到扫描页数变化这才证明索引真的用上了。4.3 把免费 Profiler 接进 C# 源码的日志链路记录执行计划与锁等待只靠人工看文件还不够。免费 Profiler 要真正在 C# 财务系统里落地最好是把慢 SQL 落进一张表方便后面出账期报表。我一般会在一个独立监控库里建一张FinanceSlowSqlLog字段就是 Profiler 能抓到的核心信息CREATE TABLE dbo.FinanceSlowSqlLog ( LogID BIGINT IDENTITY PRIMARY KEY, EventTime DATETIME2(3) NOT NULL, AppName SYSNAME NOT NULL, LoginName SYSNAME NOT NULL, DatabaseName SYSNAME NOT NULL, SqlText NVARCHAR(MAX) NULL, DurationUs BIGINT NOT NULL, Reads BIGINT NOT NULL, Writes BIGINT NOT NULL, CpuTime BIGINT NOT NULL );C# 采集服务读到一个慢事件后直接异步插入这张表。插入动作会再次产生 SQL:BatchCompleted可能又超过阈值造成“日志套娃”。解决办法是让监控服务连接串的ApplicationName设为ProfilerCollector然后在采集端只处理ApplicationName ProfilerCollector的事件。这个经验是踩坑踩出来的不加这个过滤慢 SQL 日志表里全是监控服务自己的 INSERT。5. 免费 SQL Server Profiler 实现的 6 个坑从权限到漏抓5.1 事件文件没生成Trace 启动状态与 ALTER TRACE 权限现象执行完sp_trace_create和sp_trace_setstatus后等了很久目录里一个.trc文件都没有。原因有两个一是执行账号没有ALTER TRACE权限sp_trace_create可能成功返回 TraceID但 trace 实际没有运行二是路径不存在SQL Server 不会自动创建目录。解决先用sysadmin账号检查然后用SELECT * FROM sys.traces看 status 是否为 1路径目录要手动创建并确认 SQL Server 服务账号有写权限。开发环境我可以直接用 sa生产环境必须最小权限给GRANT ALTER TRACE TO finance_monitor;就够。5.2 慢 SQL 被漏掉event_retention_mode 与 lost events现象用 Extended Events 建会话后发现压力大的时候事件数量明显少于实际执行量甚至一个上午只抓到几十条。原因是默认的EVENT_RETENTION_MODE是ALLOW_SINGLE_EVENT_LOSS在服务器忙时可能丢弃事件等你读 xel 文件时根本不知道丢了多少。解决页面显示有 No event dropped 统计但文件里没有我一般会同时开启一个计数器事件做核对或者用EVENT_RETENTION_MODENO_EVENT_LOSS但这会加大内存和磁盘压力。对财务系统来说宁可丢一点性能监控数据也不能影响正常业务所以我选择允许损失但会把阈值调高只保留超过 1 秒的慢 SQL这样事件量本身不大。5.3 监控文件拖垮数据库 IO路径和 max_file_size 没配对现象开监控 20 分钟后数据库磁盘 IO 涨到 90%数据文件所在的盘被 trace 文件写爆。原因trace 文件默认路径放在了数据库数据盘并且sp_trace_create的max_file_size设得太大rollover 发生时产生大量单个大文件。解决把 trace 路径放到独立的 SSD 或本地磁盘max_file_size设为 20MB 左右rollover 开启再写一个计划任务把超过 3 天的.trc文件清掉。注意不能直接删除正在被写入的文件必须先停对应 trace 的 status再清。5.4 TextData LIKE 过滤翻车大小写与隐藏字符现象在过滤器里写TextData LIKE N%Voucher%结果 Profiler 列表里一条都没抓到但明明系统在跑SELECT * FROM dbo.Voucher。原因TextData 的排序规则依赖数据库默认 Collation很多财务库是SQL_Latin1_General_CP1_CI_AS大小写不敏感应该能匹配更常见的坑是 SQL 文本里有换行、注释、空格比如SELECT\n* FROM\nVoucherLIKE 也会匹配但如果你过滤的是参数化语句TextData 里存的是exec sp_executesql N...大小写及引号都可能干扰。解决不要在 TextData 上做复杂过滤改用ApplicationName和DatabaseName如果必须按表名过滤先查看sys.trace_columns确认列内容然后用LIKE N%Voucher%并去掉首尾空格。5.5 只看到存储过程调用看不到内部语句缺 SP:StmtCompleted现象财务系统月底结转调了一个sp_GL_ClosingProfiler 只看到RPC:Completed这一条看不到存储过程内部哪一条 UPDATE 慢。原因默认模板没有启用SP:StmtCompleted事件。解决在sp_trace_setevent里增加事件号 45SP:StmtCompleted并抓取 TextData、Duration、Reads。注意这个事件量很大一个存储过程循环执行 1000 次就会产生 1000 条记录。建议再增加一个过滤条件只保留耗时超过 500 毫秒的语句。这样你才能看到UPDATE AccountBalance SET ... WHERE ...具体哪一行在扫描。5.6 Profiler 抓到 NULL 的 ApplicationName连接池复用了旧连接现象明明在连接字符串里设置了ApplicationName FinanceGL_Posting但 Profiler 的 ApplicationName 列是空或显示旧模块名。原因SQL Server 连接池按完全相同的连接字符串复用连接如果你在运行中修改了连接字符串并 new 了一个 SqlConnection池里可能还保留旧的物理连接。解决修改连接字符串后必须清掉连接池执行SqlConnection.ClearPool(conn)或调用SqlConnection.ClearAllPools()。在财务系统里如果程序集热更新或者登录用户在切换账套时改了 ApplicationName这个问题特别阴。我通常会在监控日志里看到 ApplicationName 为 NULL 时先去查sys.dm_exec_sessions里 program_name 列看看实际会话用的是哪个连接。6. 更值钱的用法把 Profiler 事件流做成财务慢查询自动诊断器6.1 按模块统计耗时 Top N一个 ConcurrentDictionary 窗口统计当 trace 文件源源不断进来时手工看每条 SQL 已经没有意义。我会在采集端维护一个窗口统计按 ApplicationName 加表名作为 key统计每个模块过去 5 分钟的总耗时和最大耗时。这里用 C# 的 ConcurrentDictionary 最合适因为它天然支持多线程读写。var stats new ConcurrentDictionarystring, ModuleStat(); void OnTraceReceived(TraceEvent evt) { var key ${evt.ApplicationName}|{ExtractTableName(evt.TextData)}; stats.AddOrUpdate(key, new ModuleStat { Count 1, TotalUs evt.Duration, MaxUs evt.Duration }, (_, old) new ModuleStat { Count old.Count 1, TotalUs old.TotalUs evt.Duration, MaxUs Math.Max(old.MaxUs, evt.Duration) }); }ModuleStat是一个简单的类包含 Count、TotalUs、MaxUs 三个字段。这样每个模块、每张表的耗时排行可以在内存里算出来按TotalUs降序取前十条就是财务系统当前最慢的十个热点。相比直接在 Profiler 窗口里看这种办法能保存下来并且可以对接 C# 财务系统自己的运维页面。6.2 结合执行计划与索引使用给出修改意见统计只是第一步。我会在抓取到慢 SQL 后自动去拿它的实际执行计划把 TextData 里的语句提取出来在前面包一个SET SHOWPLAN_XML ON或者直接查sys.dm_exec_query_plan。然后在报告里把关键连接词标记出来物理读很高的对应表有哪些缺失索引提示是什么。这样生成的诊断结果可以直接发给开发组改 DAL 层源码。最后说一个习惯我做的免费 Profiler 一直坚持“写日志不如出报表”。每个月末结账前跑一次这个采集器把 Top 10 慢查询和锁等待事件导成 Excel发给财务系统维护群。这样不依赖某个熟悉 Profiler 的人在场业务人员也能看懂“凭证查询慢 2 秒卡在科目编码隐式转换”这种结论。如果一个工具只有自己能操作它最终会被遗忘。把 Profiler 的能力沉淀成可视化报表才是它真正值钱的地方。希望帮到你。本文还有配套的精品资源点击获取