【问题标题】:Slow SqlCommand performance with longer CommandText较长的 CommandText 会降低 SqlCommand 的性能
【发布时间】:2009-08-28 18:46:52
【问题描述】:

SqlCommand 的 CommandText 的长度有影响吗?我也不是在谈论数千个字符。这是我所拥有的:

SqlCommand cmd = new SqlCommand();
cmd.Connection = conn;
cmd.CommandText = sql;

for (int i=0; i<1000; ++i)
{
    string name = i.ToString() + "Bob" + i.ToString();
    string email = i.ToString() + "Jim" + i.ToString();
    // etc...

    cmd.Parameters.Clear();
    cmd.Parameters.Add(new SqlParameter("@name", name));
    cmd.Parameters.Add(new SqlParameter("@country", country));

    DateTime cmdStart = DateTime.Now;
    cmd.ExecuteNonQuery();
    DateTime cmdEnd = DateTime.Now;
    TimeSpan len = cmdEnd - cmdStart;
}

如果我使用以下 sql,第一次迭代需要 0.5 秒。第二个需要 1.1 秒。第三个需要 3.3 秒。依此类推,直到它只是抛出一个超时。

string sql =
    "INSERT INTO Test " +
    "           ([name] " +
    "           ,[email] " +
    "           ,[country] " +
    "           ,[comment] " +
    "           ,[date] " +
    "           ,[key_v0] " +
    "           ,[key_v1] " +
    "           ,[expires_v1] " +
    "           ,[expires_v2] " +
    "           ) " +
    "     VALUES " +
    "           (@name " +
    "           ,@email " +
    "           ,@country " +
    "           ,' ' " +
    "           ,@date " +
    "           ,@key_v0 " +
    "           ,@key_v1 " +
    "           ,@expires_v1 " +
    "           ,@expires_v2 " +
    "           )";

但是,如果我使用下面的 sql,整个循环会在一秒钟内执行完毕。

string sql =
    "INSERT INTO Test " +
    "([name] " +
    ",[email] " +
    ",[country] " +
    ",[comment] " +
    ",[date] " +
    ",[key_v0] " +
    ",[key_v1] " +
    ",[expires_v1] " +
    ",[expires_v2] " +
    ") " +
    "VALUES " +
    "(@name " +
    ",@email " +
    ",@country " +
    ",' ' " +
    ",@date " +
    ",@key_v0 " +
    ",@key_v1 " +
    ",@expires_v1 " +
    ",@expires_v2 " +
    ")";

唯一的区别是空格。删除空格使总字符数从 428 增加到 203。除了对 4k 和 8k 限制的引用之外,我无法找到任何引用 CommandText 长度的内容。我离那还差得很远。

我已经在运行探查器的情况下运行了两个版本,并且所有调用的持续时间都低于 10 毫秒。延迟似乎是从 SQL 引擎中的命令完成到 ExecuteNonQuery 返回。

我知道有其他方法可以做到这一点。我不是在问更好的方法来做到这一点。我在问减速的根源。

更新: 作为测试,我在查询末尾添加了空格。一旦我的字符总数超过 400 个,它就变慢了。有趣的是,在 414 个字符中,前 99 个插入速度很快。在 415 个字符处,前 9 个插入速度很快。因为我正在根据迭代次数更改一些字符串,所以这有点道理。例如第 10 个刀片比第 9 个刀片长一点,第 100 个刀片比第 99 个刀片长一点。

虽然我有点理解更长的插入应该需要更长的时间,但我无法理解快速和慢速之间的明确划分以及差异的巨大程度。我也不明白为什么花费的时间会增加。

更新 2: (针对 Peter Oehlert 的回答的附加信息): 整个数据库是干净的。没有其他表,并且每次运行都会删除并重新创建测试表。没有索引、触发器或外键。有一个“id”列是主键。

这是从专门为解决此问题而编写的控制台应用程序中提取的代码。它只包含重现此行为的必要代码。

(附加分析器信息): 运行 SQL 分析器时,有一个名为 TextData 的列显示命令和数据是什么。一个例子是这样的:

exec sp_executesql N'INSERT INTO Test ([name] ,[email] ,[country] ,[comment] ,[date] ,[key_v0] ,[key_v1] ,[expires_v1] ,[expires_v2] ) VALUES (@name ,@email ,@country ,'' '' ,@date ,@key_v0 ,@key_v1 ,@expires_v1 ,@expires_v2 )                                                                                                                                                                                                                          ',N'@name nvarchar(4),@country nvarchar(2),@email nvarchar(3),@date datetime,@key_v0 nvarchar(4000),@key_v1 nvarchar(4000),@expires_v1 datetime,@expires_v2 datetime',@name=N'9Bob',@country=N'us',@email=N'Jim',@date='2009-08-28 15:35:36.5770000',@key_v0=N'',@key_v1=N'',@expires_v1='2009-08-28 15:35:36.5770000',@expires_v2='2009-08-28 15:35:36.5770000'

那行有 796 个字符,运行速度很快。将名称从“9Bob”更改为“10Bob”会导致插入速度变慢。 796 和 797 似乎都不是一个重要的数字。删除 exec sp_executesql 部分意味着长度为 777 和 778。它们似乎也不重要。

我被难住了。

更新: 在此处发布跟踪:http://www.jere.us/WierdInserts.trc

【问题讨论】:

  • 0确保在一种情况下不要将sql 添加到cmd.CommandText,并在另一种情况下替换CommandText
  • 这与您的问题无关,但通常您应该使用 StringBuilder 来创建更长的字符串连接,而不是加号运算符。效率更高。
  • @Adrian Grigore,不一定...因为他正在连接文字字符串,编译器应该能够优化它。
  • 如果你将'sql'变量声明为静态只读会发生什么?
  • 另外:使用 system.diagnostics.stopwatch 而不是 datetime.now 进行基准测试。

标签: c# sql-server


【解决方案1】:

如果 10Bob9Bob 99Bob 慢得多,则可能会指向 INDEX FILLFACTOR 设置得太高的名称,或者当 SQL Server 遇到“10Bob”中的“1”时,SQL Server 必须重新索引页面。

所以,这解释了 Bob,除了你说没有索引和空格也有区别......

768 字节是 MySQL 中决定 BLOB 是内联存储还是存储在单独表中的重要边界。也许 SQL 查询优化器也有类似的边界?

这可能解释了微小的性能差异,但不能解释数量级。

SQL Server 默认使用 8k 页面,因此当第一个 INSERT 出现需要新页面时,可能会出现微小的性能损失,但同样,与空白无关,也没有解释此处的延迟量。

【讨论】:

    【解决方案2】:

    好吧,我没有直接的答案,但这是我解决问题的方法。

    1. 确定造成问题的原因。第一步是运行 SQL 分析器并查看数据库是否存在问题或代码中有问题。

    SQL

    如果它是数据库,那么我会看一些东西:每个人都在谈论字符串连接问题,而这些问题可能会占您不到 5 毫秒的时间。我也会将这些空间视为问题的根源。同样,它会产生很小的差异,但不会解释您描述的退化。您正在寻找具有这种进展(0.5、1.1、3.3)的东西。

    我会特别查看您在此表上定义的索引、此表上的约束/触发器以及存在多少外键关系。此外,我会提取执行缓慢的查询并在查询管理器(sql 企业管理器)中运行它们。

    我可能会调查的最后一件事是,是否有一个糟糕的缓存计划会引入一些数据相关功能的问题。这仅在您有一些有趣的触发器使用您的数据或某些类型的索引更新时才有效。您可以通过在调用您的插入语句之间调用 DBCC FREEPROCCACHE 来查看这一点,看看它是否有所作为。此命令将清除计划缓存,强制 sql 为您的查询重新生成新的执行计划。

    客户

    如果是客户端,那么您需要确定代码中导致问题的原因。如果您有一个性能跟踪工具(如 Visual Studio 性能分析器)来检测您的代码,我会使用它,因为它会非常快速准确地告诉您是什么占用了这么多时间。

    如果您没有该选项,那么首先将您的代码提取到一个具有尽可能少的意外事件的新控制台应用程序中,看看您是否可以重现该行为。您正在寻找任何可能导致您所看到的进展的原因。

    【讨论】:

    • 好答案。 (+1) 我有时只是在代码中寻找答案而不是调查。当答案很明显时,它可以节省时间,但有时会浪费时间寻找错误的区域。我喜欢你系统地调查问题所在的方法。赞一个!
    【解决方案3】:

    .Net 中的字符串是不可变的。这有很多好处,但一个缺点是要将它们中的两个连接在一起,您必须为全新的第三个字符串分配一个缓冲区,而不是仅仅为第一个字符串扩展缓冲区。您在那里显示的代码有 21 个单独的连接操作,这可能会很慢。通常我希望 jit-optimizer 为你解决这个问题,但它可能会以某种方式错过这个问题。您可以尝试将变量声明为 static readonly 看看是否有帮助。

    即便如此,我只希望这最多会产生几毫秒的差异。这几乎不会影响您的查询。我能给您的最佳建议是获取您的查询的两个版本并将它们粘贴到不同的管理工作室窗口中,以及您的每个参数的 DECLARE 和 SET 语句,然后比较执行计划。

    最后,David Stratton 关于重复使用相同参数的建议是合理的。当您可以简单地更新值并重新运行相同的查询时,每轮清除和重新创建相同的参数是没有意义的。

    【讨论】:

      【解决方案4】:

      我认为部分性能影响在于清除和添加参数(以及无效的字符串操作)。如果你像这样稍微改变一下结构会发生什么?

      SqlCommand cmd = new SqlCommand();
      cmd.Connection = conn;
      cmd.CommandText = sql;
      cmd.Parameters.Add(new SqlParameter("@name", ""));
      cmd.Parameters.Add(new SqlParameter("@country", ""));
      // etc..
      
      
      for (int i=0; i<1000; ++i)
      {
                // etc...
          cmd.Parameters["@name"].Value =  i.ToString() + "Bob" + i.ToString();
          cmd.Parameters["@country"].Value =  i.ToString() + "Uganda" + i.ToString();
          DateTime cmdStart = DateTime.Now;
          cmd.ExecuteNonQuery();
          DateTime cmdEnd = DateTime.Now;
          TimeSpan len = cmdEnd - cmdStart;
      }
      

      添加

      我也是看错了代码,现在才意识到这是一个CommandType.Text,不是吗?

      是否在服务器上设置了一个真正的存储过程,然后通过指定 CommandType.StoredProcedure 并传递存储过程名称而不是 QL 语句来调用它?我意识到我没有回答你的基本问题,但我真的不认为 CommandText 的长度对 SQL Server 很重要,所以我正在研究其他可能的性能障碍。

      这是在黑暗中的一枪,我希望对 CommandObject 如何解析带有参数的文本 SQL 语句的内部知识有一定了解的人可以验证这一点,但我猜测发生的情况是 Command 对象在调用 ExecuteNonQuery()(或 ExecuteScalar() 等)时必须解析 CommandText。

      字符串操作代价高昂,如果您每次都强制命令对象重新解析参数,这也可能会在未来添加。

      此外,由于已编译执行计划,真正的存储过程通常性能更好,您可能会看到一些改进。

      【讨论】:

      • 我最初确实尝试过,但唯一将性能从“5 条记录并且失败”更改为“在不到一秒的时间内完成”的唯一方法是删除空格。另外,我只是在测量 ExecuteNonQuery 调用的时间。
      • 最初的想法是只使用此代码一次将一些数据从 MySql db 移动到 MSSql db。因此缺少存储过程。现在我只是想弄清楚到底发生了什么。
      • 不,命令对象 从不 解析命令文本。它让 sql server 做到这一点。参数化查询的意义在于,这不必发生。
      【解决方案5】:

      您的跟踪在持续时间 0-3 毫秒内具有所有插入。 次执行之间有更重要的时间:一个插入在 12:53:10 结束,下一个从 12:53:13 开始,所以 客户端有 3 秒的延迟 在两个 INSERT 之间。从技术上讲,延迟可以在客户端和服务器之间的任何地方,但从你描述的症状来看,我会排除客户端和服务器之间随机的不稳定路由器(行为会更加随机)。

      我想看看的地方:

      • 数据库增长事件/日志增长事件。这在测试中很常见,因为测试设置部署了一个全新的测试数据库,然后测试在大约同一时刻(比如第 10 个插入)触发了增长事件。可以使用性能计数器和分析器事件轻松验证:Data File Auto Grow Event Class。解决方案是预先增长测试数据库(实际上,您应该始终预先增长 mdf 和 ldf 以进行性能测试)。但不会考虑几何速度的时间增长。
      • 客户端中的垃圾收集器。同样,可以在性能计数器中进行跟踪。
      • 连接池耗尽(即测试泄漏连接,连接池必须打开新连接并设置为保持最小计数,因此它分批打开)。 sys.dm_exec_connecitons 会增长。还有用于用户会话的 perfmon 计数器(在服务器和 ADO.Net 计数器上)。
      • 代码缺陷。客户端代码中的一些等待/处理,可能是列表处理。这是最可能的原因,是唯一解释延迟增加平方率的原因(NxN 表示 N 长度列表,N 随着每次测试运行而增加,可能是排序,或测试结果保存,或类似的东西)。

      【讨论】:

      • 我喜欢你的回答。你看到的正是我所看到的。我希望它是代码缺陷,但您会看到上面的所有代码。循环就像描述的那样。没有排序。没有储蓄。只需插入循环插入等。
      【解决方案6】:

      这个问题是很久以前提出的,但也许我的回答会对某人有所帮助。 我刚刚遇到了完全相同的问题,发现这只发生在虚拟机内部(我使用 VirtualBox 进行开发)。我编写了一些测试并在 VM 和生产服务器上运行它们。在生产服务器上没有这样的性能问题。

      可能是Virtualbox有奇怪的bug,可能是VM的网络设置有问题(我使用默认设置)。

      【讨论】:

        【解决方案7】:

        SQL Server 在执行之前会解析您的查询,解析更简单,不需要的代码更少 = 解析更快。它必须在解析之前去除所有非额外的空白,但是当您执行多个操作时,每个较小的 cpu 周期都会计数。

        您可以通过两次增加空格数来验证测试,您应该会看到更大的减速,因为逐个字符的解析处理,它与字符数成正比。而解析是第一步,即使是参数化查询,SQL仍然需要解析并理解请求的内容。

        【讨论】:

        • 没错,需要更长的时间。但我们仍在谈论毫秒。我不认为查询解析是这里的问题。
        • 我能想到的就这么多了,剩下你的查询很简单,你可以尝试增加空格数,如果没有变化我们可以考虑别的。
        • 查询不会因为解析而超时,这是肯定的
        • 我同意我不认为它是解析器。 SQL Server 可以非常非常快速地解析出不需要的文本。确实,它不是最理想的,但这不会像描述的那样(0.5s、1.1s、3.3s)对 O(N^2) 减速负责。
        • 问题被问到额外的空格如何产生影响,并且没有指定其他额外的代码,我们不能在黑暗中拍摄,让我们认为我错了,那么计划 b 是什么?我得到-1但是看大家都在谈论jit优化等,看代码,CommandText甚至在for循环之外,它的一次操作,分析器显示它在sql server中花费的时间,让更多的证据而不是射击在黑暗中!!
        猜你喜欢
        • 2016-02-25
        • 1970-01-01
        • 2015-07-01
        • 2018-12-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多