Это проблема, с которой я периодически сталкиваюсь и пока не нашел хорошего решения.
Предположим, следующая структура таблицы
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
Это может обрабатывать оба столбца одновременно, поэтому наихудший случай - одно полное сканирование. Недостатком является то, что, даже если он встречает a 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) |
+----------+------------+------+---------+----------+----------------------+----------+------------------+
Для ответа @ Thomas я изменился 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
но в сравнении с предыдущими тестовыми данными, что один не дает мне план с Flow Distinct, в то время как 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