本文出處: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/這里找到了跟我對該問題理解的相似的解釋,也算釋懷了
20191127補充:
對於類似的where條件:parameter1 = @p_parameter1 OR @p_parameter1 IS NULL的OR后面一個分支:@p_parameter1 IS NULL
However, this time the Query Optimizer is not able to use the density information and instead it is using the standard guess of 30% selectivity for inequality comparisons. That means that the estimated number of rows is always 30% of the total number of records for an inequality operator and,
in this case, 30% of 100000 is 30000
這里的30%我沒有找到相關的官方文檔,但我在生產環境測試過都是取30%,如果你有找到相關支持文檔,麻煩告知~
也就是說它直接使用統計信息的Rows*30%得到估計行數
因此在預估超過30%的情況下,只能走表掃描
如果准確地預估表變量
與with recompile相比,option(recompile)選項可以地預估表變量的行數,再次說明option(recompile)是基於參數置入(Parameter Embedding Optimization)編譯的特性
直接看例子吧,應該是很清楚的
with recompile情況下對表變量的預估,
簡單解釋一下,就是在存儲過程中,with recompile強制存儲過程重編譯的情況下,表變量參數join的時候,對表變量的預估情況如下

option(recompile)強制存儲過程重編譯的情況下,表變量參數join的時候,對表變量的預估情況如下
可見,option(recompile)強制重編譯,不但可以獲取與with recompile重編譯不一樣的執行計划,也可以非常准確地預估到表變量的行數
可以解決默認情況下,表變量總是預估為1行的情況(應該是sqlserver 2012之后有改善,sqlserver 2012之前默認預估為1行)

總結:本文通過一個簡單的案例,解釋了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同學提供的參考資料和指導建議。
