You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何手动测量记录SQL Server存储过程执行时间(不使用SQL Profiler)

方案可行性判断

你提出的封装SqlConnection/SqlCommand的方案完全可行,属于低侵入性埋点,非常适合遗留系统的改造需求。需要注意的是.NET Framework 2.0不支持C# 3.0才引入的扩展方法,因此你构思的扩展方法实现需要调整为包装类实现。


包装类实现示例

1. 埋点SqlCommand封装

// 自定义用户信息类,存储你需要的用户、部门字段
public class UserInfo
{
    public string UserId { get; set; }
    public string DepartmentId { get; set; }
    public string UserName { get; set; }
}

public class TracedSqlCommand : IDisposable
{
    private readonly SqlCommand _innerCommand;
    private readonly UserInfo _currentUser;

    public TracedSqlCommand(string commandText, SqlConnection connection, UserInfo currentUser)
    {
        _innerCommand = new SqlCommand(commandText, connection);
        _currentUser = currentUser;
        // 默认指定是存储过程类型,可根据你的业务调整
        _innerCommand.CommandType = CommandType.StoredProcedure;
    }

    // 封装原有ExecuteNonQuery方法,注入计时逻辑
    public int ExecuteNonQuery()
    {
        Stopwatch stopwatch = Stopwatch.StartNew();
        try
        {
            return _innerCommand.ExecuteNonQuery();
        }
        finally
        {
            stopwatch.Stop();
            // 上报埋点数据
            TraceReporter.Submit(_innerCommand.CommandText, stopwatch.ElapsedMilliseconds, _currentUser);
        }
    }

    // 按同样逻辑封装其他执行方法
    public SqlDataReader ExecuteReader()
    {
        Stopwatch stopwatch = Stopwatch.StartNew();
        try
        {
            return _innerCommand.ExecuteReader();
        }
        finally
        {
            stopwatch.Stop();
            TraceReporter.Submit(_innerCommand.CommandText, stopwatch.ElapsedMilliseconds, _currentUser);
        }
    }

    public object ExecuteScalar()
    {
        Stopwatch stopwatch = Stopwatch.StartNew();
        try
        {
            return _innerCommand.ExecuteScalar();
        }
        finally
        {
            stopwatch.Stop();
            TraceReporter.Submit(_innerCommand.CommandText, stopwatch.ElapsedMilliseconds, _currentUser);
        }
    }

    // 封装Parameters属性,原有给参数赋值的代码无需修改
    public SqlParameterCollection Parameters => _innerCommand.Parameters;

    public void Dispose()
    {
        _innerCommand?.Dispose();
    }
}

2. 埋点上报工具类

public static class TraceReporter
{
    public static void Submit(string storedProcedureName, long elapsedMs, UserInfo user)
    {
        string logLine = string.Format("{0}|{1}|{2}|{3}|{4}",
            DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
            storedProcedureName,
            elapsedMs,
            user.DepartmentId,
            user.UserId);
        // 可根据需求调整为写本地日志/异步上报到监控看板后端/写入专用监控表
        File.AppendAllText(@"C:\LDA_Monitor\trace.log", logLine + Environment.NewLine);
    }
}

3. 原有代码改造量

仅需要把原有new SqlCommand的逻辑替换为new TracedSqlCommand即可,所有业务逻辑、参数赋值代码完全不需要调整,侵入性极低:

using (SqlConnection conn = new SqlConnection(ConnString))
{
    conn.Open();
    // 仅需要修改这一行初始化代码
    using (TracedSqlCommand cmd = new TracedSqlCommand("SP_XXX_Operate", conn, currentUserObject))
    {
        cmd.Parameters.AddWithValue("@Id", 123);
        cmd.ExecuteNonQuery();
    }
}

WinForm层面全局埋点方案

如果不想修改任何业务代码,还可以通过全局事件挂钩的方式实现全量按钮点击埋点,可以和上面的SQL埋点联动,定位到具体是哪个表单的哪个按钮触发的慢查询,排查效率更高:

// Program.cs入口修改
static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    // 注册全局消息过滤器,捕获所有Form加载事件
    Application.AddMessageFilter(new ButtonTraceFilter());
    Application.Run(new MainForm());
}

// 全局消息过滤器
public class ButtonTraceFilter : IMessageFilter
{
    private const int WM_SHOWWINDOW = 0x0018;
    public bool PreFilterMessage(ref Message m)
    {
        // 捕获窗口显示事件
        if (m.Msg == WM_SHOWWINDOW && m.WParam.ToInt32() == 1)
        {
            Control control = Control.FromHandle(m.HWnd);
            if (control is Form form)
            {
                AttachButtonTrace(form);
            }
        }
        return false;
    }

    // 递归给当前Form下所有按钮绑定统一点击埋点
    private void AttachButtonTrace(Control parent)
    {
        foreach (Control ctl in parent.Controls)
        {
            if (ctl is Button btn)
            {
                btn.Click += (sender, e) =>
                {
                    // 把当前点击的表单、按钮信息存入线程上下文,SQL埋点可以直接读取
                    TraceContext.CurrentFormName = parent.FindForm()?.Name;
                    TraceContext.CurrentButtonText = btn.Text;
                    TraceContext.TraceId = Guid.NewGuid().ToString();
                };
            }
            // 递归遍历容器内的子控件
            if (ctl.Controls.Count > 0)
            {
                AttachButtonTrace(ctl);
            }
        }
    }
}

// 线程静态上下文,保证多线程下数据隔离
public static class TraceContext
{
    [ThreadStatic]
    public static string TraceId;
    [ThreadStatic]
    public static string CurrentFormName;
    [ThreadStatic]
    public static string CurrentButtonText;
}

该方案完全不需要修改原有业务代码,仅需要在应用入口增加几十行代码即可实现全量按钮点击埋点,和SQL层埋点结合后可以实现全链路耗时统计。


内容的提问来源于stack exchange,提问作者Achilles_VN

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.24 11:15:07