SQL Server性能調優雜記4(小心SQLCmd的動態參數方法讓你墮入性能問題)
系統上線完,性能問題往往是Warranty和後期維護的一個重要問題。
這些天,客戶又來反映,有一個查詢非常慢。這個查詢用的是主關鍵字查詢,由於主鍵是聚集索引,而且又做了碎片處理。應該是非常快。但是看到的現象就是很慢(10秒左右,最差有18秒之多)。排除了硬件、資源鎖定等問題,還不用到達Database端的Tunning級別。基本判斷和SQL文有關,要細看SQL文的執行計劃。
首先把SQL文找出來
SELECT a.AWB_NO,
a.BWB_NO,
a.CWB_NO,
a.ORIGIN,
a.DEST,
a.MODIFY_ON,
a.CREATED_ON,
a.CONSIGNOR_CUSTOMER_CODE,
a.CONSIGNOR_CODE,
a.CONSIGNOR_NAME,
a.CONSIGNEE_NAME,
a.CWB_STATUS,
a.CWB_TYPE,
ISNULL(a.PCS, 0) AS PCS,
a.BWBLIST,
b.PWEIGHT
FROM TB_CWB AS a
LEFT JOIN TB_CWBWEIGHT AS b
ON a.CWB_NO = b.CWB_NO
AND b.AVAILABLE = 'Y'
WHERE a.AVAILABLE = 'Y'
AND (a.CWB_NO = @CWB_NO OR
(( @CWB_NO IS NULL)
AND (a.AWB_NO = @AWB_NO OR @AWB_NO IS NULL)
AND (a.BWB_NO = @BWB_NO OR @BWB_NO IS NULL)
AND (a.IE_TYPE = @IE_TYPE OR @IE_TYPE IS NULL)
AND (a.CREATED_ON >= @DateFrom OR @DateFrom IS NULL)
AND (a.CREATED_ON <= @DateTo OR @DateTo IS NULL)
AND (a.PAYMENT = @PAYMENT OR @PAYMENT IS NULL)
AND (a.ORIGIN = @ORIGIN OR @ORIGIN IS NULL)
AND (a.DEST = @DEST OR @DEST IS NULL)
AND (a.CONSIGNOR_CUSTOMER_CODE = @CONSIGNOR_CUSTOMER_CODE OR @CONSIGNOR_CUSTOMER_CODE IS NULL)
AND (a.CONSIGNOR_NAME LIKE '%' + @CONSIGNOR_NAME + '%' OR @CONSIGNOR_NAME IS NULL)
AND (a.CONSIGNEE_NAME LIKE '%' + @CONSIGNEE_NAME + '%' OR @CONSIGNEE_NAME IS NULL)
AND (a.CWB_TYPE = @CWB_TYPE OR @CWB_TYPE IS NULL)))
雖然冗長,但是基本結構很清晰,就是如果當用主關鍵字查詢,後面一大堆就不起什麼作用。系統的速度和聚集索引有關係。
把語句改造一下,
SELECT a.AWB_NO,
a.BWB_NO,
a.CWB_NO,
a.ORIGIN,
a.DEST,
a.MODIFY_ON,
a.CREATED_ON,
a.CONSIGNOR_CUSTOMER_CODE,
a.CONSIGNOR_CODE,
a.CONSIGNOR_NAME,
a.CONSIGNEE_NAME,
a.CWB_STATUS,
a.CWB_TYPE,
ISNULL(a.PCS, 0) AS PCS,
a.BWBLIST,
b.PWEIGHT
FROM TB_CWB AS a
LEFT JOIN TB_CWBWEIGHT AS b
ON a.CWB_NO = b.CWB_NO
AND b.AVAILABLE = 'Y'
WHERE a.AVAILABLE = 'Y'
AND (a.CWB_NO = '31017768390' OR
(( '31010930775' IS NULL)
AND (a.AWB_NO = NULL OR NULL IS NULL)
AND (a.BWB_NO = NULL OR NULL IS NULL)
AND (a.IE_TYPE = NULL OR NULL IS NULL)
AND (a.CREATED_ON >= NULL OR NULL IS NULL)
AND (a.CREATED_ON <= NULL OR NULL IS NULL)
AND (a.PAYMENT = NULL OR NULL IS NULL)
AND (a.ORIGIN = NULL OR NULL IS NULL)
AND (a.DEST = NULL OR NULL IS NULL)
AND (a.CONSIGNOR_CUSTOMER_CODE = NULL OR NULL IS NULL)
AND (a.CONSIGNOR_NAME LIKE '%' + NULL + '%' OR NULL IS NULL)
AND (a.CONSIGNEE_NAME LIKE '%' + NULL + '%' OR NULL IS NULL)
AND (a.CWB_TYPE = NULL OR NULL IS NULL)))
執行速度飛快。毫秒級別。
那麼爲什麼客戶端程序,速度很慢呢(差10個數量級)?
我第一反應,2個的SQL文一定不一樣。因爲客戶端代碼採用的是SQLCommand的動態參數寫法。在這種情況下,SQL Server會在數據庫後臺用一個動態執行的存儲過程來執行(應該是爲了重用執行計劃)。
我們來看看SQL Server的執行方法
exec sp_executesql N'
SELECT a.AWB_NO,
a.BWB_NO,
a.CWB_NO,
a.ORIGIN,
a.DEST,
a.MODIFY_ON,
a.CREATED_ON,
a.CONSIGNOR_CUSTOMER_CODE,
a.CONSIGNOR_CODE,
a.CONSIGNOR_NAME,
a.CONSIGNEE_NAME,
a.CWB_STATUS,
a.CWB_TYPE,
ISNULL(a.PCS, 0) AS PCS,
a.BWBLIST,
b.PWEIGHT
FROM TB_CWB AS a
LEFT JOIN TB_CWBWEIGHT AS b
ON a.CWB_NO = b.CWB_NO
AND b.AVAILABLE = ''Y''
WHERE a.AVAILABLE = ''Y''
AND (a.CWB_NO = @CWB_NO OR
(( @CWB_NO IS NULL)
AND (a.AWB_NO = @AWB_NO OR @AWB_NO IS NULL)
AND (a.BWB_NO = @BWB_NO OR @BWB_NO IS NULL)
AND (a.IE_TYPE = @IE_TYPE OR @IE_TYPE IS NULL)
AND (a.CREATED_ON >= @DateFrom OR @DateFrom IS NULL)
AND (a.CREATED_ON <= @DateTo OR @DateTo IS NULL)
AND (a.PAYMENT = @PAYMENT OR @PAYMENT IS NULL)
AND (a.ORIGIN = @ORIGIN OR @ORIGIN IS NULL)
AND (a.DEST = @DEST OR @DEST IS NULL)
AND (a.CONSIGNOR_CUSTOMER_CODE = @CONSIGNOR_CUSTOMER_CODE OR @CONSIGNOR_CUSTOMER_CODE IS NULL)
AND (a.CONSIGNOR_NAME LIKE ''%'' + @CONSIGNOR_NAME + ''%'' OR @CONSIGNOR_NAME IS NULL)
AND (a.CONSIGNEE_NAME LIKE ''%'' + @CONSIGNEE_NAME + ''%'' OR @CONSIGNEE_NAME IS NULL)
AND (a.CWB_TYPE = @CWB_TYPE OR @CWB_TYPE IS NULL)))',N'@CWB_NO nvarchar(11),@AWB_NO nvarchar(4000),@BWB_NO nvarchar(4000),@IE_TYPE
nvarchar(4000),@DateFrom nvarchar(4000),@DateTo nvarchar(4000),@CWB_TYPE nvarchar(4000),@PAYMENT nvarchar(4000),@ORIGIN nvarchar(4000),@DEST
nvarchar(4000),@CONSIGNOR_CUSTOMER_CODE nvarchar(4000),@CONSIGNOR_NAME nvarchar(4000),@CONSIGNEE_NAME
nvarchar(4000)',@CWB_NO=N'31017768390',@AWB_NO=NULL,@BWB_NO=NULL,@IE_TYPE=NULL,@DateFrom=NULL,@DateTo=NULL,@CWB_TYPE=NULL,@PAYMENT=NULL,@ORIGIN=NULL,@DEST=NULL,@CONSIGNOR_CUSTOMER_CODE=NULL,@CONSIGNOR_NAME=NULL,@CONSIGNEE_NAME=NULL
這前後2種SQL文,在執行計劃上有差異,所以導致一快一慢。後者竟然可以慢到17秒之多(第一產生執行計劃的時候),即使馬上再執行也有10秒到15秒,改善不大。
看一下SQL文1的執行計劃
SQL文2的執行計劃
前後最大的差別就是多了中間的Filter.
Filter這一步對於整個性能的影響很明顯。而這步處理的工作就是原本我們SQL文意圖中要忽略掉的部分。
所以,千萬要小心,當用SQLCommand的動態參數爲你的應用程序帶來共通化方便的同時,也會讓你程序帶來性能問題的風險。
解決問題的方法,顯然不能修改程序代碼(換成動態自己產生SQL文),因爲這是基礎框架代碼。
只能把SQL文改寫一下
BEGIN
IF NOT(@CWB_NO IS NULL)
SELECT a.AWB_NO, a.BWB_NO,a.CWB_NO,a.ORIGIN,
a.DEST, a.MODIFY_ON, a.CREATED_ON,a.CONSIGNOR_CUSTOMER_CODE,
a.CONSIGNOR_CODE,a.CONSIGNOR_NAME,a.CONSIGNEE_NAME, a.CWB_STATUS,
a.CWB_TYPE,ISNULL(a.PCS, 0) AS PCS,a.BWBLIST,b.PWEIGHT
FROM TB_CWB AS a
LEFT JOIN TB_CWBWEIGHT AS b
ON a.CWB_NO = b.CWB_NO
AND b.AVAILABLE = ''Y''
WHERE a.AVAILABLE = ''Y''
AND a.CWB_NO = @CWB_NO
ELSE
SELECT a.AWB_NO, a.BWB_NO,a.CWB_NO,a.ORIGIN,
a.DEST, a.MODIFY_ON, a.CREATED_ON,a.CONSIGNOR_CUSTOMER_CODE,
a.CONSIGNOR_CODE,a.CONSIGNOR_NAME,a.CONSIGNEE_NAME, a.CWB_STATUS,
a.CWB_TYPE,ISNULL(a.PCS, 0) AS PCS,a.BWBLIST,b.PWEIGHT
FROM TB_CWB AS a
LEFT JOIN TB_CWBWEIGHT AS b
ON a.CWB_NO = b.CWB_NO
AND b.AVAILABLE = ''Y''
WHERE a.AVAILABLE = ''Y''
AND (a.AWB_NO = @AWB_NO OR @AWB_NO IS NULL)
AND (a.BWB_NO = @BWB_NO OR @BWB_NO IS NULL)
AND (a.IE_TYPE = @IE_TYPE OR @IE_TYPE IS NULL)
AND (a.CREATED_ON >= @DateFrom OR @DateFrom IS NULL)
AND (a.CREATED_ON <= @DateTo OR @DateTo IS NULL)
AND (a.PAYMENT = @PAYMENT OR @PAYMENT IS NULL)
AND (a.ORIGIN = @ORIGIN OR @ORIGIN IS NULL)
AND (a.DEST = @DEST OR @DEST IS NULL)
AND (a.CONSIGNOR_CUSTOMER_CODE = @CONSIGNOR_CUSTOMER_CODE OR @CONSIGNOR_CUSTOMER_CODE IS NULL)
AND (a.CONSIGNOR_NAME LIKE ''%'' + @CONSIGNOR_NAME + ''%'' OR @CONSIGNOR_NAME IS NULL)
AND (a.CONSIGNEE_NAME LIKE ''%'' + @CONSIGNEE_NAME + ''%'' OR @CONSIGNEE_NAME IS NULL)
AND (a.CWB_TYPE = @CWB_TYPE OR @CWB_TYPE IS NULL)
END
結果SQL文的執行計劃就正確了。
這些天,客戶又來反映,有一個查詢非常慢。這個查詢用的是主關鍵字查詢,由於主鍵是聚集索引,而且又做了碎片處理。應該是非常快。但是看到的現象就是很慢(10秒左右,最差有18秒之多)。排除了硬件、資源鎖定等問題,還不用到達Database端的Tunning級別。基本判斷和SQL文有關,要細看SQL文的執行計劃。
首先把SQL文找出來
SELECT a.AWB_NO,
a.BWB_NO,
a.CWB_NO,
a.ORIGIN,
a.DEST,
a.MODIFY_ON,
a.CREATED_ON,
a.CONSIGNOR_CUSTOMER_CODE,
a.CONSIGNOR_CODE,
a.CONSIGNOR_NAME,
a.CONSIGNEE_NAME,
a.CWB_STATUS,
a.CWB_TYPE,
ISNULL(a.PCS, 0) AS PCS,
a.BWBLIST,
b.PWEIGHT
FROM TB_CWB AS a
LEFT JOIN TB_CWBWEIGHT AS b
ON a.CWB_NO = b.CWB_NO
AND b.AVAILABLE = 'Y'
WHERE a.AVAILABLE = 'Y'
AND (a.CWB_NO = @CWB_NO OR
(( @CWB_NO IS NULL)
AND (a.AWB_NO = @AWB_NO OR @AWB_NO IS NULL)
AND (a.BWB_NO = @BWB_NO OR @BWB_NO IS NULL)
AND (a.IE_TYPE = @IE_TYPE OR @IE_TYPE IS NULL)
AND (a.CREATED_ON >= @DateFrom OR @DateFrom IS NULL)
AND (a.CREATED_ON <= @DateTo OR @DateTo IS NULL)
AND (a.PAYMENT = @PAYMENT OR @PAYMENT IS NULL)
AND (a.ORIGIN = @ORIGIN OR @ORIGIN IS NULL)
AND (a.DEST = @DEST OR @DEST IS NULL)
AND (a.CONSIGNOR_CUSTOMER_CODE = @CONSIGNOR_CUSTOMER_CODE OR @CONSIGNOR_CUSTOMER_CODE IS NULL)
AND (a.CONSIGNOR_NAME LIKE '%' + @CONSIGNOR_NAME + '%' OR @CONSIGNOR_NAME IS NULL)
AND (a.CONSIGNEE_NAME LIKE '%' + @CONSIGNEE_NAME + '%' OR @CONSIGNEE_NAME IS NULL)
AND (a.CWB_TYPE = @CWB_TYPE OR @CWB_TYPE IS NULL)))
雖然冗長,但是基本結構很清晰,就是如果當用主關鍵字查詢,後面一大堆就不起什麼作用。系統的速度和聚集索引有關係。
把語句改造一下,
SELECT a.AWB_NO,
a.BWB_NO,
a.CWB_NO,
a.ORIGIN,
a.DEST,
a.MODIFY_ON,
a.CREATED_ON,
a.CONSIGNOR_CUSTOMER_CODE,
a.CONSIGNOR_CODE,
a.CONSIGNOR_NAME,
a.CONSIGNEE_NAME,
a.CWB_STATUS,
a.CWB_TYPE,
ISNULL(a.PCS, 0) AS PCS,
a.BWBLIST,
b.PWEIGHT
FROM TB_CWB AS a
LEFT JOIN TB_CWBWEIGHT AS b
ON a.CWB_NO = b.CWB_NO
AND b.AVAILABLE = 'Y'
WHERE a.AVAILABLE = 'Y'
AND (a.CWB_NO = '31017768390' OR
(( '31010930775' IS NULL)
AND (a.AWB_NO = NULL OR NULL IS NULL)
AND (a.BWB_NO = NULL OR NULL IS NULL)
AND (a.IE_TYPE = NULL OR NULL IS NULL)
AND (a.CREATED_ON >= NULL OR NULL IS NULL)
AND (a.CREATED_ON <= NULL OR NULL IS NULL)
AND (a.PAYMENT = NULL OR NULL IS NULL)
AND (a.ORIGIN = NULL OR NULL IS NULL)
AND (a.DEST = NULL OR NULL IS NULL)
AND (a.CONSIGNOR_CUSTOMER_CODE = NULL OR NULL IS NULL)
AND (a.CONSIGNOR_NAME LIKE '%' + NULL + '%' OR NULL IS NULL)
AND (a.CONSIGNEE_NAME LIKE '%' + NULL + '%' OR NULL IS NULL)
AND (a.CWB_TYPE = NULL OR NULL IS NULL)))
執行速度飛快。毫秒級別。
那麼爲什麼客戶端程序,速度很慢呢(差10個數量級)?
我第一反應,2個的SQL文一定不一樣。因爲客戶端代碼採用的是SQLCommand的動態參數寫法。在這種情況下,SQL Server會在數據庫後臺用一個動態執行的存儲過程來執行(應該是爲了重用執行計劃)。
我們來看看SQL Server的執行方法
exec sp_executesql N'
SELECT a.AWB_NO,
a.BWB_NO,
a.CWB_NO,
a.ORIGIN,
a.DEST,
a.MODIFY_ON,
a.CREATED_ON,
a.CONSIGNOR_CUSTOMER_CODE,
a.CONSIGNOR_CODE,
a.CONSIGNOR_NAME,
a.CONSIGNEE_NAME,
a.CWB_STATUS,
a.CWB_TYPE,
ISNULL(a.PCS, 0) AS PCS,
a.BWBLIST,
b.PWEIGHT
FROM TB_CWB AS a
LEFT JOIN TB_CWBWEIGHT AS b
ON a.CWB_NO = b.CWB_NO
AND b.AVAILABLE = ''Y''
WHERE a.AVAILABLE = ''Y''
AND (a.CWB_NO = @CWB_NO OR
(( @CWB_NO IS NULL)
AND (a.AWB_NO = @AWB_NO OR @AWB_NO IS NULL)
AND (a.BWB_NO = @BWB_NO OR @BWB_NO IS NULL)
AND (a.IE_TYPE = @IE_TYPE OR @IE_TYPE IS NULL)
AND (a.CREATED_ON >= @DateFrom OR @DateFrom IS NULL)
AND (a.CREATED_ON <= @DateTo OR @DateTo IS NULL)
AND (a.PAYMENT = @PAYMENT OR @PAYMENT IS NULL)
AND (a.ORIGIN = @ORIGIN OR @ORIGIN IS NULL)
AND (a.DEST = @DEST OR @DEST IS NULL)
AND (a.CONSIGNOR_CUSTOMER_CODE = @CONSIGNOR_CUSTOMER_CODE OR @CONSIGNOR_CUSTOMER_CODE IS NULL)
AND (a.CONSIGNOR_NAME LIKE ''%'' + @CONSIGNOR_NAME + ''%'' OR @CONSIGNOR_NAME IS NULL)
AND (a.CONSIGNEE_NAME LIKE ''%'' + @CONSIGNEE_NAME + ''%'' OR @CONSIGNEE_NAME IS NULL)
AND (a.CWB_TYPE = @CWB_TYPE OR @CWB_TYPE IS NULL)))',N'@CWB_NO nvarchar(11),@AWB_NO nvarchar(4000),@BWB_NO nvarchar(4000),@IE_TYPE
nvarchar(4000),@DateFrom nvarchar(4000),@DateTo nvarchar(4000),@CWB_TYPE nvarchar(4000),@PAYMENT nvarchar(4000),@ORIGIN nvarchar(4000),@DEST
nvarchar(4000),@CONSIGNOR_CUSTOMER_CODE nvarchar(4000),@CONSIGNOR_NAME nvarchar(4000),@CONSIGNEE_NAME
nvarchar(4000)',@CWB_NO=N'31017768390',@AWB_NO=NULL,@BWB_NO=NULL,@IE_TYPE=NULL,@DateFrom=NULL,@DateTo=NULL,@CWB_TYPE=NULL,@PAYMENT=NULL,@ORIGIN=NULL,@DEST=NULL,@CONSIGNOR_CUSTOMER_CODE=NULL,@CONSIGNOR_NAME=NULL,@CONSIGNEE_NAME=NULL
這前後2種SQL文,在執行計劃上有差異,所以導致一快一慢。後者竟然可以慢到17秒之多(第一產生執行計劃的時候),即使馬上再執行也有10秒到15秒,改善不大。
看一下SQL文1的執行計劃
SQL文2的執行計劃
前後最大的差別就是多了中間的Filter.
Filter這一步對於整個性能的影響很明顯。而這步處理的工作就是原本我們SQL文意圖中要忽略掉的部分。
所以,千萬要小心,當用SQLCommand的動態參數爲你的應用程序帶來共通化方便的同時,也會讓你程序帶來性能問題的風險。
解決問題的方法,顯然不能修改程序代碼(換成動態自己產生SQL文),因爲這是基礎框架代碼。
只能把SQL文改寫一下
BEGIN
IF NOT(@CWB_NO IS NULL)
SELECT a.AWB_NO, a.BWB_NO,a.CWB_NO,a.ORIGIN,
a.DEST, a.MODIFY_ON, a.CREATED_ON,a.CONSIGNOR_CUSTOMER_CODE,
a.CONSIGNOR_CODE,a.CONSIGNOR_NAME,a.CONSIGNEE_NAME, a.CWB_STATUS,
a.CWB_TYPE,ISNULL(a.PCS, 0) AS PCS,a.BWBLIST,b.PWEIGHT
FROM TB_CWB AS a
LEFT JOIN TB_CWBWEIGHT AS b
ON a.CWB_NO = b.CWB_NO
AND b.AVAILABLE = ''Y''
WHERE a.AVAILABLE = ''Y''
AND a.CWB_NO = @CWB_NO
ELSE
SELECT a.AWB_NO, a.BWB_NO,a.CWB_NO,a.ORIGIN,
a.DEST, a.MODIFY_ON, a.CREATED_ON,a.CONSIGNOR_CUSTOMER_CODE,
a.CONSIGNOR_CODE,a.CONSIGNOR_NAME,a.CONSIGNEE_NAME, a.CWB_STATUS,
a.CWB_TYPE,ISNULL(a.PCS, 0) AS PCS,a.BWBLIST,b.PWEIGHT
FROM TB_CWB AS a
LEFT JOIN TB_CWBWEIGHT AS b
ON a.CWB_NO = b.CWB_NO
AND b.AVAILABLE = ''Y''
WHERE a.AVAILABLE = ''Y''
AND (a.AWB_NO = @AWB_NO OR @AWB_NO IS NULL)
AND (a.BWB_NO = @BWB_NO OR @BWB_NO IS NULL)
AND (a.IE_TYPE = @IE_TYPE OR @IE_TYPE IS NULL)
AND (a.CREATED_ON >= @DateFrom OR @DateFrom IS NULL)
AND (a.CREATED_ON <= @DateTo OR @DateTo IS NULL)
AND (a.PAYMENT = @PAYMENT OR @PAYMENT IS NULL)
AND (a.ORIGIN = @ORIGIN OR @ORIGIN IS NULL)
AND (a.DEST = @DEST OR @DEST IS NULL)
AND (a.CONSIGNOR_CUSTOMER_CODE = @CONSIGNOR_CUSTOMER_CODE OR @CONSIGNOR_CUSTOMER_CODE IS NULL)
AND (a.CONSIGNOR_NAME LIKE ''%'' + @CONSIGNOR_NAME + ''%'' OR @CONSIGNOR_NAME IS NULL)
AND (a.CONSIGNEE_NAME LIKE ''%'' + @CONSIGNEE_NAME + ''%'' OR @CONSIGNEE_NAME IS NULL)
AND (a.CWB_TYPE = @CWB_TYPE OR @CWB_TYPE IS NULL)
END
結果SQL文的執行計劃就正確了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.