WITH RECOMPILE和OPTION(RECOMPILE)區別僅僅是預存程序級重編譯和SQL語句級重編譯

來源:互聯網
上載者:User

標籤:設定   知識   tar   固定   arc   結果   關於   效果   常量   

在考慮重編譯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 RECOMPILEASBEGIN    SET NOCOUNT ON;    SELECT * FROM TestRecompile WHERE Id = @p_parameter OR @p_parameter IS NULLENDGO

 

  關於語句級重編譯,典型用法如下,在某一條SQL語句的末尾指定OPTION(RECOMPILE)

CREATE PROCEDURE TestRecompile_OptionRecompile(    @p_parameter VARCHAR(50))ASBEGIN    SET NOCOUNT ON;    SELECT * FROM TestRecompile WHERE Id = @p_parameter OR @p_parameter IS NULL OPTION(RECOMPILE)ENDGO

 

  按照慣例,先搭建一個測試環境
  建立一張TestRecompile的表,也即上面預存程序中用到的表,插入100W行資料,Id欄位上建立一個名字為idx_id的索引

CREATE TABLE TestRecompile(    Id int,    Value varchar(50))GODECLARE @i int = 0WHILE @i<=1000000BEGIN    INSERT INTO TestRecompile VALUES (@i,NEWID())    SET @i = @i+1ENDCREATE 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測試的時候,才發現,還真不一樣。

 

下一篇寫一個跟這個機制類似的同樣有意思的文章。

 

參考資料:https://sqlperformance.com/2013/08/t-sql-queries/parameter-sniffing-embedding-and-the-recompile-options

     http://sqlinthewild.co.za/index.php/2009/03/19/catch-all-queries/

       http://www.sommarskog.se/dyn-search-2008.html

     同時,再次感謝Uest同學提供的參考資料和指導建議。

WITH RECOMPILE和OPTION(RECOMPILE)區別僅僅是預存程序級重編譯和SQL語句級重編譯

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.