如何避免从 C# 构建的 Sql Server 2005 参数化查询变慢

Posted

技术标签:

【中文标题】如何避免从 C# 构建的 Sql Server 2005 参数化查询变慢【英文标题】:How to avoid slow down on Sql Server 2005 parameterized queries build from C# 【发布时间】:2011-01-21 11:29:06 【问题描述】:

我正在构建一个复杂的查询以在 Web 视图中显示一些统计结果。根据用户的选择,视图可以有几个不同的过滤器。此外,还可以使用通配符。

我正在使用 SqlParameters 在 c# 中以编程方式构建此查询。所以查询看起来像这样:

sc.CommandText = "SELECT * FROM table 
                  WHERE field1 = @filter1 
                  AND field2 LIKE @filter2"; //...and more parameters

sc.SqlParameters.Add(
   new SqlParameter("@filter1", SqlDbType.Int, 32)  Value = 1);

sc.SqlParameters.Add(
   new SqlParameter("@filter2", SqlDbType.VarChar, 446)  Value = "whatever%");

这是一个非常简化的版本,但查询本身并不是重点。请记住,它可以有不同的可选参数(我认为这是很常见的情况)。

当我在 Sql Manager 中运行此查询时,我意识到使用参数时速度非常慢。因此,以下两个查询应该是相同的,它们使用不同的执行计划,使参数化的查询运行慢很多:

DECLARE @filter1 INT
DECLARE @filter2 VARCHAR 446
SET @filter1 = 1
SET @filter2 = "whatever%"

SELECT * FROM table WHERE field1 = @filter1 AND field2 LIKE @filter2

快速版:

SELECT * FROM table WHERE field1 = 1 AND field2 LIKE 'whatever%'

这是另一个有同样问题的人的例子:

Why does a parameterized query produces vastly slower query plan vs non-parameterized query

似乎有一个名为parameter sniffing 的东西可能会使参数化查询运行得更慢,但它不适用于我的情况,因为这不是存储过程。

solutions 建议之一是使用 OPTION(RECOMPILE) 或 OPTION(OPTIMIZE FOR)。我不能这样做,因为我有大约 10 个可选参数,它们可能在过滤器中或不在过滤器中,并且在使用 LIKE 时此选项不起作用。

所以,我觉得我陷入了死胡同,我正在考虑摆脱参数并在代码上构建动态文字查询。但随后 Sql Injection 出现在游戏中。

那么,您对如何解决此问题还有其他建议吗?或者您知道转义参数的安全方法吗?

编辑:在这里,您可以使用LIKE查看带有一个参数的查询的执行计划:

Execution Plan

编辑:更简化的代表性查询执行计划:

Simplified execution plan

【问题讨论】:

参数嗅探不仅仅适用于存储过程。它适用于参数化执行计划被缓存并稍后被传递不同参数值的查询的其他调用重用的任何查询。当您使用文字值时,查询优化器可以对查询进行某些简化,这些简化对特定情况有效,但不适用于可能需要应用于任何参数值的计划(这与参数嗅探不同)你能提供一个示例执行计划来说明您遇到的确切问题? @Martin 我已经添加了执行计划。 @despart - 第一个计划中进入哈希匹配的顶部箭头是哪个? (我可以在底部计划中看到它来自like 我只是想知道这是否在好计划中颠倒了) @Martin 在这两个计划中,顶部的箭头是 LIKE 这可以解释为什么你得到的是串行计划而不是并行计划。不过,我怀疑这可能解释 42 倍的性能差异。我想知道低估是否也意味着授予的内存不足并且您收到大量哈希警告? (你可以在 profiler 中看到) 【参考方案1】:

查看执行计划中的“估计行数”属性。对于您的慢版本(带有参数),SQL Server 无法很好地估计您的查询将返回的行,因为它不会在编译时评估变量的实际值。它只会利用统计信息来估计您用作过滤器的那些字段的基数,并据此创建执行计划。

我对此类问题的解决方案是创建一个存储过程,其参数与您想要的过滤器一样多:

CREATE PROCEDURE your_sp @filter1 INT, @filter2 VARCHAR(446) AS
 SELECT * FROM table 
 WHERE field1 = @filter1 
 AND field2 LIKE @filter2

sc.CommandText = "your_sp";
sc.CommandType = CommandType.StoredProcedure;

sc.SqlParameters.Add(new SqlParameter("@filter1", SqlDbType.Int, 32)  Value = 1);

sc.SqlParameters.Add(new SqlParameter("@filter2", SqlDbType.VarChar, 446)  Value = "whatever%");

connection.Open();
SqlDataReader reader = command.ExecuteReader();

【讨论】:

这将如何帮助改进来自计划的参数化 LIKE 分支的基数估计? 检查msdn.microsoft.com/en-us/library/ms175933(v=SQL.90).aspx中的“Compile-Time Expression Evaluation for Cardinality Estimation”我认为答案就在那里,尽管我给出的解决方案来自实践经验而不是理论。 我仍然不确定我是否错过了您的建议。您是说如果将其保留为参数化查询但放入存储过程中,这将解决问题? 没错。通过这种方式,参数在编译时被评估。看看那里是怎么说的:“避免在查询中使用局部变量。相反,在查询中使用参数、文字或表达式。”对于局部变量,优化器在估计选择性时使用默认估计值(例如总行数的 30%)。 这将发生在 OP 的原始查询中。它仍然是一个参数化查询。【参考方案2】:

有时需要过滤索引的查询会导致问题。

我刚刚遇到了类似的查询

orders.Where(x => x.Cancelled == options.isCancelled)

options.isCancelled 是一个动态布尔值。这在使用 EFCore 的 SQL 查询中被参数化为 SELECT ... FROM Orders WHERE cancelled = @param_cancelled 之类的东西。数据库不能使用过滤索引,因为它事先不知道值是什么。

我的解决方案是:

if (options.isCancelled) 

   orders = orders.Where(o => o.Cancelled == true);

else 

   orders = orders.Where(o => o.Cancelled == false);

这使查询优化器能够使用我创建的过滤索引:

`(WHERE IsCancelled = 1)`. 

这个索引大大提高了性能。

TBH 问题仍在继续,因为即使没有索引,它在 SSMS 中也能正常工作,但在 C# 中完全超时。这个技巧强制执行两个不同的不同查询,SQL Server 需要为其找到两个独立的查询计划。所以这至少让我有信心继续前进,即使我仍然不能 100% 确定发生了什么。

【讨论】:

以上是关于如何避免从 C# 构建的 Sql Server 2005 参数化查询变慢的主要内容,如果未能解决你的问题,请参考以下文章

如何将大型 SQL Server 表拉入 C# 进行分析

SqlException:在 C# (VISUAL STUDIO) 和 sql server 中构建网站时出现无效的对象名称错误

如何从 C# 代码将 SQL Server CE 迁移到 SQL Server 2012 Express

如何使用 C# 从 sql server 读取时间戳类型的数据?

如何从 C# 动态运行 SQL Server CE [Windows mobile] 中的 SQL 脚本文件?

如何从 c# 将 SQL Server 数据库转换/导出到 MSAccess