Це питання, з яким я стикаюсь періодично, і поки не знайшов хорошого рішення.
Припустимо наступну структуру таблиці
CREATE TABLE T
(
A INT PRIMARY KEY,
B CHAR(1000) NULL,
C CHAR(1000) NULL
)
і вимога полягає в тому, щоб визначити, чи містить жоден з обнулених стовпців Bчи Cнасправді якісь NULLзначення (і якщо так, то який (и)).
Припустимо також, що таблиця містить мільйони рядків (і що немає статистичних даних стовпців, на які можна було б зазирнути, оскільки мене цікавить більш загальне рішення для цього класу запитів).
Я можу придумати декілька способів підходу до цього, але всі мають слабкі сторони.
Дві окремі EXISTSзаяви. Це матиме перевагу в тому, щоб дозволити запитам припинити сканування рано, як тільки NULLбуде знайдено а. Але якщо обидва стовпці насправді не містять NULLs, то вийде два повних сканування.
Одиничний сукупний запит
SELECT
MAX(CASE WHEN B IS NULL THEN 1 ELSE 0 END) AS B,
MAX(CASE WHEN C IS NULL THEN 1 ELSE 0 END) AS C
FROM T
Це може обробити обидва стовпчики одночасно, тому в гіршому випадку буде проведено повне сканування. Недоліком є те, що навіть якщо він зустрінеться NULLв обох стовпцях дуже рано, запит все одно закінчить сканування всієї частини таблиці.
Користувацькі змінні
Я можу придумати третій спосіб зробити це
BEGIN TRY
DECLARE @B INT, @C INT, @D INT
SELECT
@B = CASE WHEN B IS NULL THEN 1 ELSE @B END,
@C = CASE WHEN C IS NULL THEN 1 ELSE @C END,
/*Divide by zero error if both @B and @C are 1.
Might happen next row as no guarantee of order of
assignments*/
@D = 1 / (2 - (@B + @C))
FROM T
OPTION (MAXDOP 1)
END TRY
BEGIN CATCH
IF ERROR_NUMBER() = 8134 /*Divide by zero*/
BEGIN
SELECT 'B,C both contain NULLs'
RETURN;
END
ELSE
RETURN;
END CATCH
SELECT ISNULL(@B,0),
ISNULL(@C,0)
але це не підходить для виробничого коду, оскільки правильна поведінка для сукупного запиту конкатенації не визначена. і припинення сканування шляхом викидання помилки - це все одно жахливе рішення.
Чи є інший варіант, який поєднує в собі сильні сторони вищезазначених підходів?
Редагувати
Просто щоб оновити це з результатами, які я отримую з точки зору прочитаних відповідей, поданих до цього часу (використовуючи тестові дані @ ypercube)
+----------+------------+------+---------+----------+----------------------+----------+------------------+
| | 2 * EXISTS | CASE | Kejser | Kejser | Kejser | ypercube | 8kb |
+----------+------------+------+---------+----------+----------------------+----------+------------------+
| | | | | MAXDOP 1 | HASH GROUP, MAXDOP 1 | | |
| No Nulls | 15208 | 7604 | 8343 | 7604 | 7604 | 15208 | 8346 (8343+3) |
| One Null | 7613 | 7604 | 8343 | 7604 | 7604 | 7620 | 7630 (25+7602+3) |
| Two Null | 23 | 7604 | 8343 | 7604 | 7604 | 30 | 30 (18+12) |
+----------+------------+------+---------+----------+----------------------+----------+------------------+
Для @ відповідь Томаса я змінив TOP 3на TOP 2потенційно дозволити йому вийти раніше. Я отримав паралельний план за замовчуванням на цю відповідь, тому також спробував його з MAXDOP 1підказкою, щоб зробити кількість прочитаних більш порівнянною з іншими планами. Я був дещо здивований результатами, оскільки в попередньому тесті я бачив це запит короткого замикання, не читаючи всієї таблиці.
План моїх тестових даних про коротке замикання нижче

План даних ypercube є

Тож це додає в план оператора блокування сортування. Я також спробував із HASH GROUPпідказкою, але це все ще закінчується читанням усіх рядків

Отже, ключовим моментом є отримання hash match (flow distinct)оператором дозволу цього плану короткого замикання, оскільки інші альтернативи все одно заблокують і споживають усі рядки. Я не думаю, що є натяк на це спеціально, але, мабуть, "загалом, оптимізатор вибирає Flow Distinct, де він визначає, що потрібно менше рядків виводу, ніж є різні значення у наборі вводу". .
Дані @ ypercube містять лише 1 рядок у кожному стовпці зі NULLзначеннями (кардинальність таблиці = 30300), і передбачувані рядки, що входять і виходять з оператора, обидва 1. Зробивши присудок більш непрозорим для оптимізатора, він створив план з оператором Flow Distinct.
SELECT TOP 2 *
FROM (SELECT DISTINCT
CASE WHEN b IS NULL THEN NULL ELSE 'foo' END AS b
, CASE WHEN c IS NULL THEN NULL ELSE 'bar' END AS c
FROM test T
WHERE LEFT(b,1) + LEFT(c,1) IS NULL
) AS DT
Редагувати 2
Остання остання помилка, яка мені трапилася, - це те, що вищезазначений запит може все-таки обробити більше рядків, ніж потрібно, якщо перший рядок, з яким він стикається, NULLмає NULL в обох стовпцях Bі C. Сканування буде продовжуватись, а не виходити негайно. Одним із способів уникнути цього було б відкручування рядків під час їх сканування. Отже, моя остаточна поправка на відповідь Томаса Кейзера наведена нижче
SELECT DISTINCT TOP 2 NullExists
FROM test T
CROSS APPLY (VALUES(CASE WHEN b IS NULL THEN 'b' END),
(CASE WHEN c IS NULL THEN 'c' END)) V(NullExists)
WHERE NullExists IS NOT NULL
Можливо, для предиката було б краще бути, WHERE (b IS NULL OR c IS NULL) AND NullExists IS NOT NULLале проти попередніх даних тесту, що ніхто не дає мені плану з розрізненням потоку, тоді як NullExists IS NOT NULLтой (план нижче).

TOP 3може бути просто ,TOP 2як в даний час вона буде сканувати до тих пір, поки не знайде кожен з наступних(NOT_NULL,NULL),(NULL,NOT_NULL),(NULL,NULL). Будь-яких 2 з цих 3 було б достатньо - і якщо він знайде(NULL,NULL)перше, то і другий теж не знадобиться. Крім того , до короткого замикання план необхідно реалізувати чіткий черезhash match (flow distinct)оператора , а неhash match (aggregate)чиdistinct sort