本文非原創,來源於網絡,作為記錄為以后查看
http://mysql.taobao.org/monthly/2016/10/10/
摘要
MSSQL Server參數嗅探既是一個涉及知識面非常廣泛,又是一個比較難於解決的課題,即使對於數據庫老手也是一個比較頭痛的問題。這篇文章從參數嗅探是什么,如何產生,表象是什么,會帶來哪些問題,如何解決這五個方面來探討參數嗅探的來龍去脈,期望能夠將SQL Server參數嗅探問題理清楚,道明白。
什么參數嗅探
當SQL Server第一次執行查詢語句或存儲過程(或者查詢語句與存儲過程被強制重新編譯)的時候,SQL Server會有一個進程來評估傳入的參數,並根據傳入的參數生成對應的執行計划緩存,然后參數的值會伴隨查詢語句或存儲過程執行計划一並保存在執行計划緩存里。這個評估的過程就叫做參數嗅探。
參數嗅探是如何產生的
SQL Server對查詢語句編譯和緩存機制是SQL語句執行過程中非常重要的環節,也是SQLOS內存管理非常重要的一環。理由是SQL Server對查詢語句編譯過程是非常消耗系統性能,代價昂貴的。因為它需要從成百上千條執行路徑中選擇一條最優的執行計划方案。所以,查詢語句可以重用執行計划的緩存,避免重復編譯,以此來節約系統開銷。這種編譯查詢語句,選擇最優執行方案,緩存執行計划的機制就是參數嗅探問題產生的理論基礎。
參數嗅探的表象
以上是比較枯燥的理論解釋,這里我們來看看兩個實際的例子。在此,我們以AdventureWorks2008R2數據庫中的Sales.SalesOrderDetail表做為我們測試的數據源,我們挑選其中三個典型的產品,ProductID分別為897,945和870,分別對應的訂單總數為2,257和4688。
挑選的方法如下:
use AdventureWorks2008R2 GO ;WITH DATA AS( select ProductID, COUNT(1) as order_count, rowid = ROW_NUMBER() OVER(ORDER BY COUNT(1) asc) from Sales.SalesOrderDetail with(nolock) group by ProductID ) SELECT * FROM DATA WHERE rowid in (1, 266, 133)
得到如下結果:
查詢語句的參數嗅探表象
接下來,我們看三個非常相似的查詢語句(僅傳入的參數值不同)的執行計划有什么差異。
三個查詢語句:
use AdventureWorks2008R2 GO SELECT SalesOrderDetailID, OrderQty FROM Sales.SalesOrderDetail WITH(NOLOCK) WHERE ProductID = 897; SELECT SalesOrderDetailID, OrderQty FROM Sales.SalesOrderDetail WITH(NOLOCK) WHERE ProductID = 945; SELECT SalesOrderDetailID, OrderQty FROM Sales.SalesOrderDetail WITH(NOLOCK) WHERE ProductID = 870;
分別的執行計划:
從這個執行計划對比來看,ProductID為945和897的兩條語句執行計划一致,因為滿足條件的記錄數非常少,分別為257條和2條,所以SQL Server均選擇走最優執行計划Index Seek + Key Lookup。但是與ProductID為870的查詢語句執行計划完全不同,這條語句SQL Server選擇走的是Clustered Index Scan,幾乎等價於Table Scan的性能消耗。這是因為,SQL Server認為滿足條件ProductID = 870的記錄數太多,達到了4688條記錄,與其走Index Seek + Key Lookup,還不如走Clustered Index Scan順序IO的效率高。從這里可以看出,SQL Server會因為傳入參數值的不同而選擇走不同的執行計划,執行效率也大不相同。確切的說,這個就是屬於查詢語句的參數嗅探問題范疇。
存儲過程的參數嗅探表象
上一小節,我們看了查詢語句的參數嗅探表象,這一小節我們來看看存儲過程參數嗅探的表象又是如何的呢?
首先,我們創建如下存儲過程:
USE AdventureWorks2008R2 GO CREATE PROCEDURE UP_GetOrderIDAndOrderQtyByProductID( @ProductID INT ) AS BEGIN SET NOCOUNT ON SELECT SalesOrderDetailID , OrderQty FROM Sales.SalesOrderDetail WITH(NOLOCK) WHERE ProductID = @ProductID; END GO
接下來,我們執行兩次這個存儲過程,傳入不同的參數:
EXEC dbo.UP_GetOrderIDAndOrderQtyByProductID 870
EXEC dbo.UP_GetOrderIDAndOrderQtyByProductID 945
從這個執行計划來看,ProductID為870和945走的相同的執行計划Clustered Index Scan,這個和上一小節得到的結果是不一樣的。上一節中ProductID = 945的查詢語句執行計划走的是Index Seek + Key Lookup。
當我們選擇第二個執行計划的Clustered Index Scan的時候,我們觀察Properties中的Estimated Number of Rows,這里顯示的是4668,但實際上正確得行數應該是257。如下如所示:
這到底是為什么呢?從另外一個角度來講,這個不正確的統計估值甚至會導致SQL Server走到一個不是最優的執行計划上來(根據上一小節,ProductID = 945的最優執行計划其實是Index Seek + Key Lookup)。
答案其實就是存儲過程的參數嗅探問題。這是因為,我們在首次執行這個存儲過程的時候,傳入的參數ProductID = 870對應的訂單總數為4668,SQL Server在編譯,緩存執行計划的時候,連同這個值一起記錄到執行計划緩存中了。從而影響到存儲過程的第二次及以后的執行計划方案,進而影響到存儲過程的執行效率。
我們可以通過如下方法來查看執行計划中傳入參數的值,右鍵 => Show Execution Plan XML => 搜索 ParameterCompiledValue
在此例中,我們很清楚的發現傳入參數值是870,同時也很清楚得看到了參數嗅探對於執行計划的影響:
...
<ColumnReference Column="@ProductID" ParameterCompiledValue="(870)" ParameterRuntimeValue="(870)" />
...
至此,我們分別從查詢語句和存儲過程兩個方便看到了參數嗅探的表象。
參數嗅探導致的問題
從參數嗅探的表象這一章節,我們可以對此參數嗅探的問題窺探一二。但是,參數嗅探可能會導致哪些常見的問題呢?根據我們的經驗,如果你遭遇了MSSQL Server以下奇怪問題,你可能就遇到參數嗅探這個“大魔頭”了。
ALTER PROCEDURE解決性能問題
某些傳入參數導致存儲過程執行非常緩慢,但是ALTER PROCDURE(所有代碼沒做任何改動)后,性能恢復正常。這個場景是我們之前經常遇到的,原因是當你ALTER PROCEDURE后,MSSQL Server會主動清除對應的存儲過程執行計划緩存,然后再次執行該存儲過程的時候,系統會重新編譯並緩存該存儲過程執行計划。
相同的存儲過程,相同的傳入參數,執行時快時慢
這個聽起來非常奇怪吧,當我們執行相同的存儲過程,傳入相同的參數值,但是執行效率時快時慢,請注意下面例子中的注釋部分。
USE AdventureWorks2008R2 GO --SQL Server 創建執行計划,優化ProductID = 870對應的大量訂單量,運行時間500毫秒 EXEC dbo.UP_GetOrderIDAndOrderQtyByProductID 870 --SQL Server直接獲取緩存中的執行計划,對於小量訂單來說可能不是最好的執行計划,不過沒關系,執行時間450毫秒 EXEC dbo.UP_GetOrderIDAndOrderQtyByProductID 945
現在我們清空了執行計划緩存,為了方便,我直接清除所有的執行計划緩存。
DBCC FREEPROCCACHE
再次執行存儲過程,這次我們交換了執行順序,先執行ProductID 945,然后執行ProductID 870。
USE AdventureWorks2008R2 GO --SQL Server 創建執行計划,優化ProductID = 945,對應於小量訂單的最優執行計划,運行時間100毫秒 EXEC dbo.UP_GetOrderIDAndOrderQtyByProductID 945 --SQL Server直接獲取緩存中的執行計划,對於大量訂單的ProductID 870來說,可能是很差的執行計划,執行時間60秒 EXEC dbo.UP_GetOrderIDAndOrderQtyByProductID 870
從這兩個批次執行的時間對比來看,ProductID 945和870執行時間有比較大的差異,特別是ProductID = 870。這種相同的存儲過程,相同的傳入參數,執行時快時慢的問題,也是由於參數嗅探導致的。
注意:這里只是為了描述這種現象,由於表數據量本來不大的原因,可能實際上執行時間可能沒有那么大的差異。
查詢語句放在存儲過程中和單獨執行效率差異大
某一個查詢語句,放在存儲過程中執行和拿出來單獨執行,時間消耗差異大,一般情況是拿出來單獨執行的語句很快,放到存儲過程中執行很慢。這個情況也是我們在產品環境常見的一種典型參數嗅探導致的問題。
參數嗅探的解決方法
上一節,我們探討了參數嗅探可能會導致的問題。當發現這些問題的時候,我們來看看兩類人的不同解決方法。請允許我將這兩類人分別命名為菜鳥和老鳥,沒有任何歧視,只是一個名字代號而已。
菜鳥的解決方法
菜鳥的理論很簡單粗暴,既然參數嗅探是因為查詢語句或者存儲過程的執行計划緩存導致,那么我只需要清空內存就可以解決這個問題了嘛。嗯,來看看菜鳥很傻很天真的做法吧。
- 方法一:重啟Windows OS。果然很黃很暴力,重啟Windows操作系統,徹底清空Windows所有內存內容。
- 方法二:重啟SQL Server Service。稍微溫柔一點點啦,重啟SQL Server Service,徹底清空SQL Server占用的所有內存,當然執行計划緩存也被清空了。
- 方法三:DBCC命令清空SQL Server執行計划緩存。又溫柔了不少吧,徹底清空了SQL Server所有的執行計划緩存,包含有問題的和沒有問題的緩存。
DBCC FREEPROCCACHE
老鳥的解決方法
當菜鳥還在為自己的解決方法解決了參數嗅探問題而沾沾自喜的時候,老鳥的思維已經走得很遠很遠了,老鳥就是老鳥,是菜鳥所望塵莫及的。老鳥的思維邏輯其實也很簡單,既然是某個或者某些查詢語句或存儲過程的執行計划緩存有問題,那么,我們只需要重新編譯緩存這些害群之馬就好了。
- 方法一:創建存儲過程使用WITH RECOMPILE
USE AdventureWorks2008R2 GO ALTER PROCEDURE dbo.UP_GetOrderIDAndOrderQtyByProductID( @ProductID INT ) WITH RECOMPILE AS BEGIN SET NOCOUNT ON SELECT SalesOrderDetailID , OrderQty FROM Sales.SalesOrderDetail WITH(NOLOCK) WHERE ProductID = @ProductID; END GO
再重新執行兩次存儲過程,傳入不同的參數值,我們可以看到均走到最優的執行計划上來了,說明參數嗅探的問題已經解決。這個方法帶來的一個問題就是每次執行這個存儲過程系統都會重新編譯,無法使用執行計划緩存。但是相對來說,重新編譯的系統開銷要遠遠小於參數嗅探導致的系統性能消耗,所以,兩害取其輕。
- 方法二:查詢語句使用Query Hits
如果我們知道ProductID對應的訂單總數分布,認為ProductID = 945為最好的執行計划,那么我們可以強制SQL Server按照參數輸入945來執行存儲過程,我們可以添加Query Hits來實現。這種方法的難點在於對表中數據分布有着精細的認識,可操作性不強,因為表中數據分布是隨時在改變的。
USE AdventureWorks2008R2 GO ALTER PROCEDURE dbo.UP_GetOrderIDAndOrderQtyByProductID( @ProductID INT ) AS BEGIN SET NOCOUNT ON SELECT SalesOrderDetailID , OrderQty FROM Sales.SalesOrderDetail WITH(NOLOCK) WHERE ProductID = @ProductID --OPTION (RECOMPILE); OPTION (OPTIMIZE FOR (@ProductID=945)); --OPTION (OPTIMIZE FOR (@ProductID UNKNOWN)); END GO
- 方法三:DBCC清除特定語句或存儲過程緩存
當清除執行計划緩存后,SQL Server再次執行會重新編譯對應語句或者存儲過程,以獲得最好的執行計划。在此以清除特定存儲過程執行計划緩存為例。
USE AdventureWorks2008R2 GO declare @plan_id varbinary(64) ; SELECT TOP 1 @plan_id = cache.plan_handle FROM sys.dm_exec_cached_plans cache CROSS APPLY sys.dm_exec_query_plan(cache.plan_handle) AS pla CROSS APPLY sys.dm_exec_sql_text(cache.plan_handle) AS txt WHERE pla.objectid = object_id(N'UP_GetOrderIDAndOrderQtyByProductID','P') and txt.objectid = object_id(N'UP_GetOrderIDAndOrderQtyByProductID','P') DBCC FREEPROCCACHE (@plan_id); GO
- 方法四:更新表對象統計信息
表統計信息過時導致執行計划評估不准確,進而影響查詢語句執行效率。這個也是導致參數嗅探問題另一個重要原因。這種情況,我們只需要手動更新表統計信息。這個解決方法的難點在於找到有問題的查詢語句和對應有問題的表。統計信息更新方法如下,如果發現StatsUpdated時間太過久遠就應該是被懷疑的對象:
USE AdventureWorks2008R2 GO UPDATE STATISTICS Sales.SalesOrderDetail WITH FULLSCAN; SELECT name AS index_name , STATS_DATE(OBJECT_ID, index_id) AS StatsUpdated FROM sys.indexes WHERE OBJECT_ID = OBJECT_ID('Sales.SalesOrderDetail') GO
- 方法五:重整表對象索引
另外一個導致執行計划評估不准確的重要原因是索引碎片過高(超過30%),這個也會導致參數嗅探問題的重要原因。這種情況我們需要手動重整索引碎片,方法如下:
USE AdventureWorks2008R2 GO select DB_NAME = DB_NAME(database_id) ,SCHEMA_NAME = SCHEMA_NAME(schema_id) ,OBJECT_NAME = tb.name ,ix.name ,avg_fragmentation_in_percent from sys.dm_db_index_physical_stats(db_id(),object_id('Sales.SalesOrderDetail','U'),NULL,NULL,'LIMITED') AS fra CROSS APPLY sys.indexes AS ix WITH (NOLOCK) INNER JOIN sys.tables as tb WITH(NOLOCK) ON ix.object_id = tb.object_id WHERE ix.object_id = fra.object_id and ix.index_id = fra.index_id USE AdventureWorks2008R2 GO ALTER INDEX PK_SalesOrderDetail_SalesOrderID_SalesOrderDetailID ON Sales.SalesOrderDetail REBUILD;
- 方法六:創建缺失的索引
還有一個重要的導致執行計划評估不准確的因素是表缺失索引,這個也是會導致參數嗅探的問題。查找缺失索引的方法如下:
USE AdventureWorks2008R2 GO SELECT TOP 10 database_name = db_name(details.database_id) , schema_name = SCHEMA_NAME(tb.schema_id) , object_name = tb.name , avg_estimated_impact = dm_migs.avg_user_impact*(dm_migs.user_seeks+dm_migs.user_scans) , last_user_seek = dm_migs.last_user_seek , create_index = 'CREATE INDEX [IX_' + OBJECT_NAME(details.OBJECT_ID,details.database_id) + '_' + REPLACE(REPLACE(REPLACE(ISNULL(details.equality_columns,''),', ','_'),'[',''),']','') + CASE WHEN details.equality_columns IS NOT NULL AND details.inequality_columns IS NOT NULL THEN '_' ELSE '' END + REPLACE(REPLACE(REPLACE(ISNULL(details.inequality_columns,''),', ','_'),'[',''),']','') + ']' + ' ON ' + details.statement + ' (' + ISNULL (details.equality_columns,'') + CASE WHEN details.equality_columns IS NOT NULL AND details.inequality_columns IS NOT NULL THEN ',' ELSE '' END + ISNULL (details.inequality_columns, '') + ')' + ISNULL (' INCLUDE (' + details.included_columns + ')', '') FROM sys.dm_db_missing_index_groups AS dm_mig WITH(NOLOCK) INNER JOIN sys.dm_db_missing_index_group_stats AS dm_migs WITH(NOLOCK) ON dm_migs.group_handle = dm_mig.index_group_handle INNER JOIN sys.dm_db_missing_index_details AS details WITH(NOLOCK) ON dm_mig.index_handle = details.index_handle INNER JOIN sys.tables AS tb WITH(NOLOCK) ON details.object_id = tb.object_id WHERE details.database_ID = DB_ID() ORDER BY Avg_Estimated_Impact DESC GO
- 方法七:使用本地變量
這是一個非常奇怪的解決方法,使用這種方法的原因是,對於本地變量SQL Server使用統計密度來代替統計直方圖,它會認為所有的本地變量均擁有相同的統計密度,即對應於相同的記錄數。這樣可以避免因為數據分布不均勻導致的參數嗅探問題。
USE AdventureWorks2008R2 GO ALTER PROCEDURE dbo.UP_GetOrderIDAndOrderQtyByProductID( @ProductID INT ) AS BEGIN SET NOCOUNT ON DECLARE @Local_ProductID INT ; SET @Local_ProductID = @ProductID ; SELECT SalesOrderDetailID , OrderQty FROM Sales.SalesOrderDetail WITH(NOLOCK) WHERE ProductID = @Local_ProductID END GO
至此結束,本節分享了菜鳥和老鳥關於參數嗅探問題的解決方法,我相信大家應該可以輕松的做出正確選擇適合自己的解決方法。
補充說明
以上所有代碼的測試環境是在MSSQL Server 2008R2 Enterprise中完成。
Microsoft SQL Server 2008 R2 (SP2) - 10.50.4000.0 (X64) Jun 28 2012 08:36:30 Copyright