前面我们了解了参数嗅探可能是好的也可能是坏的。当数列的分布不均匀的时候参数嗅探就是不好的事情。例如,考虑“Status”列在Orders表中有总共10M行。该列有7个不同的值,如下分布:
另外的使用参数嗅探的糟糕情况是用非相等的谓词使用参数。请看下面的查询:
如果查询使用参数嗅探编译,使用值“2014-07-01″ 和“2014-08-01″,那么优化器基于统计估计行数并且大概估计行数为20000。然后创建基于这个估计行数的计划并且放在缓存中。后来的执行可以使用完全不同的参数。例如,用户执行查询用时间参数“2012-01-01″ 和“2014-01-01″。结果集大概有61000行,但是基于之前的行数的计划被重用,并且很可能不是一个好的执行计划。
我将展示一些基于我之前使用存储过程实例的技术:
<div class=”cnblogs_Highlighter”>
<pre class=”brush:sql;gutter:true;”>CREATE PROCEDURE
Marketing.usp_CustomersByCountry
(
@Country AS NCHAR(2)
)
AS
SELECT
Id,Name,LastPurchaseDate
FROM
Marketing.Customers
WHERE
Country = @Country;
GO
这里是一个“Country”列的分布情况:
在讨论可行的解决方案之前,先看一下问题…
首先参数赋值为IL。当存储过程首次用“IL”参数执行时,生成计划包含了一个寻找“Country”的索引。对于这个指定的执行这是很有帮助的优化器估计行数是72,完全准确。
下次存储过程执行时,使用参数为“US”。数据中有40,101行,并且这种情况下的最佳执行计划是使用聚集索引扫描,可以避免很多“key lookups”。但是计划已经在内存中,就会重用。不幸的是,这个计划包含了索引查找和“key lookup ”而不是聚集索引扫描,这就是一个非常差的执行计划。此时我们看到索引查找操作符的属性中估计行数是72,然后实际却是40000+。这就是执行计划错误引起的估计行数错误。如果我们查看SELECT 的“Parameter List” 属性,就能发现原因所在。由于编译1是“IL”,而运行时是“US”。
那么现在我们发现了问题,接下来让我们看一下可能的解决方案… Solution #1 – sys.sp_recompile
很简单就是使用系统存储过程sys.sp_recompile从缓存中移除指定的执行计划或者所有计划引用的指定表和视图。这就是说下次存储过程再次执行时需要重新编译,新的执行计划将被创建。
记住我们的主要问题是值的分布。因此基于一套新的参数重新编译存储过程将创建指定的执行计划,但是大多数时候这并不解决问题,因为新的计划仍然只针对本次的值是好的,当遇到其他不同分布的参数值时依然是不好的计划。我建议当查询中过滤的值绝大多数情况下是惟一值的时候可以考虑重新编译的方式来解决问题,比如当where后面的status 状态为1的占据99%的数据值时,一般情况就是好的计划。
Solution #2 – WITH RECOMPILE
如果你不喜欢前面这个赌博式的方法,那么WITH RECOMPILE很适合你。与之前依赖传递给指定执行的参数值不同,这种方式使你可以告诉优化器编译在每一个存储过程中编译计划。
Id,LastPurchaseDate
FROM
Marketing.Customers
WHERE
Country = @Country;
GO
每一次参数嗅探被使用时,意味着执行将得到优化器提供的最佳执行计划。既然新的计划每次执行都被创建,那么SQLServer将不会把计划放到缓存中。
这是一个不错的解决方案,因为每次执行存储过程都产生一个最佳的计划,消除了随机赌博式的副作用。但是缺点是每次编译都必须经过昂贵的优化过程。这是需要密集的CPU处理过程。如果系统已经处在PCU高负载并且存储过程频繁执行,那么这种方式是不合适的。另一方面,如果CPU使用率相对较低并且存储过程只是偶尔执行,那么这就是一个带给你最佳的解决方案。
Solution #3 – OPTION (RECOMPILE)
是一个与前者相似的解决方案,但是也有两个重要的不同点。首先,这个查询参数针对有问题的查询语句而不是整个存储过程。
Id,LastPurchaseDate
FROM
Marketing.Customers
WHERE
Country = @Country
OPTION
(RECOMPILE);
GO
只对一个语句的重编译节省了大量的资源。
其次,“WITH RECOMPILE”发生在编译时,而“OPTION (RECOMPILE)” 发生在运行时。整个例子中运行时执行这个语句时,暂停执行,重新编译该查询,生成新的执行计划。而其他部分则使用计划缓存。运行时编译带来的好处就是使优化器能预先知道所有的运行时值,甚至不需要参数嗅探。优化器知道参数的值,局部变量和环境设置,然后使用这些数据编译查询。多数情况下,运行时编译生成的计划要比编译时生成的计划好很多。
因此,你应该考虑使用“OPTION (RECOMPILE)” 而不是“WITH RECOMPILE”,因为它使用了更少的资源长生了更好的计划。但是要注意这种方式依然是十分占用CPU的。
Solution #4 – OPTIMIZE FOR
另一查询选项“OPTIMIZE FOR”也可以解决参数嗅探问题。该选项指示优化器使用特定的一套参数而不是实际的参数来编译查询。实际上就是重写参数嗅探。注意,这个选项只有当查询必须被重编译的时候才能被使用。选项本身不会引起重编译。
Id,LastPurchaseDate
FROM
Marketing.Customers
WHERE
Country = @Country
OPTION
(OPTIMIZE FOR (@Country = N’US’));
GO