COALESCE - гарантировано короткое замыкание? - PullRequest
11 голосов
/ 03 февраля 2009

С этот вопрос , точный ответ об использовании COALESCE для упрощения сложных логических деревьев. Я рассмотрел проблему короткого замыкания.

Например, в функциях большинства языков аргументы полностью вычисляются и затем передаются в функцию. В С:

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?

Ответы [ 3 ]

8 голосов
/ 03 февраля 2009

Я только что взглянул на связанную статью и могу подтвердить, что короткое замыкание может дать сбой как для 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.
3 голосов
/ 03 февраля 2009

Эффективный способ гарантировать короткое замыкание в 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
1 голос
/ 03 февраля 2009

Я также был удивлен, увидев, что ответ работает! Я не уверен, что такое поведение гарантировано. (Но я не смог найти пример, который не работает!)

Пять лет 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 после последнего случая!)?

...