这个问题是在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;
其实这个案例也间接验证了嵌套循环链接,随着数据量的增加,这种方式对性能的消耗将呈现出指数级别的增加。