在上篇文章中咱们谈到了查询优化器和执行计划缓存的关系,以及其两者之间的冲突。本篇文章中,咱们会主要阐述执行计划缓存常见的问题以及一些解决办法。sql
上篇文章中提到了查询优化器解析语句的过程,当将计划缓存考虑在内时,首先须要查看计划缓存中是否已经有语句的缓存,若是没有,才会执行编译过程,若是存在则直接利用编译好的执行计划。所以,完整的过程如图1所示。数据库
图1.将计划缓存考虑在内的过程缓存
图1中咱们能够看到,其中有一步须要在缓存中找到计划的过程。所以不难猜出,只要是这一类查找,必定跑不了散列(Hash)的数据结构。经过sys.dm_os_memory_cache_hash_tables这个DMV能够找到有关该Hash表的一些信息,如图2所示。这里值得注意的是,当执行计划过多致使散列后的对象在同一个Bucket过多时,则须要额外的Bucket,所以可能会致使查找计划缓存效率低下。解决办法是尽可能减小在计划缓存中的计划个数,咱们会在本文后面讨论到。数据结构
图2.有关存储计划缓存的HashTable的相关信息app
当出现这类问题时,咱们能够在buckets_avg_scan_miss_length列看出问题。这类状况在缓存命中率(SQL Server: Plan Cache-Cache Hit Ratio)比较高,但编译时间过长时能够做为考虑对象。ide
查询计划的惟一标识是查询语句自己,但假设语句的主体同样,而仅仅是查询条件谓词不同,那在执行计划中算1个执行计划仍是两个执行计划呢?It’s Depends。性能
假设下面两个语句,如图3所示。优化
图3.仅仅谓词条件不同的两个语句ui
虽然执行计划同样,可是在执行计划缓存中却会保留两份执行计划,如图4所示。spa
图4.同一个语句,不一样条件,有两份不一样的执行计划缓存
咱们知道,执行计划缓存依靠查询语句自己来判别缓存,所以上面两个语句在执行计划缓存中就被视为两个不一样的语句。那么解决该问题的手段就是使得执行计划缓存中的查询语句如出一辙。
参数化
使得仅仅是某些参数不一样,而查询自己相同的语句能够复用,就是参数化的意义所在。好比说图3中的语句,若是咱们启用了数据库的强制参数化,或是使用存储过程等。SQL Server会将这些语句强制参数话,好比说咱们根据图5修改了数据库层级的选项。
图5.数据库层级的选项
此时咱们再来执行图3中的两条语句,经过查询执行计划缓存,咱们发现变量部分被参数化了,从而在计划缓存中的语句变得一致,如图6所示,从而能够复用.
图6.参数话以后的查询语句
可是,强制参数会引发一些问题,查询优化器不少时候就没法根据统计信息最优化一些具体的查询,好比说不能应用一些索引或者该扫描的时候却查找。所产生的负面影响在上篇文章中已经说过,这里就不细说了。
所以对于上面的问题能够有几种解决办法。
在具体的状况下,参数化有些时候是好的,但有些时候倒是性能问题的罪魁祸首,下面咱们来看几种平衡这二者之间关系的手段。
使用RECOMPILE
当查询中,不许确的执行计划的成本要高于编译的成本时,在存储过程当中使用RECOMPILE选项或是在即席查询中使用RECOMPILE提示使得每次查询都会从新生成执行计划,该参数会使得生成的执行计划不会被插入到执行计划缓存中。对于OLAP类查询来讲,不许确的执行计划所耗费的成本每每高于编译成本太多,因此能够考虑该参数或选项,您能够如代码清单1中的查询所示这样使用Hint。
SELECT * FROM Sales.Customer
WHERE CustomerID>20000 AND TerritoryID = 4
OPTION (recompile)
代码清单1.使用Recompile
除去咱们能够手动提示SQL Server重编译以外,SQL Server也会在下列条件下自动重编译:
使用Optimize For参数
RECOMPILE方式提供了彻底不使用计划缓存的节奏。但有些时候,特性谓语的执行计划被使用的次数h更多,好比说,仅仅那些谓语条件产生大量返回结果集的参数编译,咱们能够考虑Optimize For参数。好比咱们来看代码清单2。
DECLARE @vari INT
SET @vari=4
SELECT * FROM Sales.Customer
WHERE CustomerID>20000 AND TerritoryID = @vari
OPTION (OPTIMIZE FOR (@vari=4))
代码清单2.使用OPTIMIZE FOR提示
使用了该参数会使得缓存的执行计划按照OPTIMIZE FOR后面的谓语条件来生成并缓存执行计划,这也可能形成不在该参数中的查询效率低下,可是该参数是咱们选择的,所以一般咱们知道哪些谓语条件会被使用的多一些。
另外,自SQL Server 2008开始多了一个OPTIMIZE FOR UNKNOWN参数,这使得在优化查询的过程当中探测做为谓语条件的局部参数的值,而不是根据局部变量的初始值去探测统计信息。
在存储过程当中使用局部变量代替存储过程参数
在存储过程当中不使用过程参数,而是使用局部变量至关于直接禁用参数嗅探。毕竟,局部变量的值只有在运行时才能知道,在执行计划被查询优化器编译时是没法知道该值的,所以强迫查询分析器使用条件列的平均值进行估计。
虽然这种方式使得参数估计变得很是不许确,可是会变得很是稳定,毕竟统计信息不会变动的过于频繁。该方式不被推荐,若是可能,尽可能使用Optimizer的方式。
代码清单3展现了这种方式。
CREATE PROC TestForLocalVari
@v INT
AS
DECLARE @vari INT
SET @vari=@v
SELECT * FROM Sales.Customer
WHERE CustomerID>20000 AND TerritoryID = @vari
代码清单3.直接引用局部变量,而不是存储过程参数
强制参数化
在本篇文章的前面已经提到过了强制参数化,这里就再也不提了。
使用计划指导
在某些状况下,咱们的环境不容许咱们直接修改SQL语句,好比所不但愿破坏代码的逻辑性或是应用程序是第三方开发,所以不管是加HINT或参数都变得不现实。此时咱们可使用计划指导。
计划指导使得查询语句在由客户端应用程序扔到SQL Server的时候,SQL Server对其加上提示或选项,好比说经过代码清单4能够看到一个计划指导的例子。
EXEC sp_create_plan_guide N'MyPlanGuide1',
@stmt=N'SELECT * FROM Sales.Customer WHERE CustomerID>20000 AND TerritoryID=@vari',
@type=N'sql',
@module_or_batch=NULL,
@params=N'@vari int',
@hints=N'OPTION (RECOMPILE)'
代码清单4.对咱们前面的查询设置计划指导
当加入了计划指导后,当批处理到达SQL Server时,在查找匹配的计划缓存时也会去找是否有计划指导和其相匹配。若是匹配,则应用计划指导中的提示或选项。这里要注意的是,这里@stmt参数必须和查询语句中的一句如出一辙,差一个空格都会被认为不匹配。
PARAMETERIZATION SIMPLE
当咱们在数据库层级启用了强制参数化时,对于特定语句,咱们却不想启用强制参数化,咱们可使用PARAMETERIZATION SIMPLE选项,如代码清单5所示。
DECLARE @stmt NVARCHAR(MAX)
DECLARE @params NVARCHAR(MAX)
EXEC sp_get_query_template N'SELECT * FROM Sales.Customer WHERE CustomerID>20000 AND TerritoryID=2',
@stmt OUTPUT, @params OUTPUT
PRINT @stmt
PRINT @params
EXEC sp_create_plan_guide N'MyTemplatePlanGuide', @stmt, N'TEMPLATE', NULL,
@params, N'OPTION(PARAMETERIZATION SIMPLE)'
代码清单5.经过计划指南对单条语句应用简单参数化
执行计划缓存但愿尽可能重用执行计划,这会减小编译所消耗的CPU和执行缓存所消耗的内存。而查询优化器但愿尽可能生成更精准的执行计划,这势必会形成大量的执行计划,这不只仅可能引发重编译大量消耗CPU,还会形成内存压力,甚至当执行计划缓存过多超过BUCKET的限制时,在缓存中匹配执行计划的步骤也会消耗更多的时间。
所以利用本篇文章中所述的方法基于实际的状况平衡二者之间的关系,就变得很是重要。