為了改進性能,分析重編譯的起因很重要。往往,重編譯可能並不需要,可以避免它以改進性能。了解導致重編譯發生的不同條件有助於評估重編譯的起因,並決定在重編譯不必要時避免它的方法。
存儲過程重編譯在以下情況下發生:
- 存儲過程語句中引用的常規表、臨時表或視圖的架構變化。架構變化包括表的元數據或表上索引的變化;
- 常規或臨時表的列上的綁定(如默認/規則)變化。
- 表索引或列上的統計的變化超過一定的閾值。
- 存儲過程編譯時一個對象不存在,但是在執行期間創建(延遲對象解析);
- SET選項變化;
- 執行計划老化並釋放;
- 對sp_recompile系統存儲過程的顯式調用。
- 顯式使用RECOMPILE子句;
可以在Profiler中看到這些變化。原因由SP:Recompile事件的EventSubClass數據列值指出:
| EventSubClass | 描述 |
| 1 | 常規表或試圖的結構或綁定變化 |
| 2 | 統計變化 |
| 3 | 在執行期間創建存儲過程計划中不存在的對象 |
| 4 | SET選項變化 |
| 5 | 臨時表架構或綁定變化 |
| 6 | 遠程行集的架構或綁定變化 |
| 7 | FOR BROWSE許可變化 |
| 8 | 查詢通知環境變化 |
| 9 | MPI視圖變化 |
| 10 | 游標選項變化 |
| 11 | 調用WITH RECOMPILE選項 |
一、架構或綁定變化
當一個視圖、常規表或臨時表的架構或綁定變化時,現有的存儲過程執行計划將作廢。執行任何引用這些對象的語句之前該存儲過程必須重編譯。SQL Server自動偵測這種情況並且重編譯存儲過程。
因此,要避免架構變化,盡量不要在存儲過程中涉及到臨時表,索引,字段的操作,即不要交替使用DML、DDL語句。
在存儲過程中,DDL語句通常用於創建局部臨時表以及修改他們的架構(包括添加索引)。這樣做可能影響現有計划的有效性,並且可能在引用該表的存儲過程語句被執行時導致重編譯。
如:
CREATE PROC sp_Test AS CREATE TABLE #TB1(ID INT,DSC NVARCHAR(50)) INSERT INTO #TB1(ID,DSC) SELECT TOP 100 ID,NAME FROM PersonTenThousand; --第1次重編譯 SELECT * FROM #TB1 AS TT --第2次重編譯 CREATE CLUSTERED INDEX IXID ON #TB1(ID); SELECT TOP 10 * FROM #TB1; --第3次重編譯 CREATE TABLE #TB2(c1 INT); SELECT * FROM #TB2; --第4次重編譯 GO EXEC sp_Test GO EXEC sp_Test
SQL Server Profiler跟蹤輸出:

可以看到存儲過程被重編譯4次。
- 第一次重編譯來自於多了個表#TB1,現有自行計划不包含表#TB1的任何信息。
- 第二次重編譯來自於臨時表數據的變化;
- 第三次重編譯來自於臨時表架構變化,多了個索引;
- 第四次沖編譯多了表#TB2,因為是新建的表,現有執行計划沒有關於#TB2的任何信息;
二、統計變化
SQL Server記錄表的變化數量。如果變化數量超過重編譯閾值(RT),SQL Server自動在存儲過程中引用該表時更新統計。當偵測到自動更新統計的條件時,SQL Server自動重新編譯存儲過程並更新統計。
RT由一組取決於表是永久表或臨時表(不是表變量)以及表中的行數的公式來確定。表10-3顯示了基本的公式,可以確定由於數據變化引起的語句重編譯預期時間。
| 表類型 | 公式 |
| 永久表 | 如果n(行數)<=500,則RT=500; 如果n>500,則RT=500+.2*n |
| 臨時表 | 如果n<6,則RT=6; 如果6<=n<=500,則RT=500; 如果n>500,則RT=500+.2*n |
統計變化引起的重編譯可能生成一個和前一個計划相同的計划,在這種情況下,重編譯是沒有必要的,如果開銷較大則應避免。
避免統計的變化而引起的重編譯有兩個方法:
- 使用KEEPFIXED PLAN選項;
- 禁用該表上的自動更新統計特性;
1、使用KEEPFIXED PLAN選項
SQL Server提供KEEPFIXED PLAN選項來避免因為統計變化引起的重編譯。
CREATE PROC GetPerson AS SELECT * FROM Person WHERE Id = 1 OPTION(KEEPFIXED PLAN);
如,像上面這種寫法,執行存儲過程就不會再因為表Person上的統計變化而引起重編譯。
2、禁用表上的自動更新統計
也可以通過禁用相關表上的自動更新統計來避免統計更新引起的重編譯。(不過這種方法就好比,腳疼就把整個腳砍掉,不可取)。
例如,可以關閉表Person上的自動更新統計
EXEC sp_autostats 'Person','OFF'
雖然這種方式可以避免重編譯,但是使用這種技術應該非常小心,因為過時的統計可能對基於開銷的優化器有負面的影響,如果禁用統計的自動更新,應該有一個定期更新統計的SQL任務。
三、延遲對象解析
存儲過程通常動態創建然后訪問數據庫對象。當這樣的存儲過程第一次執行時,第一個執行計划不包含關於運行時創建的對象的信息。因此,在第一個執行計划中,對這些對象的處理策略被延遲到存儲過程的運行時。
當執行一個引用這些對象的DML語句時,存儲過程被重新編譯以生成一個包含該對象處理策略的新計划。
在存儲過程中可以創建常規表和局部臨時表來保存中間結果。由於延遲對象解析引起的存儲過程重編譯對於常規表和局部臨時表來說有所不同。
1、由於常規表引起的重編譯
為了理解在存儲過程中創建常規表所致的存儲過程重編譯問題,考慮以下實例:
CREATE PROC dbo.p1 AS CREATE TABLE dbo.p1_t1(c1 INT); --存儲過程開始時表不存在 SELECT * FROM dbo.p1_t1; --導致重編譯 DROP TABLE dbo.p1_t1 --結束后刪除該表 GO EXEC dbo.p1 --第一次執行 EXEC dbo.p1 --第二次執行
SQL Server Profiler輸出:

第一次執行該存儲過程時,執行計划在存儲過程實際執行之前生成。
但是如果在存儲過程創建之前存儲過程中創建的表不存在,引用該表的SELECT語句尚不會有執行計划。因此為了執行SELECT語句,存儲過程必須重編譯。可以看到,在第二次執行時SELECT語句被重編譯,在第一次執行期間卸載存儲過程中的表並沒有卸載保存在過程緩沖中的存儲過程計划。SQL Server考慮為其表架構的一次變化,因此SQL Server在存儲過程后執行SELECT語句之前重新編譯存儲過程。因此,看到第一次的重編譯依然發生在BatchStarting之后。
2、由於局部臨時表引起的重編譯
大部分時候,在存儲過程中創建局部臨時表而不是常規表。為了禮節局部臨時表對存儲過程重編譯的不同影響,修改前面的實例,只用一個臨時表替換常規表。
CREATE TABLE dbo.p1 AS CREATE TAVLE #p1_t1(c1 INT) --指定本地臨時表 SELECT * FROM #p1_t1 --在第一次執行時導致重編譯 DROP TABLE #p1_t1 GO EXEC dbo.p1 --第一次執行 EXEC dbo.p1 --第二次執行
因為局部臨時表在存儲過程執行結束時自動卸載,所有沒有必要明確地卸載臨時表。但是,在局部臨時表工作完成之后馬上卸載它是一個好的編程習慣。
最好分開點擊,這樣SQL Server Profiler生成的監控比較清晰:

從監控輸出可以看到,第一次執行時存儲過程被重編譯。對應的EventSubClass值支出的重編譯起因和常規表上的一樣,但是,存儲過程在重新執行時不被重編譯,這和常規表不同。
存儲過程后續執行期間的局部臨時表架構與前一次執行時保持一致。局部臨時表不可用於存儲過程的范圍之外,所以其架構無論執行多少次都是一致的,因此,SQL Server在存儲過程后續執行期間確定能安全地重用現有計划,避免了重編譯。
臨時表會引起存儲過程重編譯,想要避免可以使用表變量代替。
四、SET選項變化
在存儲過程的執行計划取決於環境設置。如果環境設置在存儲過程中變化,則SQL Server在每次執行時重編譯存儲過程。
CREATE PROC dbo.p1 AS SELECT 'a' + null + 'b'; --第一次拼接 SET CONCAT_NULL_YIELDS_NULL OFF; SELECT 'a' + null + 'b'; --第二次拼接 SET ANSI_NULLS OFF; SELECT 'a' + null + 'b'; --第三次拼接 GO EXEC dbo.p1 --第一次執行 EXEC dbo.p1 --第二次執行
SQL Server Profiler輸出如下:

因為SET NOCOUNT沒有修改環境設置,不像前面看到的用於修改ANSI設置的SET語句,SET NOCOUNT不會導致存儲過程重編譯。
在存儲過程中修改SET選項導致SQL Server在執行SET語句后面的語句之前重編譯該存儲過程。因此,這個存儲過程分別在SET語句后被重編譯兩次。
但如果在這之后,在執行存儲過程:
EXEC dbo.p1 --第三次執行 EXEC dbo.p1 --第四次執行 EXEC dbo.p1 --第五次執行
都不會再重編譯,因為那些內容現在已經成為了執行計划的一部分。
因此,如果想要避免由於SET選項變化引起的存儲過程重編譯時,盡量不要在存儲過程中設置SET選項。
另外,SET NOCOUNT選項是一個例外,它沒有修改環境設置,不像前面示例中的那樣會導致重編譯。
五、執行計划老化
SQL Server通過維護緩沖中執行計划的壽命來管理過程緩沖的大小,如果一個存儲過程長時間未被重新執行,執行計划的壽命字段將下降為0,內存短缺時將把該計划從緩沖中刪除。當這種情況發生並且存儲過程被重新執行時,將生成一個新計划並將其緩沖到過程緩沖中。但是,如果系統中有足夠的內存,未使用的計划在內存壓力增加之前不會被刪除。
六、顯式調用sp_recompile系統存儲過程
SQL Server還提供了sp_recompile系統存儲過程來手工標記需要重編譯的存儲過程。這個系統存儲過程可以在表、視圖、存儲過程或觸發器上調用。如果在存儲過程或觸發器上調用,則該存儲過程或觸發器在下次執行時被重編譯。在表或視圖上調用標記所有調用該表/視圖的存儲過程和觸發器在下次執行時重新編譯。
如在表Person上調用sp_recompile,則所有調用Person表的存儲過程和觸發器被標記為需要重編譯,在下次執行時重新編譯:
sp_recompile 'Person'
也可以使用sp_compile來使用sp_executesql執行時指定撤銷重用現有計划。注意在 SQL Server Profiler集合中,指定重編譯存儲過程的記錄事件 SP:CacheInsert 而不是事件 SP:Recompile。指定表才是SP:Recompile。
示例:
CREATE PROC GetPerson @Id int AS SELECT * FROM PersonTenThousand WHERE ID = @Id GO EXEC GetPerson @Id = 9876 --連續執行3次,都沒有重編譯 sp_recompile 'GetPerson' --指定重編譯存儲過程 EXEC GetPerson @Id = 9876 --這次重編譯了 sp_recompile 'PersonTenThousand' --指定對引用表的存儲過程重編譯 EXEC GetPerson @Id = 9876 --這次也重編譯了
捕獲如下:

七、顯示使用WITH RECOMPILE子句
SQL Server允許使用CREATE PROCRDURE或EXECUTE的RECOMPILE子句顯式地重編譯一個存儲過程。這些方法降低了計划可重用性的效率,所以只應該在一些特殊的場合使用它們。
1、CREATE PROCEDURE語句的RECOMPILE子句
有時候,存儲過程的計划需求可能隨着調用存儲過程的參數值變化而變化。在這種情況下,重用使用不同參數值的該計划可能降低存儲過程的性能,可以使用CREATE PROCEDURE語句的RECOMPILE子句來強制每次執行存儲過程都生成一個新計划(僅僅新生成執行計划,並非重編譯整個存儲過程)。
示例:
CREATE PROC GetPerson @Id int WITH RECOMPILE AS SELECT * FROM PersonTenThousand WHERE ID = @Id GO EXEC GetPerson @Id = 9876 --每次都重新生成執行計划
SQL Server Profiler監控輸出如下:

2、Execute語句的RECOMPILE子句
存儲過程中的特定參數值可能需要不同的執行計划,可以在執行存儲過程時動態采用WITH RECOMPILE,對特定一次執行重新生成新計划。
CREATE PROC GetPerson @Id int AS SELECT * FROM PersonTenThousand WHERE ID = @Id GO EXEC GetPerson @Id = 9876 --不重新生成計划 EXEC GetPerson @Id = 9876 WITH RECOMPILE --新生成計划
SQL Server Profiler輸出:

當存儲過程使用WITH RECOMPILE子句執行時,將臨時生成一個新計划。這個新的計划不會被緩沖,並且不會影響現有計划。當存儲過程不使用RECOMPILE子句執行時,該計划和往常一樣被緩沖。僅僅影響一次執行,這與CREATE PROCDURE不同。因為可以考慮創建不同的存儲過程來代替使用CREATE PROCDURE時RECOMPILE。
八、使用OPTIMIZE FOR查詢提示
盡管不總是能減少或消除重編譯,但是使用OPTIMIZE FOR查詢提示可以幫助你使用指定的參數值來編譯計划,而不管調用的應用程序傳入的參數值。
如:
CREATE PROC dbo.getPerson @Id INT AS SELECT * FROM Person INNER JOIN Province WHERE Person.Id = @Id OPTION (OPTIMIZE FOR(@Id = 1)) --指定使用Id為1的參數來生成執行計划
執行:
EXEC dbo.getPerson @Id = 1234 WITH RECOMPILE --強制重編譯 EXEC dbo.getPerson @Id = 5678 WITH RECOMPILE --強制重編譯
現在,執行存儲過程都重編譯了,但是每次都是根據Id為1來生成執行計划。不會因為傳入的參數不同而改變。
