ADO.NET:安全地为所有VarChar参数指定SqlParameter.Size的-1?

我们有一个现有的C#代码体,它在很多地方调用参数化的临时SQL Server查询。 我们从不指定SqlParameter.Size,并且记录了在这种情况下,SqlParameter类从参数值推断出大小。 我们最近才意识到这会产生SQL Server计划缓存污染问题,其中为每个不同的参数大小组合缓存单独的计划。

幸运的是,每当我们创建一个SqlParameter时,我们都是通过一个实用方法来实现的,所以我们有机会在该方法中添加几行并使这个问题消失。 我们正考虑添加以下内容:

if((sqlDbType == SqlDbType.VarChar) || (sqlDbType == SqlDbType.NVarChar)) m_sqlParam.Size = -1; 

换句话说,每次传递varchar参数时,都将其作为varchar(max)传递。 基于一些快速测试,这很好用,我们可以看到(通过SQL Profiler和sys.dm_exec_cached_plans)每个ad-hoc查询的缓存中现在有一个计划,以及字符串参数的类型现在是varchar(max)。

这似乎是一个简单的解决方案,必须有一些隐藏的,破坏性能的缺点。 有人知道吗?

(请注意,我们只需要支持SQL Server 2008及更高版本。)

更新(1月16日)

是的,有一个隐藏的,破坏性能的缺点!

非常感谢Martin Smith,他的回答(见下文)向我指出了正确的分析方法。 我使用我们的应用程序的Users表进行了测试,该表具有定义为nvarchar(100)的Email列,并且在Email列上具有非聚集索引(IX_Users_Email)。 我修改了Martin的示例查询,如下所示:

 declare @a nvarchar(max) = cast('a' as nvarchar(max)) --declare @a nvarchar(100) = cast('a' as nvarchar(100)) --declare @a nvarchar(4000) = cast('a' as nvarchar(4000)) select Email from Users where Email = @a 

根据我取消评论的“声明”语句,我得到了一个非常不同的查询计划。 nvarchar(100)和nvarchar(4000)版本都给我一个IX_Users_Email的索引搜索 – 实际上,我指定的任何长度都给了我相同的计划。 另一方面,nvarchar(max)版本为我提供了对IX_Users_Email的索引扫描 ,然后是Filter运算符以应用谓词。

这对我来说已经足够了 – 如果有可能进行扫描而不是寻求,那么这种“治愈”比疾病更糟糕。

新提案

我注意到每次SQL Server使用varchar参数参数化查询时,缓存计划只使用varchar(8000)(或nvarchar(4000))作为参数。 我认为如果它对SQL Server来说足够好,对我来说这已经足够了! 用我原来的问题(上面)替换C#代码:

 if(sqlDbType == SqlDbType.VarChar) m_sqlParam.Size = 8000; else if(sqlDbType == SqlDbType.NVarChar) m_sqlParam.Size = 4000; 

这似乎解决了计划缓存污染问题,而不会像使用-1的大小那样对查询计划产生相同的影响。 但是,我没有对此进行过大量的测试,我很想听听任何人对此修订方法的意见。

更新(9月24日)

我们必须修改先前版本(上面的New Proposal)来处理参数值超过最大值的情况。 此时,您别无选择,只能将其设为varchar(max):

 if((sqlDbType == SqlDbType.VarChar) || (sqlDbType == SqlDbType.NVarChar)) { m_sqlParam.Size = (sqlDbType == SqlDbType.VarChar) ? 8000 : 4000; if((value != null) && !(value is DBNull) && (value.ToString().Length > m_sqlParam.Size)) m_sqlParam.Size = -1; } 

我们已经使用这个版本大约六个月没有问题。

它并不理想,因为最好指定一个与所涉及的列的数据类型匹配的参数。

您需要检查您的查询计划,看看它们是否仍然合理。

尝试以下测试

 CREATE TABLE #T ( X VARCHAR(10) PRIMARY KEY ) DECLARE @A VARCHAR(MAX) = CAST('A' AS VARCHAR(MAX)) SELECT * FROM #T WHERE X = @A 

给出一个像这样的计划

计划

SQL Server向计划添加计算标量,该计划调用内部函数GetRangeWithMismatchedTypes并仍设法执行索引查找( 有关隐式转换的更多详细信息 )。

一个重要的例子显示在文章为什么不分区消除工作? 。 该文章中描述的行为也适用于针对varchar(n)列上分区的表的varchar(max)参数。