Search code examples
sqlsql-servert-sqlcoalesceshort-circuiting

COALESCE - guaranteed to short-circuit?


From this question, a neat answer about using COALESCE to simplify complex logic trees. I considered the problem of short circuiting.

For instance, in functions in most languages, arguments are fully evaluated and are then passed into the function. In C:

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

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

That does not appear to be a limitation of the COALESCE "function" in 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

If it were evaluating the second case when Denominator = 0, I would expect to see an error like:

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

I found some mentions related to Oracle. And some tests with SQL Server. Looks like the short-circuiting might break down when you include user-defined functions.

So, is this behavior supposed to be guaranteed by the ANSI standard?


Solution

  • I just had a look at the linked article and can confirm short circuiting can fail for both COALESCE and ISNULL.

    It seems to fail if you have any sub-query involved, but it works fine for scalar functions and hard coded values.

    For example,

    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 is implemented according to the ANSI standard. It is simply a shorthand for a CASE statement. ISNULL is not part of the ANSI standard. Section 6.9 does not seem to require short circuiting explicitly, but it does imply that the first true clause in the when statement should be returned.

    Here is some proof that is works for scalar based functions (I ran it on 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
    

    Here is some proof that the underlying implementation with CASE will execute sub queries.

    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.