這個問題是在SQL SERVER 2005 升級到SQL SERVER 2014的測試過程當中一同事發現的。我以爲有點意思,遂稍微修改一下腳本展現出來,原本想構造這樣的一個案例來演示,可是畏懼麻煩,遂直接貼上原表,但願Leader不要叼我(固然我的以爲真沒啥,兩張表名而已,真泄露不了啥信息)。 html
腳本以下所示,很是簡單的一段SQL語句,我將其分爲SQL一、SQL二、SQL3. 其實SQL二、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比值依然爲0%,99%,1%。實際測試發現這個COST的比值是不許確的。由於單獨執行SQL一、SQL2只須要一秒。可是執行SQL3就須要4分多鐘。(固然SQL SERVER 2005 與SQL SERVER 2014的數據,索引是一致的,細心的人會注意下面提示缺乏索引,加上這個索引依然慢的出奇,這個影響因素徹底能夠忽略) app
SQL 2的實際執行計劃以下所示 oop
SQL 3的實際執行計劃以下所示 post
另外,表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語句的寫法。在此不作過多闡述url
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;
其實這個案例也間接驗證了嵌套循環鏈接,隨着數據量的增加,這種方式對性能的消耗將呈現出指數級別的增加。