Читает ли SQL Server всю функцию COALESCE, даже если первый аргумент не равен NULL?

98

Я использую функцию T-SQL, COALESCEгде первый аргумент не будет нулевым в 95% случаев, когда он запускается. Если первый аргумент NULL, второй аргумент довольно длительный процесс:

SELECT COALESCE(c.FirstName
                ,(SELECT TOP 1 b.FirstName
                  FROM TableA a 
                  JOIN TableB b ON .....)
                )

Если, например, c.FirstName = 'John'SQL Server все еще будет выполнять подзапрос?

Я знаю с помощью IIF()функции VB.NET , если второй аргумент равен True, код все равно читает третий аргумент (даже если он не будет использоваться).

односложный
источник

Ответы:

95

Неа . Вот простой тест:

SELECT COALESCE(1, (SELECT 1/0)) -- runs fine
SELECT COALESCE(NULL, (SELECT 1/0)) -- throws error

Если оценивается второе условие, для деления на ноль выдается исключение.

Согласно документации MSDN, это связано с тем COALESCE, как переводчик смотрит на это - это просто простой способ написать CASEзаявление.

CASE Хорошо известно, что это одна из немногих функций в SQL Server, которая (в основном) надежно замыкает цепи.

Есть некоторые исключения при сравнении со скалярными переменными и агрегациями, как показано Аароном Бертраном в другом ответе здесь (и это относится как к, так CASEи к COALESCE):

DECLARE @i INT = 1;
SELECT CASE WHEN @i = 1 THEN 1 ELSE MIN(1/0) END;

сгенерирует деление на ноль ошибок.

Это следует считать ошибкой и, как правило, COALESCEбудет разбираться слева направо.

JNK
источник
6
@JNK, пожалуйста, смотрите мой ответ, чтобы увидеть очень простой случай, когда это не выполняется (меня беспокоит то, что есть еще больше, но еще не обнаруженных сценариев - затрудняющих согласие, что CASEвсегда оценивает слева направо и всегда короткие замыкания ).
Аарон Бертран
4
Другое интересное поведение @SQLKiwi указало мне на: SELECT COALESCE((SELECT CASE WHEN RAND() <= 0.5 THEN 1 END), 1);- повторить несколько раз. Вы получите NULLиногда. Попробуйте еще раз с ISNULL- вы никогда не получите NULL...
Аарон Бертран
@ Мартин да, я верю в это. Но не поведение, которое большинство пользователей сочли бы интуитивным, если бы они не слышали (или не были укушены) этой проблемы.
Аарон Бертран
73

Как насчет этого - как мне сообщил Ицик Бен-Ган, которому об этом рассказал Хайме Лафарг ?

DECLARE @i INT = 1;
SELECT CASE WHEN @i = 1 THEN 1 ELSE MIN(1/0) END;

Результат:

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

Конечно, существуют тривиальные обходные пути, но суть в том, что CASEне всегда гарантируется оценка / короткое замыкание слева направо. Я сообщил об ошибке здесь, и она была закрыта как «по замыслу». Впоследствии Пол Уайт подал этот элемент подключения , и он был закрыт как исправленный. Не потому, что это было исправлено само по себе, а потому, что они обновили Books Online с более точным описанием сценария, в котором агрегаты могут изменять порядок оценки CASEвыражения. Я недавно написал больше об этом здесь .

РЕДАКТИРОВАТЬ только приложение, хотя я согласен, что это крайние случаи, что большую часть времени вы можете полагаться на оценку слева-направо и короткие замыкания, и что это ошибки, которые противоречат документации и, вероятно, в конечном итоге будут исправлены ( это не определенно - посмотрите последующий разговор в блоге Барта Дункана, чтобы понять, почему), я должен не согласиться, когда люди говорят, что что-то всегда верно, даже если есть один крайний случай, который опровергает это. Если Ицик и другие могут найти такие одиночные ошибки, как это, то, по крайней мере, существует вероятность того, что есть и другие ошибки. А поскольку мы не знаем остальной части запроса ОП, мы не можем с уверенностью сказать, что он будет полагаться на это короткое замыкание, но в конечном итоге его укусит. Поэтому для меня более безопасный ответ:

Хотя обычно вы можете рассчитывать на CASEоценку слева направо и на наличие короткого замыкания, как описано в документации, неверно говорить, что вы всегда можете это сделать. На этой странице есть два продемонстрированных случая, где это не так, и ни одна ошибка не была исправлена ​​ни в одной общедоступной версии SQL Server.

EDIT - это еще один случай (мне нужно прекратить это делать), когда CASEвыражение оценивается не в том порядке, в котором вы ожидаете, даже если не используются никакие агрегаты.

Аарон Бертран
источник
2
И выглядит так, как будто была еще одна проблема, CASE которая была незаметно исправлена
Martin Smith
IMO это не доказывает, что оценка выражения CASE не гарантируется, потому что агрегированные значения вычисляются до выбора (чтобы их можно было использовать внутри имеющего).
Салман А
1
@SalmanA Я не уверен, что еще это возможно, кроме как доказать, что порядок вычисления в выражении CASE не гарантируется. Мы получаем исключение, потому что агрегат вычисляется в первую очередь, даже если это в предложении ELSE, которое - если вы придерживаетесь документации - никогда не должно быть достигнуто.
Аарон Бертран
Агрегаты @AaronBertrand рассчитываются перед оператором CASE (и они должны быть IMO). Пересмотренная документация указывает именно на это, что ошибка происходит до того, как CASE будет оценен.
Салман А
@SalmanA Все еще демонстрируется случайному разработчику, что выражение CASE не оценивается в том порядке, в котором оно было написано - лежащие в основе механики не имеют значения, если все, что вы пытаетесь сделать, это понять, почему возникает ошибка из ветви CASE, которая не должна т был достигнут. У вас есть аргументы против всех других примеров на этой странице?
Аарон Бертран
37

Мое мнение об этом заключается в том, что документация достаточно ясно показывает, что намерение состоит в том, что CASE должен закорачиваться. Как упоминает Аарон, было несколько случаев (ха!), Где было показано, что это не всегда верно.

До сих пор все они были признаны ошибками и исправлены - хотя не обязательно в версии SQL Server, которую вы можете купить и исправить сегодня (ошибка постоянного сворачивания еще не дошла до накопительного обновления AFAIK). Новейшая потенциальная ошибка - первоначально сообщенная Ициком Бен-Ганом - еще не исследована (либо Аарон, либо я добавлю ее в ближайшее время).

Относительно исходного вопроса, есть другие проблемы с CASE (и, следовательно, COALESCE), где используются побочные функции или подзапросы. Рассмотреть возможность:

SELECT COALESCE((SELECT CASE WHEN RAND() <= 0.5 THEN 999 END), 999);
SELECT ISNULL((SELECT CASE WHEN RAND() <= 0.5 THEN 999 END), 999);

Форма COALESCE часто возвращает NULL, более подробную информацию можно найти по адресу https://connect.microsoft.com/SQLServer/feedback/details/546437/coalesce-subquery-1-may-return-null.

Продемонстрированные проблемы с преобразованиями оптимизатора и отслеживанием общего выражения означают, что невозможно гарантировать, что CASE будет иметь короткое замыкание при любых обстоятельствах. Я могу представить себе случаи, когда было бы невозможно даже предсказать поведение путем проверки выходных данных плана публичного шоу, хотя у меня сегодня нет на это информации.

Таким образом, я думаю, что вы можете быть достаточно уверены в том, что CASE в целом будет иметь короткое замыкание (особенно если достаточно квалифицированный специалист проверяет план выполнения, и этот план выполнения «исполняется» с помощью руководства или подсказок плана), но если вам нужно абсолютная гарантия, вы должны написать SQL, который вообще не содержит выражения.

Полагаю, не совсем удовлетворительное положение дел.

Пол Уайт
источник
18

Я сталкивался с другим случаем, когда CASE/ COALESCEне замыкается. Следующий TVF вызовет нарушение PK, если передан 1в качестве параметра.

CREATE FUNCTION F (@P INT)
RETURNS @T TABLE (
  C INT PRIMARY KEY)
AS
  BEGIN
      INSERT INTO @T
      VALUES      (1),
                  (@P)

      RETURN
  END

Если вызывается следующим образом

DECLARE @Number INT = 1

SELECT COALESCE(@Number, (SELECT number
                          FROM   master..spt_values
                          WHERE  type = 'P'
                                 AND number = @Number), 
                         (SELECT TOP (1)  C
                          FROM   F(@Number))) 

Или как

DECLARE @Number INT = 1

SELECT CASE
         WHEN @Number = 1 THEN @Number
         ELSE (SELECT TOP (1) C
               FROM   F(@Number))
       END 

Оба дают результат

Нарушение ограничения PRIMARY KEY 'PK__F__3BD019A800551192'. Невозможно вставить повторяющийся ключ в объект 'dbo. @ T'. Значение дубликата ключа (1).

показывая, что SELECT(или, по крайней мере, заполнение табличной переменной) все еще выполняется и выдает ошибку, даже если эта ветвь оператора никогда не должна быть достигнута. План для COALESCEверсии ниже.

Строить планы

Это переписывание запроса, кажется, чтобы избежать проблемы

SELECT COALESCE(Number, (SELECT number
                          FROM   master..spt_values
                          WHERE  type = 'P'
                                 AND number = Number), 
                         (SELECT TOP (1)  C
                          FROM   F(Number))) 
FROM (VALUES(1)) V(Number)   

Который дает план

Plan2

Мартин Смит
источник
8

Другой пример

CREATE TABLE T1 (C INT PRIMARY KEY)

CREATE TABLE T2 (C INT PRIMARY KEY)

INSERT INTO T1 
OUTPUT inserted.* INTO T2
VALUES (1),(2),(3);

Запрос

SET STATISTICS IO ON;

SELECT T1.C,
       COALESCE(T1.C , CASE WHEN EXISTS (SELECT * FROM T2 WHERE T2.C = T1.C)  THEN -1 END)
FROM T1
OPTION (LOOP JOIN)

Показывает не читает против T2вообще.

Поиск T2выполняется по предикату, и оператор никогда не выполняется. Но

SELECT T1.C,
       COALESCE(T1.C , CASE WHEN EXISTS (SELECT * FROM T2 WHERE T2.C = T1.C)  THEN -1 END)
FROM T1
OPTION (MERGE JOIN)

Есть ли шоу , которое T2читается. Несмотря на то, что ценность от T2никогда не нужна.

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

Мартин Смит
источник
7

Я просто хотел упомянуть стратегию, которую вы, возможно, не рассматривали. Это может быть не совпадение здесь, но иногда бывает полезно. Посмотрите, дает ли эта модификация лучшую производительность:

SELECT COALESCE(c.FirstName
            ,(SELECT TOP 1 b.FirstName
              FROM TableA a 
              JOIN TableB b ON .....
              WHERE C.FirstName IS NULL) -- this is the changed part
            )

Другой способ сделать это может быть следующим (в основном эквивалентно, но позволяет вам при необходимости получить доступ к большему количеству столбцов из другого запроса):

SELECT COALESCE(c.FirstName, x.FirstName)
FROM
   TableC c
   OUTER APPLY (
      SELECT TOP 1 b.FirstName
      FROM
         TableA a 
         JOIN TableB b ON ...
      WHERE
         c.FirstName IS NULL -- the important part
   ) x

По сути, это метод «жесткого» объединения таблиц, но включающий условие, когда нужно присоединять любые строки. По моему опыту, это иногда помогало в выполнении планов.

ErikE
источник
3

Нет, не будет. Это будет работать только тогда, когда c.FirstNameесть NULL.

Тем не менее, вы должны попробовать это сами. Эксперимент. Вы сказали, что ваш подзапрос длинный. Benchmark. Сделайте свои собственные выводы по этому вопросу.

@ Аарон ответ на выполняемый подзапрос является более полным.

Тем не менее, я все еще думаю, что вы должны переработать свой запрос и использовать LEFT JOIN. В большинстве случаев подзапросы можно удалить, переработав запрос для использования LEFT JOINs.

Проблема с использованием подзапросов заключается в том, что ваш общий оператор будет выполняться медленнее, потому что подзапрос выполняется для каждой строки в наборе результатов основного запроса.

Адриан
источник
@ Адриан, это все еще не правильно. Посмотрите на план выполнения, и вы увидите, что подзапросы часто довольно умно преобразуются в JOIN. Это просто ошибка эксперимента с мыслью о том, что весь подзапрос должен выполняться снова и снова для каждой строки, хотя это может эффективно произойти, если выбрано соединение вложенного цикла со сканированием.
ErikE
3

В действующем стандарте говорится, что все предложения WHEN (а также предложение ELSE) должны быть проанализированы для определения типа данных выражения в целом. Я действительно должен был бы получить некоторые из моих старых заметок, чтобы определить, как обрабатывается ошибка. Но 1/0 использует целые числа, поэтому я предполагаю, что пока это ошибка. Это ошибка с целочисленным типом данных. Когда в списке слияния есть только нули, определить тип данных немного сложнее, и это еще одна проблема.

Джо Селько
источник