问题 COALESCE - 保证短路?


这个问题关于使用COALESCE的一个简洁的答案 简化复杂的逻辑树。我考虑过短路问题。

例如,在大多数语言的函数中,参数被完全评估,然后传递给函数。在C:

int f(float x, float y) {
    return x;
}

f(a, a / b) ; // This will result in an error if b == 0

这似乎不是一个限制 COALESCE SQL Server中的“函数”:

CREATE TABLE Fractions (
    Numerator float
    ,Denominator float
)

INSERT INTO Fractions VALUES (1, 1)
INSERT INTO Fractions VALUES (1, 2)
INSERT INTO Fractions VALUES (1, 3)
INSERT INTO Fractions VALUES (1, 0)
INSERT INTO Fractions VALUES (2, 0)
INSERT INTO Fractions VALUES (3, 0)

SELECT Numerator
    ,Denominator
    ,COALESCE(
        CASE WHEN Denominator = 0 THEN 0 ELSE NULL END,
        CASE WHEN Numerator <> 0 THEN Numerator / Denominator ELSE NULL END,
        0
    ) AS TestCalc
FROM Fractions

DROP TABLE Fractions

如果在Denominator = 0时正在评估第二种情况,我希望看到如下错误:

Msg 8134, Level 16, State 1, Line 1
Divide by zero error encountered.

我发现了一些 提到  有关 到Oracle。和一些测试 SQL Server。当您包含用户定义的函数时,看起来短路可能会中断。

那么,这种行为是否应该由ANSI标准保证?


2799
2018-02-03 04:00


起源

高度相关 - Martin Smith
总结一下DBA的答案, SELECT COALESCE(1, (SELECT 1/0)) 运行没有错误,并显示它短路。口译员认为它缩短了 CASE 声明。 - Paul Draper


答案:


我刚看了一下链接的文章,可以确认COALESCE和ISNULL的短路都会失败。

如果您涉及任何子查询,它似乎失败,但它适用于标量函数和硬编码值。

例如,

DECLARE @test INT
SET @test = 1
PRINT 'test2'
SET @test = COALESCE(@test, (SELECT COUNT(*) FROM sysobjects))
SELECT 'test2', @test
-- OUCH, a scan through sysobjects

COALESCE是根据实施的 ANSI标准。它只是CASE声明的简写。 ISNULL不是ANSI标准的一部分。第6.9节似乎没有明确要求短路,但它确实暗示了第一个真正的条款 when 声明应该退回。

这是一些适用于基于标量的函数的证明(我运行它 SQL Server 2005):

CREATE FUNCTION dbo.evil
(
)
RETURNS int
AS
BEGIN
    -- Create an huge delay
    declare @c int
    select @c = count(*) from sysobjects a
    join sysobjects b on 1=1
    join sysobjects c on 1=1
    join sysobjects d on 1=1
    join sysobjects e on 1=1
    join sysobjects f on 1=1
    return @c / 0
END
go

select dbo.evil()
-- takes forever

select ISNULL(1,  dbo.evil())
-- very fast

select COALESCE(1,  dbo.evil())
-- very fast

这里有一些证明,CASE的底层实现将执行子查询。

DECLARE @test INT
SET @test = 1
select
    case
        when @test is not null then @test
        when @test = 2 then (SELECT COUNT(*) FROM sysobjects)
        when 1=0 then (SELECT COUNT(*) FROM sysobjects)
        else (SELECT COUNT(*) FROM sysobjects)
    end
-- OUCH, two table scans. If 1=0, it does not result in a table scan.

8
2018-02-03 04:40



是的,看起来COALESCE完全等同于CASE,并且以同样的方式短路,但是,正如您所示,CASE的行为并不总是短路,这实际上是非常讨厌的。 - Cade Roux
COALESCE在11g中正确地进行了短路(即使是子查询) - Jeffrey Kemp
确实如此 不 即使计划显示2次扫描,也进行2次表扫描。这很容易验证 SET STATISTICS IO ON 或者只是查看执行计划属性中的“执行次数”。那里 是  一个问题 同 COALESCE 这不会发生 ISNULL 虽然。 - Martin Smith


高效 保证MS SQL Server短路的方法是使用CASE。 对于成功的WHEN条款,不评估其他条款。

COALESCE可能有问题

在这种情况下,为什么在COALESCE / CASE结构中有这么多分支?

SELECT Numerator
    ,Denominator
    ,CASE
        WHEN Denominator = 0 THEN 0 END,
        ELSE Numerator / Denominator
     END AS TestCalc
FROM Fractions

3
2018-02-03 05:08



看到我的回答,CASE有一个潜在的问题,流向ISNULL等...... - Sam Saffron
是的,CASE可以做子查询,但我不确定与OP问题的相关性。我已经看到它用作短路但我不喜欢它因为表扫描或增加IO(如你所示) - gbn


我也惊讶地发现答案有效!我不确定这种行为是否得到保证。 (但我找不到一个不起作用的例子!)

五年了 SQL,我仍然感到惊讶。

我也继续前进并做了一个改变:

INSERT INTO #Fractions VALUES (0, 0)

SELECT Numerator
    ,Denominator
    ,coalesce (
        CASE WHEN Denominator = 0 THEN 0 ELSE NULL END,
        CASE WHEN Numerator <> 0 THEN Numerator / Denominator ELSE NULL END)
     AS TestCalc
FROM #Fractions

我得到的结果是:

Numerator   Denominator TestCalc
1             1           1
1             2           0.5
1             3           0.3333333333333335
1             0           0
2             0           0
3             0           0
0             0           0

现在我更加困惑了!对于num = 0和den = 0的情况,我如何将testcalc作为0(特别是因为我在最后一个案例后删除了0)?


1
2018-02-03 04:21



这应该属于第一种情况。十多年的SQL Server,我从未考虑过COALESCE会短路,因为它看起来就像一个函数调用。显然CASE确实如此,并且像COALESCE这样的接缝被定义为与CASE功能相同。 - Cade Roux
我的坏...当然它属于第一种情况。这是我的生命的使命,找到一个不起作用的情况:) - Learning
@Learning,请确保你看看我的扩展答案,它纠正了一些问题。 - Sam Saffron