[SQL SERVER][Performance] 注意隱示轉換 Part2

[SQL SERVER][Performance] 注意隱示轉換 Part2

論壇上網友詢問WHERE 條件的執行規則

該網友已經知道 SMALLDATETIME 的合理範圍,

但網友不解的事,為什麼查詢語法多加了一個條件( PER_ID = 'A' ) 反而就不會出現轉換溢位錯誤,

由於T-SQL並非程序性的語言,但如果你知道SQL Server是用什麼來表示查詢邏輯順序的話,

對於這類看似無解的問題其實透過執行計畫比較後方可略知一二,

還沒查看執行計畫之前,我腦中想像大概是這樣的(有一點想像力是很重要的...XD)。

1. PER_ID = 'A' 

這條件會激起查詢最佳化程式使用 Index Seek 強烈無比的欲望(有相關正確索引前提下)

2.(CONVERT(SMALLDATETIME, CARD_TIME) BETWEEN '2012/08/01' AND '2012/08/31')

這條件會自動觸發隱示轉換(如果你不知道隱示轉換的話,可以看看我前一篇文章),

且Convert函式也將導致使用 index scan 來處理,但查詢最佳化程式為了要使用 index seek,

所以可能會改寫原有TSQL來達到使用 index seek 目的。

下面我會比較兩者執行計畫來驗證我的假設是否正確。

 
CREATE TABLE [dbo].[test_DATA](
 [PER_ID] NVARCHAR(50) NOT NULL,
 [CARD_TIME] [datetime] NOT NULL,
 CONSTRAINT [PK_test_DATA] PRIMARY KEY CLUSTERED 
(
 [PER_ID] ASC,
 [CARD_TIME] ASC
)WITH (IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]

 

INSERT INTO test_DATA
SELECT 'A', '2012-08-26 18:24:00.000'
INSERT INTO test_DATA
SELECT 'A', '2012-08-27 18:24:00.000'
INSERT INTO test_DATA
SELECT 'A', '2012-08-01 18:24:00.000'
INSERT INTO test_DATA
SELECT 'A', '2012-08-31 18:24:00.000'
INSERT INTO test_DATA
SELECT 'B', '2012-08-26 18:24:00.000'
INSERT INTO test_DATA
SELECT 'B', '2012-08-27 18:24:00.000'

--模擬實際狀況插入一筆大於2079:06:06的紀錄
INSERT INTO test_DATA
SELECT 'A', '2123-08-26 18:24:00.000'

 


 

 --發生溢位錯誤的查詢

SELECT  * 
FROM test_DATA
WHERE (CONVERT(SMALLDATETIME, CARD_TIME) BETWEEN '2012/08/01' AND '2012/08/31')


執行計畫

SELECT  *   FROM test_DATA  WHERE (CONVERT(SMALLDATETIME, CARD_TIME) BETWEEN '2012/08/01' AND '2012/08/31')
  |--Clustered Index Scan(OBJECT:([demo].[dbo].[test_DATA].[PK_test_DATA]), 

     WHERE:(CONVERT(smalldatetime,[demo].[dbo].[test_DATA].[CARD_TIME],0)>=CONVERT_IMPLICIT(smalldatetime,[@1],0) 

     AND CONVERT(smalldatetime,[demo].[dbo].[test_DATA].[CARD_TIME],0)<=CONVERT_IMPLICIT(smalldatetime,[@2],0)))

 

Argument:


OBJECT:([demo].[dbo].[test_DATA].[PK_test_DATA]), 

WHERE:(CONVERT(smalldatetime,[demo].[dbo].[test_DATA].[CARD_TIME],0)>=CONVERT_IMPLICIT(smalldatetime,[@1],0) 

AND 

CONVERT(smalldatetime,[demo].[dbo].[test_DATA].[CARD_TIME],0)<=CONVERT_IMPLICIT(smalldatetime,[@2],0))

可以看到查詢最佳化程式使用 clustered index scan( PK_test_DATA full scan)作業,
並呼叫 CONVERT_IMPLICIT 函式自動轉換資料型別,

因為是 Full Scan 作業,所以會掃描基底資料表( 一筆一筆逐一處理),
所以只要某一筆資料內容範圍不符合 smalldatetime 範圍就會發生溢位錯誤,

透過查詢計畫查看這是很正常的結果。

 


--不會發生溢位錯誤的查詢(只多了一個PER_ID = 'A' 條件)
SELECT  * 
FROM test_DATA
WHERE PER_ID = 'A' 
AND (CONVERT(SMALLDATETIME, CARD_TIME) BETWEEN '2012/08/01' AND '2012/08/31')

 


 

執行計畫 

SELECT  *   FROM test_DATA  WHERE PER_ID = 'A'   AND (CONVERT(SMALLDATETIME, CARD_TIME) BETWEEN '2012/08/01' AND '2012/08/31')
  |--Nested Loops(Inner Join, OUTER REFERENCES:([Expr1013], [Expr1014], [Expr1015]))
       |--Merge Interval
       |    |--Concatenation
       |         |--Compute Scalar(DEFINE:(([Expr1008],[Expr1009],[Expr1007])=GetRangeThroughConvert(CONVERT_IMPLICIT(smalldatetime,[@2],0),NULL,(22))))
       |         |    |--Constant Scan
       |         |--Compute Scalar(DEFINE:(([Expr1011],[Expr1012],[Expr1010])=GetRangeThroughConvert(NULL,CONVERT_IMPLICIT(smalldatetime,[@3],0),(42))))
       |              |--Constant Scan
       |--Clustered Index Seek(OBJECT:([demo].[dbo].[test_DATA].[PK_test_DATA]), 

        SEEK:([demo].[dbo].[test_DATA].[PER_ID]=CONVERT_IMPLICIT(nvarchar(4000),[@1],0) 

        AND [demo].[dbo].[test_DATA].[CARD_TIME] < [Expr1014] 

        AND [demo].[dbo].[test_DATA].[CARD_TIME] > [Expr1013]),  

        WHERE:(CONVERT(smalldatetime,[demo].[dbo].[test_DATA].[CARD_TIME],0)>=CONVERT_IMPLICIT(smalldatetime,[@2],0) 

        AND CONVERT(smalldatetime,[demo].[dbo].[test_DATA].[CARD_TIME],0)<=CONVERT_IMPLICIT(smalldatetime,[@3],0)) 

        ORDERED FORWARD)

 


大家有看到查詢最佳化程式為了要使用 Clustered Index Seek,可說大幅度改寫原有TSQL甚至還多了兩個條件,

而這兩個條件就是讓查詢最佳化程式可以使用 Index Seek作業,且讓該查詢又不發生溢位錯誤的真正原因,

那麼 Expr1014和Expr1013又是什麼呢?下面讓我來解釋一下

 

Compute Scalar (計算純量)作業:


DEFINE:(([Expr1008],[Expr1009],[Expr1007])
=GetRangeThroughConvert(CONVERT_IMPLICIT(smalldatetime,[@2],0),NULL,(22)))

DEFINE:(([Expr1011],[Expr1012],[Expr1010])
=GetRangeThroughConvert(NULL,CONVERT_IMPLICIT(smalldatetime,[@3],0),(42)))

 

 


Clustered Index Seek (索引搜尋)作業:

OBJECT:([demo].[dbo].[test_DATA].[PK_test_DATA]), 
SEEK:([demo].[dbo].[test_DATA].[PER_ID]=CONVERT_IMPLICIT(nvarchar(4000),[@1],0) 
AND [demo].[dbo].[test_DATA].[CARD_TIME] < [Expr1014] 
AND [demo].[dbo].[test_DATA].[CARD_TIME] > [Expr1013]),  
WHERE:(CONVERT(smalldatetime,[demo].[dbo].[test_DATA].[CARD_TIME],0)>=CONVERT_IMPLICIT(smalldatetime,[@2],0) 
AND 
CONVERT(smalldatetime,[demo].[dbo].[test_DATA].[CARD_TIME],0)<=CONVERT_IMPLICIT(smalldatetime,[@3],0)) 
ORDERED FORWARD

 

因為查詢最佳化程式無法直接對 [CARD_TIME] 欄位使用 Index Seek(原TSQL針對該欄位使用了Convert函式),

而且還必須進行資料型別的轉換 ( Convert Datetime to smalldatetime),

但前面我有說過 PER_ID = 'A'  條件會激起查詢最佳化程式使用 Index Seek作業,

所以最佳化程式將大幅度改寫TSQL並進行資料型別的轉換,

但這樣的改寫加上資料型別轉換可能會造成查詢結果不正確,

所以最佳化程式先執行計算純量取得符合 smalldatetime 範圍,

確保原有 datetime 資料也在 smalldatetime 範圍內,

然後使用這計算出來的範圍邊界,針對該索引物件執行 Index Seek作業(少量資料執行隱示轉換),

最後在和 smalldatetime 的資料比較,最終返回使用者所要求的資料結果集,

查詢最佳化程式,為了要避免 index scan並保有結果正確性,

採用大幅度改寫原有TSQL並計算該資料型別正確範圍邊界,

而這也是該查詢不會發生溢位錯誤的真正原因。

 

而這句TSQL被查詢最佳化程式改寫後應該如下

SELECT  * 
FROM test_DATA
WHERE (PER_ID = 'A' AND CARD_TIME < '20120831 00:00:00' AND  CARD_TIME > '20120801 00:00:00' )
AND (CONVERT(SMALLDATETIME, CARD_TIME) BETWEEN '2012/08/01' AND '2012/08/31')

 

針對搜尋datetime欄位相關建議可以查看 [SQL SERVER][Memo]搜尋datetime類型欄位三兩事

但有沒有辦法來解決這問題呢?經過上面的測試,我們已經知道查詢最佳化程式為了要使用 Index Seek,

所以會偷偷加上兩個邊界條件,所以這問題當然是有解的。

 


create index idx_1 on test_DATA(CARD_TIME)
include(PER_ID)

 


SELECT  * 
FROM test_DATA with(INDEX(idx_1)) --使用hint 強制走idx_1 索引
--邊界條件記得加上時間,這樣查詢結果才會正確
WHERE (CONVERT(SMALLDATETIME, CARD_TIME) BETWEEN '2012/08/01 00:00:00' AND '2012/08/31 23:59:59') 

 


image 

 

 

 

參考

[SQL SERVER][Performance] 注意隱示轉換