本文出處:http://www.cnblogs.com/wy123/p/6262800.html 在考慮重編譯T-SQL(或者存儲過程)的時候,有兩種方式可以實現強制重編譯(前提是忽略導致重編譯的其他因素的情況下,比如重建索引,更新統計信息等等), 一是基於WITH RECOMPILE的存儲過程級別 ...
本文出處:http://www.cnblogs.com/wy123/p/6262800.html
在考慮重編譯T-SQL(或者存儲過程)的時候,有兩種方式可以實現強制重編譯(前提是忽略導致重編譯的其他因素的情況下,比如重建索引,更新統計信息等等),
一是基於WITH RECOMPILE的存儲過程級別重編譯,另外一種是基於OPTION(RECOMPILE)的語句級重編譯。
之前瞭解的比較淺,僅僅認為是前者就是編譯整個存儲過程中的所有的語句,後者是重編譯存儲過程中的某一個語句,也沒有追究到底是不是僅僅只有這麼一點區別。
事實上在某些特定情況下,兩者的區別並非僅僅是存儲過程級重編譯和語句級重編譯的區別,
從編譯生成的執行計劃來看,這兩種強制編譯的方式內在機制差異還是比較大的。
這裡同時引申出來另外一個問題:The Parameter Embedding Optimization(怎麼翻譯?也沒有中文資料中提到The Parameter Embedding Optimization,勉強翻譯為“參數植入優化”)
本文通過一個簡單的示例來說明這兩者的區別(測試環境為SQL Server2014)。這裡首先感謝UEST同學提供的參考資料和指導建議。
WITH RECOMPILE 和 OPTION(RECOMPILE)使用上的區別
關於存儲過程級別的重編譯,典型用法如下,在存儲過程參數之後指定“WITH RECOMPILE”
CREATE PROCEDURE TestRecompile_WithRecompile ( @p_parameter int )WITH RECOMPILE AS BEGIN SET NOCOUNT ON; SELECT * FROM TestRecompile WHERE Id = @p_parameter OR @p_parameter IS NULL END GO
關於語句級重編譯,典型用法如下,在某一條SQL語句的末尾指定OPTION(RECOMPILE)
CREATE PROCEDURE TestRecompile_OptionRecompile ( @p_parameter VARCHAR(50) ) AS BEGIN SET NOCOUNT ON; SELECT * FROM TestRecompile WHERE Id = @p_parameter OR @p_parameter IS NULL OPTION(RECOMPILE) END GO
按照慣例,先搭建一個測試環境
創建一張TestRecompile的表,也即上面存儲過程中用到的表,插入100W行數據,Id欄位上建立一個名字為idx_id的索引
CREATE TABLE TestRecompile ( Id int, Value varchar(50) ) GO DECLARE @i int = 0 WHILE @i<=1000000 BEGIN INSERT INTO TestRecompile VALUES (@i,NEWID()) SET @i = @i+1 END CREATE INDEX idx_Id ON TestRecompile(Id) GO
WITH RECOMPILE 和 OPTION(RECOMPILE)使用時重編譯生成的執行計劃的異同
如果說With Recompile存儲過程級的重編譯和Option Recompile的SQL語句級的重編譯效果是一樣的話,
由上面的存儲過程可知,存儲過程中僅僅只有一句SQL代碼,那麼存儲過程級別的重編譯和SQL語句級別的重編譯都是編譯這一句SQL
如果這樣的話,兩者在輸入同樣參數的情況下執行計劃也應該是一樣的,那麼到底一樣不一樣呢?
首先來看TestRecompile_WithRecompile這個存儲過程的執行計劃,可以看到是一個索引掃描(INDEX SCAN)
然後再來看TestRecompile_OptionRecompile的執行計劃,帶入同樣的參數
至此,可以看出,雖然都用到索引,很明顯第一個語句是索引掃描(INDEX SCAN),第二個語句是索引查找(INDEX SEEK)
可以證明:在存儲過程級指定 WITH RECOMPILE 強制重編譯 和SQL語句級指定的OPTION(RECOMPILE)強制重編譯,相同條件下生成的執行計劃是不一樣的。
為什麼WITH RECOMPILE強制重編譯 和 OPTION(RECOMPILE)強制重編譯得到的執行計劃是不一樣的?
WITH RECOMPILE強制重編譯是每次運行存儲過程,都根據當前的參數情況做一次重編譯,
首先我們暫時先不糾結為什麼第一種方法用不到索引查找(INDEX的SEEK)。
事實上正式因為使用了Id = @p_parameter OR @p_parameter IS NULL這種寫法導致的,具體我後面做解釋。
那麼對於OPTION(RECOMPILE)強制重編譯存儲過程中同樣寫法的SQL語句,為什麼有能用到索引了呢?
因為在用OPTION(RECOMPILE)強制重編譯的時候,這裡涉及到一個“Parameter Embedding Optimization”編譯問題,
事實上我之前也沒有聽說過這個名詞,直譯過來就是“參數植入編譯”(不知道恰不恰當)
OPTION(RECOMPILE)強制重編譯在一定程度上增強和優化重編譯的效果,
參考這裡:https://sqlperformance.com/2013/08/t-sql-queries/parameter-sniffing-embedding-and-the-recompile-options,文章中分析的極度牛逼,案例也非常精彩
原文中是這麼解釋的:
The Parameter Embedding Optimization takes this process a step further: query parameters are replaced with literal constant values during query parsing.
The parser is capable of surprisingly complex simplifications, and subsequent query optimization may refine things even further.
翻譯過來大概意思就是:
相比WITH RECOMPILE這種強制重編譯的方式,OPTION(RECOMPILE)中的Parameter Embedding Optimization機制更進一步,解析查詢的過程中,參數值被字面常量所替代
解析器神奇地把複雜的問題簡化。至於怎麼簡化了,還是強烈建議參考原文,演示的案例相當弔。
至於怎麼簡化,這裡大概做一下解釋,原文中的解釋更加詳細和有趣。
首先,SQL語句是這麼寫的:SELECT * FROM TestRecompile WHERE Id = @p_parameter OR @p_parameter IS NULL
當“植入參數”之後,也即上文中使用的@p_parameter = 123456,SQL語句變成了SELECT * FROM TestRecompile WHERE Id = 12345 OR 12345 IS NULL
因為OR 12345 IS NULL是永遠不成立的,甚至可以認為是將SQL語句直接簡化成了SELECT * FROM TestRecompile WHERE Id = 12345 ,這樣子的話,在當前情況下,肯定是可以用到索引的。
因此,OPTION(RECOMPILE)強制重編譯的SQL在編譯並且簡化之後,就變成瞭如下的SQL,這裡解釋還是感覺有點牽強的,沒有原文有說服力。
那麼再回頭看WITH RECOMPILE強制重編譯,WITH RECOMPILE強制重編譯的時候,沒有能夠做到OPTION(RECOMPILE)強制重編譯中的“解析器神奇地把複雜的問題簡化”
參考這個鏈接:http://sqlinthewild.co.za/index.php/2009/03/19/catch-all-queries/
對於類似WHERE Id = @p_parameter OR @p_parameter IS NULL這種查詢方式,
上述文章中是這麼解釋的:
The problem with these types of queries is that there is no stable plan.
The optimal plan differs completely depending on what paramters are passed.
The optimiser can tell that and it plays safe. It creates plans that will always work. That’s (one of the reasons) why in the first example it was an index scan, not an index seek.
翻譯過來大概意思就是:
這種類型的查詢問題在於沒有固定的執行計劃,
優化方案是基於具體傳入進來的參數值的,
優化器只能做到保證安全性(plays safe),他創建的執行計劃確保總是可以正常工作的。
我這裡補充解釋一下 it plays safe在我的理解:
如果@p_parameter 參數非空,走索引Seek完全沒有問題。
如果@p_parameter 為null,此時and (Id= @p_parameter or @p_parameter is null )這個條件恆成立,如果再走索引Seek會出現什麼結果?
如果繼續採用Index Seek的方式執行,語義上變成了是查找Id為null的值,這樣的話邏輯上已經錯誤了。
因此出現這種寫法,為了安全起見(上文所謂的plays safe),優化器只能選擇一個這種的索引的掃描的方案(所謂的always work的執行計劃)
關於OPTION(RECOMPILE)在SQL語句級重編譯神奇的魔力,他會根據具體的參數做到真正的重編譯,我們在做一個測試:
這一次設置@p_parameter = null,看看是不是又重新編譯了一個合理的執行計劃,沒錯,這次它生成了一個全表掃描的執行計劃,也是沒有問題的。
唯一有瑕疵的地方時,相對WITH RECOMPILE強制重編譯的方式,他的執行計劃沒有用到並行。這也是WITH RECOMPILE和OPTION(RECOMPILE)兩種強制重編譯生成執行計劃的區別
但是不能否認OPTION(RECOMPILE)強制重編譯中的Parameter Embedding Optimization這種優化機制的特性
而此時WITH RECOMPILE強制重編譯方式的執行計劃,在傳入參數值為null的時候,生成的是並行的執行計劃
現在來解釋為什麼非常強烈不建議寫這種SQL:SELECT * FROM TestRecompile WHERE Id = @p_parameter OR @p_parameter IS NULL
我在之前也寫過http://www.cnblogs.com/wy123/p/5958047.html,感覺沒有徹底解釋清楚索引抑制問題的原因。
開發中常見的一個潛在的多個條件的查詢SQL,具體的查詢條件是依賴於用戶輸入的,
比如提供給用戶三個查詢條件可選的查詢條件,用戶可以輸入一個,兩個或者三個,這個太常見了,也不用再解釋了
那麼我們就要構造出適應這種查詢的一種方案
面對這種catch-all-queries的查詢方式,其中方案之一就是類似於這種寫法
SELECT * FROM TestRecompile
WHERE (parameter1 = @p_parameter1 OR @p_parameter1 IS NULL)
and (parameter2 = @p_parameter2 OR @p_parameter2 IS NULL)
and (parameter3 = @p_parameter3 OR @p_parameter3 IS NULL)
這種最大的問題就是在查詢列上有索引,且查詢列上接收到的輸入參數非空的時候,是會抑制到索引的使用的
上文中演示了,雖然用到了Id 列上的索引,採用的是INDEX SCAN,比全表掃描(TABLE SCAN)強一點點,他跟真正用到INDEX SEEK在效率上講,完全是兩碼事,
所以我們在開發的過程中強烈不建議使用 Id = @p_parameter OR @p_parameter IS NULL這種寫法,
當然,在不考慮parameter sinffing問題的時候,我們首選參數化動態SQL,即便是非參數化動態SQL(EXEC的方式執行一個拼湊出來的字元串),也比Id = @p_parameter OR @p_parameter IS NULL這種方式好
如果有人進一步問:為什麼查詢條件中Id = @p_parameter OR @p_parameter IS NULL這種寫法會抑制到索引的使用,真的是一個很難解釋清楚的問題,解釋不清楚也是一件很尷尬的事。
這種邏輯之所以抑制到索引的最優化使用,真如上文分析的,優化器沒有真正的用到INDEX SEEK,是為了安全起見(上文所謂的plays safe)考慮
說道到這裡我又開始凌亂了,也就是WITH RECOMPILE和OPTION(RECOMPILE)這兩種方式的造強制,有一種只可意會不可言傳的感覺。
這就是即便是編譯的過程中知道具體的參數值,也做到編譯出來INDEX SEEK的執行計劃的原因
總是我在http://sqlinthewild.co.za/index.php/2009/03/19/catch-all-queries/這裡找到了跟我對該問題理解的相似的解釋,也算釋懷了
總結:本文通過一個簡單的案例,解釋了WITH RECOMPILE和OPTION(RECOMPILE)這種強制重編譯方式的區別,以及引申出來的The Parameter Embedding Optimization(第一次聽說)優化機制。
很多時候,自己對一些知識只是想當然地去理解和使用,比如隨意使用WITH RECOMPILE和OPTION(RECOMPILE),
粗暴地認為這兩種強制重編譯的方式區別僅僅在於一個是存儲過程級的重編譯,一個是SQL語句級的重編譯。真正拿著case測試的時候,才發現,還真不一樣。
下一篇寫一個跟這個機制類似的同樣有意思的文章。
http://sqlinthewild.co.za/index.php/2009/03/19/catch-all-queries/
http://www.sommarskog.se/dyn-search-2008.html
同時,再次感謝Uest同學提供的參考資料和指導建議。