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

相關文章
相關標籤/搜索