這個問題是在SQL SERVER 2005 升級到SQL SERVER 2014的測試過程中一同事發現的。我覺得有點意思,遂稍微修改一下腳本展示出來,本來想構造這樣的一個案例來演示,但是畏懼麻煩,遂直接貼上原表,希望Leader不要叼我(當然個人覺得真沒啥,兩張表名而已,真泄露不了啥信息)。
腳本如下所示,非常簡單的一段SQL語句,我將其分為SQL1、SQL2、SQL3. 其實SQL2、SQL3是差不多的,唯一的區別在於多了一個IF EXISTS
DECLARE @Operation_Code CHAR(3) ,
@FNCardList VARCHAR(1000) ,
@RollList VARCHAR(1000) ,
@White VARCHAR(20) ,
@OneMinute VARCHAR(20) ,
@Operator VARCHAR(20) ,
@Is_NoWait BIT ,
@HoldCards VARCHAR(3000);
SELECT @Operation_Code = '999' ,
@FNCardList = 'A15309913' ,
@RollList = 'A15309913';
--SQL 1
DECLARE @FNCardTable TABLE ( Iden INT, FN_Card CHAR(9) );
INSERT INTO @FNCardTable
SELECT Iden ,
[No]
FROM PUBDB.dbo.udf_ConvertStrToTable(@FNCardList, ',') a;
--SQL 2
SELECT 1
FROM dbo.fnRepairOperation a WITH ( NOLOCK )
INNER JOIN @FNCardTable b ON CHARINDEX(b.FN_Card, a.FN_Card) > 0
INNER JOIN dbo.fnJobTraceHdr c WITH ( NOLOCK ) ON c.FN_Card = b.FN_Card
AND c.Current_Department = a.Current_Department
WHERE a.Check_Time IS NULL
AND a.Is_Ignore = 0;
PRINT ( @Operation_Code );
--SQL 3
IF EXISTS ( SELECT 1
FROM dbo.fnRepairOperation a WITH ( NOLOCK )
INNER JOIN @FNCardTable b ON CHARINDEX(b.FN_Card,
a.FN_Card) > 0
INNER JOIN dbo.fnJobTraceHdr c WITH ( NOLOCK ) ON c.FN_Card = b.FN_Card
AND c.Current_Department = a.Current_Department
WHERE a.Check_Time IS NULL
AND a.Is_Ignore = 0 )
BEGIN
RAISERROR('返回錯誤!', 16, 1);
RETURN;
END;
在SQL SERVER 2005的環境中,整個批處理的SQL執行只需要不到1秒的樣子。我們也能看到執行計划的COST對比值為0%,99%,1%。
在SQL SERVER 2014(SQL Server 2014 - 12.0.2000.8 Standard Edition )中執行時間突然變成了4分41秒。 最奇怪的是查詢計划的COST比值依然為1%,99%,0%。實際測試發現這個COST的比值是不准確的。因為單獨執行SQL1、SQL2只需要一秒。但是執行SQL3就需要4分多鍾。(當然SQL SERVER 2005 與SQL SERVER 2014的數據,索引是一致的,細心的人會注意下面提示缺少索引,加上這個索引依然慢的出奇,這個影響因素完全可以忽略)
SQL 2的實際執行計划如下所示
SQL 3的實際執行計划如下所示
另外,表dbo.fnRepairOperation的記錄數有332553,dbo.fnJobTraceHdr 的記錄數為110058。表變量@FNCardTable記錄數為1.對比執行計划,我們可以看到兩者的Nested Loops的外部表變化了,從表變量@FNCardTable變成了dbo.fnRepairOperation
我們先來看看SQL2執行計划里面的一些詳細信息,我們可以看到外邊循環表為@FNCardTable,循環次數為1(Actual Number of Rows 值為1),內部循環表為dbo.fnJobTraceHdr,循環次數為1(Number of Executions為1),符合條件的記錄集數據為1條(Actual Number of Rows 值為1)
那么再來看SQL3, 外部循環表變為dbo.fnRepairOperation,它走表掃描(Table Scan),循環次數為432(Actual Number of Rows),內部循環表為dbo.fnJobTraceHdr, 走索引掃描,總共循環了47545056次,這個值怎么來的呢? 因為內部循環表中符合記錄數為110058(表dbo.fnJobTraceHdr的記錄數), 110058*432 = 47545056,也就是說總共循環了四千七百多萬次。 偶的神啊。難怪如此之慢。起初,我以為是統計信息不准確導致數據庫優化器選擇了錯誤的執行計划,於是我更新了這兩個表的統計信息,甚至連索引也重建了。結果還是如此。看來的確是優化器沒有選擇最優的執行計划。但是沒有IF EXITS它又是正常的, 加了IF EXITS后執行計划就變成這個鳥樣。說不清是優化器的bug還是算法問題所導致。
那么怎么解決這個問題,可以用聯接提示(HASH JOIN HINT)指定SQL語句走HASH JOIN,此時批處理的SQL語句可以1秒出來。另外就是改寫該SQL語句的寫法。在此不做過多闡述
IF EXISTS ( SELECT 1
FROM dbo.fnRepairOperation a WITH ( NOLOCK )
INNER JOIN @FNCardTable b ON CHARINDEX(b.FN_Card,
a.FN_Card) > 0
INNER HASH JOIN dbo.fnJobTraceHdr c WITH ( NOLOCK ) ON c.FN_Card = b.FN_Card
AND c.Current_Department = a.Current_Department
WHERE a.Check_Time IS NULL
AND a.Is_Ignore = 0 )
BEGIN
RAISERROR('部分卡中有 班長新增加的工序或 回修工序,請聯系一下工藝員和當班班長!', 16, 1);
RETURN;
END;
其實這個案例也間接驗證了嵌套循環連接,隨着數據量的增長,這種方式對性能的消耗將呈現出指數級別的增長。