20

我们有一个现有的 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)现在每个临时查询的缓存中都有一个计划,以及字符串参数的类型现在是 varchar(max)。

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

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

更新(1 月 16 日)

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

非常感谢 Martin Smith,他的回答(见下文)为我指出了分析这个问题的正确方法。我使用我们应用程序的用户表进行了测试,该表有一个定义为 nvarchar(100) 的电子邮件列,并且在电子邮件列上有一个非聚集索引 (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 日)

我们不得不修改之前的版本(上面的新提案)来处理参数值大于最大值的情况。此时,您别无选择,只能将其设为 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;
}

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

4

1 回答 1

4

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

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

尝试以下测试

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(max)针对按列分区的表的参数varchar(n)

于 2013-01-15T19:19:13.187 回答