SQL Server如何固定执行计划

   SQL Server 其实从SQL Server 2005开始,也提供了相似ORACLE中固定执行计划的功能,只是好像不多人使用这个功能。固然在SQL Server中不叫"固定执行计划"这个概念,而是叫"执行计划指南"(Plan Guide 不少翻译是计划指南,我的以为执行计划指南稍好一些)。固然二者虽然概念与命名不一样,实质上它们所说的是相同的事情,固然商业包装是很常见的事情。我的仍是以为“固定执行计划”这个概念叫起来顺口,通俗易懂,执行计划指南(Plan Guide)叫起来老感受很是拗口,不知所云(后面会在这两个概念切换,你知道我所说的是一件事情就好)。其实我之前也不多使用这些功能,直到最近在SQL Server 2014数据库中使用固定执行计划解决了几个SQL的性能问题,因此以为仍是有必要总结、概括一下。 算法

 

为何要固定执行计划? sql

 

为何要使用固定执行计划(Plan Guid)呢? 我的简单的从下面几个方面介绍一下,若有不足,敬请指正。我的也是在探索当中。 数据库

 

因为一些特殊缘由(例如Parameter Sniffing、统计信息的变化或采样比例低形成的统计信息出现误差、或其余像SQL Server 2014新的基数评估(Cardinality Estimator)特性引发优化器选择不合适的JOIN操做等等),致使某个SQL的执行计划出现很大误差,当数据库优化器为SQL选择了一个糟糕的执行计划时,就可能出现严重性能问题,我就碰到过这样一个例子,在SQL Server 2014中,有一个SQL的执行频率较频繁,有时候优化器忽然选择了一个较差的执行计划时,这时就会出现严重的性能问题。因此,这个时候,咱们就必须使用Plan Guide固定这个执行计划,从而让优化器使用正确的执行计划,从而解决这样的性能问题。 缓存

 

另一方面,由于优化器生成执行计划自己是很复杂的过程,咱们所能干涉的很少,最多使用HINT提示来改变执行计划。并且优化器基于一些算法和开销考虑,也有可能生成的执行计划不是最优执行计划,而Plan Guid是DBA管理数据库的一件利器,若是你发现了一个比当前更好的执行计划,也能使用执行计划指南固定这个SQL的执行计划。固然这种状况很是、很是少,至少我在生产环境使用得很少。 app

 

有时候,某个系统是购买供应商的,你发现数据库里面有大量几乎相同的SQL解析,而后缓存了,其实你发现这些SQL彻底能够只解析一次,彻底能够参数化,没有必要大量解析。可是如今供应商没有提供技术支持了,不可能去优化代码里面的SQL语句,那么你也可使用执行计划指南来帮你解决这个问题。 运维

 

还有就是使用Plan Guide来调优,对比不一样的执行计划的优劣。固然应该还有一些其它应用场景,只是我没有碰到过而已。 ide

 

如何固定执行计划? oop

 

Plan Guide主要用到下面几个存储,关于这些系统存储过程的使用方法、功能介绍,官方文档有详细的介绍。在此就不多此一举了。 性能

sys.sp_create_plan_guide, 测试

sys.sp_create_plan_guide_from_handle,

sys.sp_control_plan_guide

下面咱们仍是看看一些应用场景案例吧!构造一个合适、贴切的例子实在是太花精力和时间,生产环境案例又不能搬出来,咱们先来看看官方文档提供的例子吧,以下SQL所示,在测试数据库AdventureWorks2014,该SQL使用Nested Loop关联两个表

SELECT COUNT(*) AS c
FROM Sales.SalesOrderHeader AS h
INNER JOIN Sales.SalesOrderDetail AS d
  ON h.SalesOrderID = d.SalesOrderID
WHERE h.OrderDate >= '20000101' AND h.OrderDate <='20050101';

clipboard

 

假如(注意这里是假设)发现若是这个SQL中,两个表使用MERGE JOIN的方式,效率更高,那么咱们可使用sp_create_plan_guide来建立执行计划指南(固定执行计划),以下所示

EXEC sp_create_plan_guide 
    @name = N'my_table_jon_guid',
    @stmt = N'SELECT COUNT(*) AS c
FROM Sales.SalesOrderHeader AS h
INNER JOIN Sales.SalesOrderDetail AS d
  ON h.SalesOrderID = d.SalesOrderID
WHERE h.OrderDate >= ''20000101'' AND h.OrderDate <=''20050101'';',
    @type = N'SQL',
    @module_or_batch = NULL,
    @params = NULL,
    @hints = N'OPTION (MERGE JOIN)';

 

那么此时再执行这个SQL时,你就会发现执行计划就会变成Merge Join方式了。 这样好过在SQL Server中使用HINT,为何呢? 有可能这个SQL是写死在应用程序里面,若是之后这个执行计划变成了一个糟糕的执行计划,维护的成本很是高(一方面若是没有记录,须要耗费精力去定位、查找这段SQL,另一方面,DBA是没有权限接触这些应用程序代码的,可能须要你沟通、协调开发人员、运维人员。耗费无数的时间、精力.....,还有可能其余接手维护的人不了解状况等等),而使用执行计划指南,那么你查找、禁用、删除这个执行计划指南便可。很是方便、高效,也许你一分钟就能搞定,若是是Hint,说不定处理完,须要几天,想必这样的耗费精力沟通、协调的事情不少人都遇到过。

SELECT COUNT(*) AS c
FROM Sales.SalesOrderHeader AS h
INNER MERGE JOIN Sales.SalesOrderDetail AS d
  ON h.SalesOrderID = d.SalesOrderID
WHERE h.OrderDate >= '20000101' AND h.OrderDate <='20050101';

clipboard

 

另外,咱们再来构造一个例子,模拟系统里面出现大量解析的SQL语句的案例,以下所示

USE AdventureWorks2014;
GO
SET NOCOUNT ON;
GO
DROP TABLE TEST
GO
CREATE TABLE TEST (OBJECT_ID  INT, NAME VARCHAR(8));
GO
CREATE INDEX PK_TEST ON TEST(OBJECT_ID);
GO
 
DECLARE @Index INT =1;
 
WHILE @Index <= 10000
BEGIN
    INSERT INTO TEST
    SELECT @Index, 'kerry';
   
    SET @Index = @Index +1;
END
GO
UPDATE STATISTICS  TEST WITH FULLSCAN;
GO

 

构造了上面案例后,咱们清空该数据库全部缓存的执行计划(仅仅是为了干净的测试环境,避免之前缓存的执行计划影响实验结果),生产环境你不能使用DBCC FREEPROCCACHE清空全部缓存的执行计划,可是能够用DBCC FREEPROCCACHE删除特定的执行计划。

DBCC FREEPROCCACHE;

GO

而后咱们开始测试咱们的例子,假设系统里面有大量相似的SQL语句,数量惊人(咱们仅仅测试四个)。若是这个系统是从供应商那里购买的,如今又没有技术支持和Support的人(或者及时有人Support,可是不严重影响使用的状况,人家不想花费精力去优化),没有人协助你优化这些SQL,你又不能将数据库参数“参数化”从简单设置为强制(由于影响太大,并且没有测试,不肯定是否带来潜在的性能问题).....

SELECT * FROM TEST WHERE OBJECT_ID=1;
GO
SELECT * FROM TEST WHERE OBJECT_ID=2;
GO
SELECT * FROM TEST WHERE OBJECT_ID=3;
GO
SELECT * FROM TEST WHERE OBJECT_ID=4;
GO
....................................................................

 

此时查看执行计划,发现缓存了4个执行计划

SELECT qs.sql_handle,
       qs.statement_start_offset,
       qs.statement_end_offset,
       qs.plan_handle,
       qs.creation_time,
       qs.execution_count,
       qs.query_hash,
       qs.query_plan_hash,
       st.text,
       qp.query_plan
FROM sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_sql_text(sql_handle) AS st
CROSS APPLY sys.dm_exec_text_query_plan(qs.plan_handle, qs.statement_start_offset, qs.statement_end_offset) AS qp
WHERE text LIKE N'%SELECT * FROM TEST WHERE OBJECT_ID%' AND text NOT LIKE 'SELECT qs.sql_handle%';

clipboard

 

那么此时,执行计划指南就能发挥其做用了,使用sp_create_plan_guide建立执行计划指南,强制SELECT * FROM TEST WHERE OBJECT_ID=xxx这样的SQL参数化

DECLARE @stmt nvarchar(max);
DECLARE @params nvarchar(max);
EXEC sp_get_query_template N'SELECT * FROM TEST WHERE OBJECT_ID=1',
@stmt OUTPUT, 
@params OUTPUT;
 
EXEC sp_create_plan_guide N'my_sql_parameter_test', 
    @stmt, 
N'TEMPLATE', 
NULL, 
@params, 
N'OPTION(PARAMETERIZATION FORCED)';

 

 

而后咱们执行下面命令,清空该数据库全部缓存的执行计划,而后执行上面四个SQL语句

DBCC FREEPROCCACHE;
 
GO
 
SELECT * FROM TEST WHERE OBJECT_ID=1;
 
SELECT * FROM TEST WHERE OBJECT_ID=2;
 
SELECT * FROM TEST WHERE OBJECT_ID=3;
 
SELECT * FROM TEST WHERE OBJECT_ID=4;

 

 

你会发现他们所有使用执行计划指南里面的执行计划了。不用屡次解析了。

clipboard

 

仍是使用上面的例子,咱们来解决一个Parameter Sniffing(参数嗅探)的问题,在实验前,咱们先删除前面建立的Plan Guide,以避免这个影响测试结果,

EXEC sp_control_plan_guide @operation=N'DROP', @name=N'my_sql_parameter_test';

 

咱们构造一个数据倾斜的案例,这样方便咱们演示

 
UPDATE dbo.TEST SET OBJECT_ID =1 WHERE OBJECT_ID <=2000;
 
UPDATE STATISTICS dbo.TEST WITH FULLSCAN;

 

而后咱们建立一个简单的存储过程Proc_Parameter_Sniffing

CREATE PROCEDURE Proc_Parameter_Sniffing
( @Object_ID  INT)
AS 
BEGIN
    SELECT * FROM TEST WHERE OBJECT_ID=@Object_ID;
END
GO

 

接下来,咱们清空缓存的执行计划,而后执行存储过程,参数为1

DBCC FREEPROCCACHE;
 
GO
 
EXEC Proc_Parameter_Sniffer 1;

而后咱们查看这个存储过程的实际执行计划,以下所示,将Query_Plan这些XML拷贝出来并格式化

clipboard

  
  
  
  
1 < Batch > 2 < Statements > 3 < StmtSimple StatementText ="SELECT * FROM TEST WHERE OBJECT_ID=@Object_ID" StatementId ="1" StatementCompId ="3" StatementType ="SELECT" RetrievedFromCache ="true" StatementSubTreeCost ="0.0350227" StatementEstRows ="2000" StatementOptmLevel ="FULL" QueryHash ="0xA99C3EB3A64627F3" QueryPlanHash ="0x50042F73B31C8535" StatementOptmEarlyAbortReason ="GoodEnoughPlanFound" CardinalityEstimationModelVersion ="120" > 4 < StatementSetOptions QUOTED_IDENTIFIER ="true" ARITHABORT ="true" CONCAT_NULL_YIELDS_NULL ="true" ANSI_NULLS ="true" ANSI_PADDING ="true" ANSI_WARNINGS ="true" NUMERIC_ROUNDABORT ="false" /> 5 < QueryPlan CachedPlanSize ="16" CompileTime ="0" CompileCPU ="0" CompileMemory ="152" > 6 < MemoryGrantInfo SerialRequiredMemory ="0" SerialDesiredMemory ="0" /> 7 < OptimizerHardwareDependentProperties EstimatedAvailableMemoryGrant ="209715" EstimatedPagesCached ="26214" EstimatedAvailableDegreeOfParallelism ="2" MaxCompileMemory ="3112816" /> 8 < RelOp NodeId ="0" PhysicalOp ="Table Scan" LogicalOp ="Table Scan" EstimateRows ="2000" EstimateIO ="0.0238657" EstimateCPU ="0.011157" AvgRowSize ="19" EstimatedTotalSubtreeCost ="0.0350227" TableCardinality ="10000" Parallel ="0" EstimateRebinds ="0" EstimateRewinds ="0" EstimatedExecutionMode ="Row" > 9 < OutputList > 10 < ColumnReference Database ="[AdventureWorks2014]" Schema ="[dbo]" Table ="[TEST]" Column ="OBJECT_ID" /> 11 < ColumnReference Database ="[AdventureWorks2014]" Schema ="[dbo]" Table ="[TEST]" Column ="NAME" /> 12 </ OutputList > 13 < TableScan Ordered ="0" ForcedIndex ="0" ForceScan ="0" NoExpandHint ="0" Storage ="RowStore" > 14 < DefinedValues > 15 < DefinedValue > 16 < ColumnReference Database ="[AdventureWorks2014]" Schema ="[dbo]" Table ="[TEST]" Column ="OBJECT_ID" /> 17 </ DefinedValue > 18 < DefinedValue > 19 < ColumnReference Database ="[AdventureWorks2014]" Schema ="[dbo]" Table ="[TEST]" Column ="NAME" /> 20 </ DefinedValue > 21 </ DefinedValues > 22 < Object Database ="[AdventureWorks2014]" Schema ="[dbo]" Table ="[TEST]" IndexKind ="Heap" Storage ="RowStore" /> 23 < Predicate > 24 < ScalarOperator ScalarString ="[AdventureWorks2014].[dbo].[TEST].[OBJECT_ID]=[@Object_ID]" > 25 < Compare CompareOp ="EQ" > 26 < ScalarOperator > 27 < Identifier > 28 < ColumnReference Database ="[AdventureWorks2014]" Schema ="[dbo]" Table ="[TEST]" Column ="OBJECT_ID" /> 29 </ Identifier > 30 </ ScalarOperator > 31 < ScalarOperator > 32 < Identifier > 33 < ColumnReference Column ="@Object_ID" /> 34 </ Identifier > 35 </ ScalarOperator > 36 </ Compare > 37 </ ScalarOperator > 38 </ Predicate > 39 </ TableScan > 40 </ RelOp > 41 < ParameterList > 42 < ColumnReference Column ="@Object_ID" ParameterCompiledValue ="(1)" /> 43 </ ParameterList > 44 </ QueryPlan > 45 </ StmtSimple > 46 </ Statements > 47 </ Batch > 48 </ BatchSequence > 49 </ ShowPlanXML >

clipboard[1]

 

以下所示,目前它确实是使用准确的执行计划,进行全表扫描(TableScan),若是此时使用其它参数(例以下面SQL),就会出现Parameter Sniffer(参数嗅探)问题,这个是由于SQL Server在处理存储过程的时候,是一次编译,屡次重用,执行计划重用。因此当参数为2500的时候,执行计划依然是进行全表扫描(TableScan),这个时候,全表扫描显然是一个糟糕的执行计划。

EXEC Proc_Parameter_Sniffer 2001;

并且,大部分数据应该作Index Seek是一个较优的执行计划,只有Object_ID=1这样的特殊数据,所有扫描才是一个较优的执行计划,假如实际使用环境中,也不多用到Object_ID=1这样的查询,那么咱们能够固定执行计划,让其使用参数2001的执行计划

EXEC sp_create_plan_guide 
    @name = N'parameter_sniffing_guid',
    @stmt = N'SELECT * FROM TEST WHERE OBJECT_ID=@Object_ID',
    @type = N'OBJECT',
    @module_or_batch =N'Proc_Parameter_Sniffing',
    @params = NULL,
    @hints = N'OPTION(optimize for(@Object_ID=2001))';

 

而后咱们再次调用EXEC Proc_Parameter_Sniffer 1;时,你会发现该SQL的执行计划变动为索引查找了。

clipboard[2]

 

固然实际生产环境中,状况每每比较复杂,毫不可能有这么简单、理想的环境出现,每每还须要根据实际状况、权衡利弊,多方考虑才能指定一个折中的方案。具体问题具体分析、不能依葫芦画瓢。理论要结合实际状况。

 

 

查看执行计划指南

 

查看执行计划指南很是信息很是简单,你只须要查询sys.plan_guides便可。

SELECT * FROM sys.plan_guides;

另外,启用、禁用、删除执行计划指南都是经过一个系统存储过程sys.sp_control_plan_guide来实现的,使用很是简单。下面仅仅简单举几个例子。sys.sp_control_plan_guide的存储过程以下,实际上它都是封装调用了sys.sp_control_plan_guide_int的功能

SET QUOTED_IDENTIFIER ON
SET ANSI_NULLS ON
GO
create procedure sys.sp_control_plan_guide
    @operation nvarchar(60),
    @name sysname = NULL
as
BEGIN TRANSACTION
 
declare @return_code int
 
if( lower(@operation) = 'drop' OR lower(@operation) = 'enable' OR lower(@operation) = 'disable')
    exec @return_code =  @operation, @name
else
    exec @return_code = sys.sp_control_plan_guide_int @operation
 
 
if( @return_code = 0 )
begin
    if( lower(@operation) = 'drop' OR lower(@operation) = 'drop all')
    begin
    EXEC %%System().FireTrigger(ID = 238, ID = 27, ID = 0, ID = 0, Value = @name,
            ID = -1, ID = 0, ID = 0, Value = NULL, ID = 2,
            Value = @operation, Value = @name, Value = NULL, Value = NULL, Value = NULL, Value = NULL, Value = NULL)
    end
    else
    begin
    EXEC %%System().FireTrigger(ID = 216, ID = 27, ID = 0, ID = 0, Value = @name,
            ID = -1, ID = 0, ID = 0, Value = NULL, ID = 2,
            Value = @operation, Value = @name, Value = NULL, Value = NULL, Value = NULL, Value = NULL, Value = NULL)
    end
end
 
COMMIT TRANSACTION
 
 
GO

 

禁用执行计划指南

 

1:禁用名字为my_sql_plan_test的执行计划指南

 

USE AdventureWorks2014;
GO
EXEC sp_control_plan_guide @operation=N'DISABLE', @name=N'my_sql_plan_test'

 

2:禁用全部的执行计划指南

USE AdventureWorks2014;
GO
EXEC sys.sp_control_plan_guide @operation = N'DISABLE ALL';

确切的说,应该是禁用数据库AdventureWorks2014下全部的执行计划指南。

 

启用执行计划指南

 

1:启用名字为my_sql_plan_test的执行计划指南

USE AdventureWorks2014;
 
GO
 
EXEC sp_control_plan_guide @operation=N'ENABLE', @name=N'my_sql_plan_test';

 

2:启用全部的执行计划指南

USE AdventureWorks2014;
 
GO
 
EXEC sys.sp_control_plan_guide @operation = N'ENABLE ALL';

确切的说,应该是启用数据库AdventureWorks2014下全部被禁用的执行计划指南。

 

删除执行计划指南

 

删除执行计划指南很是简单,以下所示

咱们首先查看有执行计划指南,找到想要删除的Plan Guide,例如,咱们想删除命名为my_sql_plan_test的执行计划指南。

EXEC sp_control_plan_guide @operation=N'DROP', @name=N'my_sql_plan_test';

 

参考资料:

https://technet.microsoft.com/zh-cn/library/ms188255(v=sql.105).aspx

https://technet.microsoft.com/zh-cn/library/bb964726(v=sql.105).aspx

https://msdn.microsoft.com/zh-cn/library/ms179880.aspx

相关文章
相关标签/搜索