SQL Server 其實從SQL Server 2005開始,也提供了類似ORACLE中固定執行計劃的功能,只是好像很少人使用這個功能。當然在SQL Server中不叫"固定執行計劃"這個概念,而是叫"執行計劃指南"(Plan Guide 很多翻譯是計劃指南,個人覺得執行計劃指南稍好一些)。當然兩... ...
SQL Server 其實從SQL Server 2005開始,也提供了類似ORACLE中固定執行計劃的功能,只是好像很少人使用這個功能。當然在SQL Server中不叫"固定執行計劃"這個概念,而是叫"執行計劃指南"(Plan Guide 很多翻譯是計劃指南,個人覺得執行計劃指南稍好一些)。當然兩者雖然概念與命名不同,實質上它們所說的是相同的事情,當然商業包裝是很常見的事情。個人還是覺得“固定執行計劃”這個概念叫起來順口,通俗易懂,執行計劃指南(Plan Guide)叫起來老感覺非常拗口,不知所云(後面會在這兩個概念切換,你知道我所說的是一件事情就好)。其實我以前也很少使用這些功能,直到最近在SQL Server 2014資料庫中使用固定執行計劃解決了幾個SQL的性能問題,所以覺得還是有必要總結、歸納一下。
為什麼要固定執行計劃?
為什麼要使用固定執行計劃(Plan Guid)呢? 個人簡單的從下麵幾個方面介紹一下,如有不足,敬請指正。個人也是在探索當中。
由於一些特殊原因(例如Parameter Sniffing、統計信息的變化或採樣比例低造成的統計信息出現偏差、或其他像SQL Server 2014新的基數評估(Cardinality Estimator)特性引起優化器選擇不合適的JOIN操作等等),導致某個SQL的執行計划出現很大偏差,當資料庫優化器為SQL選擇了一個糟糕的執行計劃時,就可能出現嚴重性能問題,我就碰到過這樣一個例子,在SQL Server 2014中,有一個SQL的執行頻率較頻繁,有時候優化器突然選擇了一個較差的執行計劃時,這時就會出現嚴重的性能問題。所以,這個時候,我們就必須使用Plan Guide固定這個執行計劃,從而讓優化器使用正確的執行計劃,從而解決這樣的性能問題。
另外一方面,因為優化器生成執行計劃本身是很複雜的過程,我們所能幹涉的不多,最多使用HINT提示來改變執行計劃。而且優化器基於一些演算法和開銷考慮,也有可能生成的執行計劃不是最優執行計劃,而Plan Guid是DBA管理資料庫的一件利器,如果你發現了一個比當前更好的執行計劃,也能使用執行計劃指南固定這個SQL的執行計劃。當然這種情況非常、非常少,至少我在生產環境使用得不多。
有時候,某個系統是購買供應商的,你發現資料庫裡面有大量幾乎相同的SQL解析,然後緩存了,其實你發現這些SQL完全可以只解析一次,完全可以參數化,沒有必要大量解析。但是現在供應商沒有提供技術支持了,不可能去優化代碼裡面的SQL語句,那麼你也可以使用執行計劃指南來幫你解決這個問題。
還有就是使用Plan Guide來調優,對比不同的執行計劃的優劣。當然應該還有一些其它應用場景,只是我沒有碰到過而已。
如何固定執行計劃?
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';
假如(註意這裡是假設)發現如果這個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';
另外,我們再來構造一個例子,模擬系統裡面出現大量解析的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%';
那麼此時,執行計劃指南就能發揮其作用了,使用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;
你會發現他們全部使用執行計劃指南裡面的執行計划了。不用多次解析了。
還是使用上面的例子,我們來解決一個Parameter Sniffing(參數嗅探)的問題,在實驗前,我們先刪除前面創建的Plan Guide,以免這個影響測試結果,
EXEC sp_control_plan_guide @operation=N'DROP', @name=N'my_sql_parameter_test';
我們構造一個數據傾斜的案例,這樣方便我們演示
UPDATE