本文屬於《理解效能的奧祕——應用程式中慢,SSMS中快》系列

接上文:理解效能的奧祕——應用程式中慢,SSMS中快(2)——SQL Server如何編譯儲存過程
在我們開始深入研究如何處理引數嗅探相關的效能問題之前,由於這個課題過於廣泛,所以首先先介紹一些跟引數嗅探沒有直接關係的內容,但是又會導致語句在SSMS和應用程式中存在效能差異的情況。

替換變數和引數:

前面已經接觸過,但是在這裡對其進行擴充套件。有時會看到論壇上有人說,某個儲存過程很慢,但是把相同的語句提取出來單獨執行就很快。真相就是:語句涉及了變數,可能是本地變數或者引數。為了單獨檢查語句問題,他們把變數替換成常量。但是前面提到過,單獨的語句和在儲存過程中很不一樣,當用常量替換變數時,SQL Server可以更加準確地預估影響行數,從而生成更好的查詢計劃。並且SQL Server不用考慮下一次執行是,常量會被改變。
另外一個類似的錯誤就是把引數放入變數中,如:

CREATE PROCEDURE some_sp @par1 int 
AS
   ...
   -- 某些使用到變數 @par1的語句

你想檢查某個語句問題,所以你改成:
DECLARE @par1 int
SELECT @par1 = 4711
-- 接下來的語句

上一節已經說過,這種方式和原來方式有很大不同,SQL Server因為不清楚本地變數的情況,所以會使用標準假設(一般就是表總數的30%)。但是如果你有一個千行程式碼的儲存過程,然後其中某個語句很慢,要如何定位問題,同時又能保持與在儲存過程中一樣的環境呢?其中一個簡單但也存在限制的方法就是新增“OPTION(RECOMPILE)”提示到語句的最後,這個提示僅在SQL 2005中有效,可以觸發語句對所有引數、變數在執行時進行引數嗅探和重編譯。
但是這個方法只對只有一個引數的查詢或者儲存過程中沒有本地變數的情況有用。(因為本地變數的值通常不會被嗅探。)另外,這個方法對SQL 2008及後續版本無效,因為OPTION(RECOMPILE)的實現方式變得更加合理:如果所有變數值都是常數,SQL Server會編譯查詢。(由於一個致命bug的存在,微軟不得不把這種行為恢復,所以並不能應用於所有的SQL 2008版本中)。
一個總能生效的方式就是把語句封裝到sp_executesql中:

EXEC sp_executesql N'-- 牽涉到引數的語句 @par1', N'@par1 int', 4711
這裡需要注意的是,對於字元文字,需要使用雙引號包住。如果查詢涉及本地變數,可以在動態SQL中賦值。
還有一種方式就是建立一個帶有問題語句的虛擬儲存過程,為了避免資料庫中存在垃圾物件,你可以使用臨時儲存過程:
CREATE PROCEDURE #test @par1 int AS
   -- 問題語句.

如果是動態SQL ,也同樣確保在這類儲存過程中進行區域性定義本地變數。但是需要警告一下,目前還沒發現SQL Server是否有對臨時儲存過程有特殊的調整或優化的限制。

阻塞:


不要忘記應用程式執行慢的其中一個可能原因就是與阻塞 有關。當你在3小時後在SSMS中測試查詢時,阻塞源可能已經結束。如果你在SSMS中不管是否改動ARITHABORT,執行儲存過程都沒問題,一直都很快,那麼阻塞就應該納入考慮內容並進行深究。但是這個話題很大,也超出了本系列的主旨,所以可以看作者的另外一篇文章(以後有空也會一起翻譯)beta lockinfo


索引檢視和索引化的計算列:

在SQL 2000時代,這個問題比後續版本嚴重很多,從SQL Server 2005開始,所以檢視和索引化後的計算列(包括SQL 2008加入的過濾索引filter index)在語句編譯期間,下面的設定必須為ON:QUOTED_IDENTIFIER, ANSI_NULLS, ANSI_WARNINGS, ANSI_PADDING, CONCAT_NULL_YIELDS_NUL,而NUMERIC_ROUNDABORT必須為OFF。

而在SQL 2000中,應用程式使用預設SET選項對索引化的計算列和索引檢視是沒有什麼好處的。但是即使存在引數嗅探,當你在SSMS或查詢分析器中執行是,效能也往往可能好很多,因為ARITHABORT預設為ON。

但是由於SQL 2000時代過去太久了,估計已經很少人還在用,所以如果對這個內容有興趣,讀者可以去看原文,因為這部分主要是對SQL 2000描述的。點選開啟連結



連結伺服器的問題:

這裡假定你用的遠端伺服器是SQL 2012 SP1之前的版本。假設下面的語句:
SELECT C.*
FROM SOME_SERVER.Northwind.dbo.Orders O
JOIN Customers C
	ON O.CustomerID = C.CustomerID
WHERE O.OrderID > 20000

以兩個不同的使用者運行了語句兩次。第一個使用者是兩臺伺服器的sysadmin,第二個使用者僅有SELECT許可權。為了確保得到不同快取條目, 使用了不同的ARITHABORT設定。在以sysadmin執行時,查詢計劃如下:



以普通使用者執行是,得到的執行計劃如下:

為什麼查詢計劃不一樣呢?可以肯定的是不是引數嗅探問題,因為語句沒有引數。但是當發現查詢計劃不是與其形狀或者預期操作符時,應該看看預估行數。這兩個查詢計劃的“Remote Query”操作符的屬性如下:左邊的是第一個,右邊的是第二個


可以發現預估行數不一樣。當以sysadmin身份執行時,預估行數為1,這是正確的數量,因為Northwind庫的Orders表中不存在Order ID超過20000的資料。但是當以普通使用者執行時,預估行數為249行。也就是表總數的30%。因為此時統計資訊丟失導致優化器對資料量的預估不準確。
當查詢直接訪問本地例項的表時,優化器可以得到語句相關的所有表的統計資訊,不存在額外的許可權檢查。 當SQL Server訪問一個連結伺服器時,伺服器之間的通訊並沒有使用專用的的協議,而是使用標準的OLE DB介面訪問連結伺服器,其他諸如sql server例項、oracle、文字檔案或任何自定義資料來源也是如此。具體如何獲取統計資訊還要取決於資料來源和請求的OLE DB Provider。 在這種情況下,SQL Server Native Client會通過兩步獲得統計資訊(可以在遠端伺服器上使用Profiler檢查):
  1. SQL Server Native Client驅動會先執行sp_table_statistics2_rowset返回統計資訊包含的列的資訊,也包括了基數資訊和密度資訊。
  2. 驅動執行DBCC SHOW_STATISTICS,返回完整的分佈統計資訊。
直到從SQL Server 2012 RTM位置,為了有足夠許可權執行DBCC SHWO_STATISTICS命令,必須使用屬於sysadmin角色或者資料庫層面的db_owner或db_ddladmin角色的許可權。
所以使用不同許可權的賬號運行同一個語句的時候,會得到不同的結果,當以sysadmin執行的時候,可以獲取完整的分佈資訊,意味著能發現沒有資料複合orderid>20000的條件,所以預估行數為1(注意SQL Server永遠不會用預估行數為0代表沒有匹配資料)。但是以普通使用者執行時,因為許可權問題導致DBCC SHOW_STATISTICS失敗,而且不丟擲錯誤,以優化器接受“沒有統計資訊”為替代方案,然後進行預設假設。所以在基數預估的時候就不夠準確了。
不管什麼時候,當你遇到一個包含連結伺服器查詢,在應用程式中很慢,在SSMS中很快時,你都應該檢查一下是否許可權問題導致的。如果是許可權問題,那麼可以考慮下面方案:
  • 可以把使用者加到遠端資料庫的db_ddladmin角色中,但是這個意味著使用者可以增加和刪除表,一般不建議。
  • 預設情況下,一個使用者連線遠端伺服器的時候,都是使用遠端伺服器中相同的賬號,但是可以使用sp_addlinkedsrvlogin對映 一個登入號,以便使用者有許可權執行屬於db_ddladminde 角色的事情。但是這個賬號必須是SQL 登入賬號,所以要確保遠端伺服器是否啟用了混合身份驗證,但是這種方案從安全形度來說還是有問題。
  • 某些情況下可以改寫成OPENQUERY強制在遠端伺服器中進行預估。如果查詢包含了多個遠端表的情況下,這種方式特別有效。(但是也有風險,因為優化器可能只能從遠端伺服器中獲取更少的統計資訊。)
  • 也可以使用完整的提示和計劃嚮導來實現你期望的查詢計劃。
  • 最後,要考慮一下是否真的有必要使用連結伺服器而不能放在同一個 伺服器上?能否修改?或者有其他解決方案?
再次提醒:重要的是查詢在遠端伺服器上是否有許可權,而不是在本地伺服器上。另外,這個問題僅存在於遠端伺服器的SQL Server版本是SQL Server 2012 RTM及以前版本的SQL Server。從SQL 2012 SP1開始,執行DBCC SHOW_STATISTICS的許可權要求已經被放寬,只要有SELECT許可權即可執行。

小結:

本節沒有介紹引數嗅探的情況,而是 介紹一些同樣也會導致在應用程式中很慢,在SSMS中很快的情況。但是因為本主題主要是介紹引數嗅探,所以還是儘可能把篇幅放在這個點上,下一節將會介紹:
  • 收集解決引數嗅探問題的資訊