Чи читає 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буде розбиратися зліва направо.


6
@JNK, будь ласка, подивіться мою відповідь, щоб побачити дуже простий випадок, коли це не відповідає дійсності (я хвилююся, що існує ще більше, ще нерозкритих сценаріїв - важко погодитись, що CASEзавжди оцінюється зліва направо і завжди коротке замикання ).
Аарон Бертран

4
Інша цікава поведінка @SQLKiwi вказала на мене: SELECT COALESCE((SELECT CASE WHEN RAND() <= 0.5 THEN 1 END), 1);- повторити кілька разів. Ви потрапите NULLіноді. Спробуйте ще раз ISNULL- ніколи не отримаєте NULL...
Аарон Бертран


@Martin так, я вважаю, що так. Але не поведінка, яку більшість користувачів вважають інтуїтивно зрозумілою, якщо б вони не чули про (або покусали) це питання.
Аарон Бертран

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це не завжди гарантує оцінку / коротке замикання зліва направо. Я повідомив про помилку тут, і це було закрито як "задумом". Згодом Пол Уайт подав цей елемент Connect , і він був закритий як Виправлений. Не тому, що воно було зафіксовано як таке, а тому, що вони оновлювали Книги онлайн з більш точним описом сценарію, коли агрегати можуть змінювати порядок оцінювання CASEвиразу. Нещодавно я більше про це розповідав тут .

EDIT - це лише доповнення, але я погоджуюся, що це кращі випадки, що більшу частину часу ви можете покластися на оцінку зліва направо і коротке замикання, і що це помилки, які суперечать документації і, ймовірно, з часом будуть виправлені ( це не визначено - див. подальшу розмову в публікації блогу Барта Данкана, щоб дізнатися, чому), я повинен не погодитися, коли люди кажуть, що щось завжди правдиво, навіть якщо є один крайній випадок, який спростує це. Якщо Іцік та інші можуть знайти одиночні помилки, як це, це робить принаймні в царині можливості, що є й інші помилки. І оскільки ми не знаємо решти запитів ОП, ми не можемо сказати напевно, що він буде покладатися на це коротке замикання, але в кінцевому підсумку його вкусить. Тож для мене більш безпечна відповідь:

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

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


2
І виглядає так, ніби з CASE тихою
Мартін Сміт

IMO це не доводить, що оцінка вираження CASE не гарантована, оскільки сукупні значення обчислюються перед вибором (щоб їх можна було використовувати всередині).
Салман А

1
@SalmanA Я не впевнений, що ще це можливо, крім того, щоб довести саме те, що порядок оцінки в виразі CASE не гарантується. Ми отримуємо виняток, оскільки сукупність обчислюється спочатку, навіть якщо це в пункті ELSE, який - якщо ви переходите до документації - ніколи не повинен бути досягнутий.
Аарон Бертран

Агрегати @AaronBertrand обчислюються перед оператором CASE (і вони повинні IMO). Переглянута документація вказує саме на це, що помилка виникає перед оцінкою CASE.
Салман А

@SalmanA Він все ще демонструє випадковому розробнику, що вираз CASE не оцінює в тому порядку, яким він був написаний - основна механіка не має значення, якщо все, що ви намагаєтесь зробити, це зрозуміти, чому помилка надходить з гілки CASE, яка не повинна ' t були досягнуті. Чи є у вас аргументи і проти всіх інших прикладів на цій сторінці?
Аарон Бертран

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 призведе до порушення ПК, якщо його передано 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 

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

Порушення ПЕРВИННОГО КЛЮЧОВОГО обмеження "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)   

Що дає план

План2


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

В основному це техніка "жорсткого" з'єднання таблиць, але включаючи умову, коли будь-які рядки взагалі повинні бути з'єднані. На мій досвід, це справді допомагало планам виконання часом.


3

Ні, не буде. Він би запускався лише тоді, коли c.FirstNameє NULL.

Однак ви повинні спробувати самі. Експеримент. Ви сказали, що ваш запит тривалий. Орієнтир. Зробіть на цьому свої власні висновки.

@Aaron відповідь на запущений підзапит є більш повним.

Однак я все-таки думаю, що вам слід переробити запит та використовувати LEFT JOIN. Більшу частину часу підзапити можна видалити, переробивши запит на використання LEFT JOINs.

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


@Adrian це все ще не так. Подивіться на план виконання, і ви побачите, що підзапити часто досить спритно перетворюються в JOIN. Припускати, що весь підзапит потрібно перезапускати для кожного рядка - це просто помилка експерименту, хоча це може ефективно статися, якщо обрано вкладений цикл зі скануванням.
ErikE

3

Фактичний стандарт говорить, що всі пропозиції WHEN (а також пункт ELSE) повинні бути проаналізовані для визначення типу даних виразу в цілому. Мені б дійсно довелося вийти із старих записок, щоб визначити, як обробляється помилка. Але просто з рук, 1/0 використовує цілі числа, тому я б припустив, що хоча це помилка. Це помилка з цілим типом даних. Коли у списку злиття є лише нулі, визначити тип даних трохи складніше, і це ще одна проблема.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.