Це питання, з яким я стикаюсь періодично, і поки не знайшов хорошого рішення.
Припустимо наступну структуру таблиці
CREATE TABLE T
(
A INT PRIMARY KEY,
B CHAR(1000) NULL,
C CHAR(1000) NULL
)
і вимога полягає в тому, щоб визначити, чи містить жоден з обнулених стовпців B
чи C
насправді якісь NULL
значення (і якщо так, то який (и)).
Припустимо також, що таблиця містить мільйони рядків (і що немає статистичних даних стовпців, на які можна було б зазирнути, оскільки мене цікавить більш загальне рішення для цього класу запитів).
Я можу придумати декілька способів підходу до цього, але всі мають слабкі сторони.
Дві окремі EXISTS
заяви. Це матиме перевагу в тому, щоб дозволити запитам припинити сканування рано, як тільки NULL
буде знайдено а. Але якщо обидва стовпці насправді не містять NULL
s, то вийде два повних сканування.
Одиничний сукупний запит
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