1. 程式人生 > >WITH RECOMPILE和OPTION(RECOMPILE)區別僅僅是儲存過程級重編譯和SQL語句級重編譯嗎

WITH RECOMPILE和OPTION(RECOMPILE)區別僅僅是儲存過程級重編譯和SQL語句級重編譯嗎

 在考慮重編譯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)選項可以地預估表變數的行數,再次說明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://www.cnblogs.com/wy123/p/6262800.html