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

Ran*_*ard 17 c# sql-server ado.net

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

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

if((sqlDbType == SqlDbType.VarChar) || (sqlDbType == SqlDbType.NVarChar))
    m_sqlParam.Size = -1;
Run Code Online (Sandbox Code Playgroud)

换句话说,每次传递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
Run Code Online (Sandbox Code Playgroud)

根据我取消评论的"声明"语句,我得到了一个非常不同的查询计划.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;
Run Code Online (Sandbox Code Playgroud)

这似乎解决了计划缓存污染问题,而不会像使用-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;
}
Run Code Online (Sandbox Code Playgroud)

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

Mar*_*ith 4

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

\n\n

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

\n\n

尝试以下测试

\n\n
CREATE TABLE #T\n(\nX VARCHAR(10) PRIMARY KEY\n)\n\n\nDECLARE @A VARCHAR(MAX) = CAST(\'A\' AS VARCHAR(MAX))\n\nSELECT *\nFROM #T \nWHERE X = @A\n
Run Code Online (Sandbox Code Playgroud)\n\n

给出一个像这样的计划

\n\n

计划

\n\n

SQL Server 将一个计算标量添加到调用内部函数的计划中GetRangeWithMismatchedTypes,并且仍然设法执行索引查找(有关隐式转换的更多详细信息请参见此处)。

\n\n

文章《为什么\xe2\x80\x99t 分区消除有效?》中显示了一个重要的反例。。该文章中描述的行为也适用于varchar(max)针对分区表的参数varchar(n)

\n