Как эффективно проверить EXISTS на нескольких столбцах?

26

Это проблема, с которой я периодически сталкиваюсь и пока не нашел хорошего решения.

Предположим, следующая структура таблицы

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

Это может обрабатывать оба столбца одновременно, поэтому наихудший случай - одно полное сканирование. Недостатком является то, что, даже если он встречает 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тот дает (план ниже).

Unpivoted

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

Ответы:

20

Как насчет:

SELECT TOP 3 *
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 T 
  WHERE 
    (B IS NULL AND C IS NOT NULL) 
    OR (B IS NOT NULL AND C IS NULL) 
    OR (B IS NULL AND C IS NULL)
) AS DT
Томас Кейсер
источник
Мне нравится этот подход. Тем не менее, есть несколько возможных проблем, которые я решаю в своих изменениях. Как написано 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
Мартин Смит
6

Как я понимаю вопрос, вы хотите знать, существует ли значение NULL в каком-либо из значений столбцов, а не возвращать строки, в которых B или C имеют значение NULL. Если это так, то почему бы и нет:

Select Top 1 'B as nulls' As Col
From T
Where T.B Is Null
Union All
Select Top 1 'C as nulls'
From T
Where T.C Is Null

На моем тестовом стенде с SQL 2008 R2 и миллионом строк я получил следующие результаты в мс на вкладке Статистика клиента:

Kejser                          2907,2875,2829,3576,3103
ypercube                        2454,1738,1743,1765,2305
OP single aggregate solution    (stopped after 120,000 ms) Wouldn't even finish
My solution                     1619,1564,1665,1675,1674

Если вы добавите подсказку nolock, результаты будут еще быстрее:

Select Top 1 'B as nulls' As Col
From T With(Nolock)
Where T.B Is Null
Union All
Select Top 1 'C as nulls'
From T With(Nolock)
Where T.C Is Null

My solution (with nolock)       42,70,94,138,120

Для справки я использовал SQL-генератор Red-gate для генерации данных. Из моего миллиона строк, 9,886 строк имели нулевое значение B, а 10,019 имели нулевое значение C.

В этой серии тестов каждая строка в столбце B имеет значение:

Kejser                          245200  Scan count 1, logical reads 367259, physical reads 858, read-ahead reads 367278
                                250540  Scan count 1, logical reads 367259, physical reads 860, read-ahead reads 367280

ypercube(1)                     249137  Scan count 2, logical reads 367276, physical reads 850, read-ahead reads 367278
                                248276  Scan count 2, logical reads 367276, physical reads 869, read-ahead reads 368765

My solution                     250348  Scan count 2, logical reads 367276, physical reads 858, read-ahead reads 367278
                                250327  Scan count 2, logical reads 367276, physical reads 854, read-ahead reads 367278

Перед каждым тестом (оба сета) я бегал CHECKPOINTи DBCC DROPCLEANBUFFERS.

Вот результаты, когда в таблице нет нулей. Обратите внимание, что решение 2 существует, предоставляемое ypercube, практически идентично моему с точки зрения чтения и времени выполнения. Я (мы) полагаю, что это связано с преимуществами редакции Enterprise / Developer, использующей Advanced Scanning . Если вы использовали только стандартную версию или ниже, решение Kejser вполне может быть самым быстрым решением.

Kejser                          248875  Scan count 1, logical reads 367259, physical reads 860, read-ahead reads 367290

ypercube(1)                     243349  Scan count 2, logical reads 367265, physical reads 851, read-ahead reads 367278
                                242729  Scan count 2, logical reads 367265, physical reads 858, read-ahead reads 367276
                                242531  Scan count 2, logical reads 367265, physical reads 855, read-ahead reads 367278

My solution                     243094  Scan count 2, logical reads 367265, physical reads 857, read-ahead reads 367278
                                243444  Scan count 2, logical reads 367265, physical reads 857, read-ahead reads 367278
Томас
источник
4

IFРазрешены ли заявления?

Это должно позволить вам подтвердить существование B или C за один проход через таблицу:

DECLARE 
  @A INT, 
  @B CHAR(10), 
  @C CHAR(10)

SET @B = 'X'
SET @C = 'X'

SELECT TOP 1 
  @A = A, 
  @B = B, 
  @C = C
FROM T 
WHERE B IS NULL OR C IS NULL 

IF @@ROWCOUNT = 0 
BEGIN 
  SELECT 'No nulls'
  RETURN
END

IF @B IS NULL AND @C IS NULL
BEGIN
  SELECT 'Both null'
  RETURN
END 

IF @B IS NULL 
BEGIN
  SELECT TOP 1 
    @C = C
  FROM T
  WHERE A > @A
  AND C IS NULL

  IF @B IS NULL AND @C IS NULL 
  BEGIN
    SELECT 'Both null'
    RETURN
  END
  ELSE
  BEGIN
    SELECT 'B is null'
    RETURN
  END
END

IF @C IS NULL 
BEGIN
  SELECT TOP 1 
    @B = B
  FROM T 
  WHERE A > @A
  AND B IS NULL

  IF @C IS NULL AND @B IS NULL
  BEGIN
    SELECT 'Both null'
    RETURN
  END
  ELSE
  BEGIN
    SELECT 'C is null'
    RETURN
  END
END      
8kb
источник
4

Протестировано в SQL-Fiddle в версиях: 2008 R2 и 2012 с 30K строк.

  • На EXISTSзапрос показывает огромное преимущество в эффективности , когда он находит NULLS рано - который , как ожидается.
  • Я получаю лучшую производительность с EXISTSзапросом - во всех случаях в 2012 году, что я не могу объяснить.
  • В 2008R2, когда нет нулей, это медленнее, чем в двух других запросах. Чем раньше он находит нулевые значения, тем быстрее он получается, и когда оба столбца имеют нулевые ранние значения, это происходит намного быстрее, чем в двух других запросах.
  • Запрос Томаса Кейзера, кажется, работает немного, но постоянно лучше в 2012 году и хуже в 2008R2, по сравнению с CASEзапросом Мартина .
  • Версия 2012 года, кажется, имеет гораздо лучшую производительность. Возможно, это связано с настройками серверов SQL-Fiddle, а не только с улучшениями в оптимизаторе.

Запросы и сроки. Сроки, где сделано:

  • 1-й без нулей вообще
  • 2-й с колонкой, Bимеющей один NULLв малом id.
  • 3-й с обоими столбцами, имеющими по одному NULLна малых идентификаторах.

Здесь мы идем (есть проблема с планами, я попробую позже. Перейдите по ссылкам на данный момент):


Запрос с 2 подзапросами EXISTS

SELECT 
      CASE WHEN EXISTS (SELECT * FROM test WHERE b IS NULL)
             THEN 1 ELSE 0 
      END AS B,
      CASE WHEN EXISTS (SELECT * FROM test WHERE c IS NULL)
             THEN 1 ELSE 0 
      END AS C ;

-------------------------------------
Times in ms (2008R2): 1344 - 596 -  1  
Times in ms   (2012):   26 -  14 -  2

Единый общий запрос Мартина Смита

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 test ;

--------------------------------------
Times in ms (2008R2):  558 - 553 - 516  
Times in ms   (2012):   37 -  35 -  36

Запрос Томаса Кейзера

SELECT TOP 3 *
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 
    (B IS NULL AND C IS NOT NULL) 
    OR (B IS NOT NULL AND C IS NULL) 
    OR (B IS NULL AND C IS NULL)
) AS DT ;

--------------------------------------
Times in ms (2008R2):  859 - 705 - 668  
Times in ms   (2012):   24 -  19 -  18

Мое предложение (1)

WITH tmp1 AS
  ( SELECT TOP (1) 
        id, b, c
    FROM test
    WHERE b IS NULL OR c IS NULL
    ORDER BY id 
  ) 

  SELECT 
      tmp1.*, 
      NULL AS id2, NULL AS b2, NULL AS c2
  FROM tmp1
UNION ALL
  SELECT *
  FROM
    ( SELECT TOP (1)
          tmp1.id, tmp1.b, tmp1.c,
          test.id AS id2, test.b AS b2, test.c AS c2 
      FROM test
        CROSS JOIN tmp1
      WHERE test.id >= tmp1.id
        AND ( test.b IS NULL AND tmp1.c IS NULL
           OR tmp1.b IS NULL AND test.c IS NULL
            )
      ORDER BY test.id
    ) AS x ;

--------------------------------------
Times in ms (2008R2): 1089 - 572 -  16   
Times in ms   (2012):   28 -  15 -   1

Это требует некоторой полировки на выходе, но эффективность аналогична EXISTSзапросу. Я думал, что будет лучше, когда нет нулей, но тестирование показывает, что это не так.


Предложение (2)

Попытка упростить логику:

CREATE TABLE tmp
( id INT
, b CHAR(1000)
, c CHAR(1000)
) ;

DELETE  FROM tmp ;

INSERT INTO tmp 
    SELECT TOP (1) 
        id, b, c
    FROM test
    WHERE b IS NULL OR c IS NULL
    ORDER BY id  ; 

INSERT INTO tmp 
    SELECT TOP (1)
        test.id, test.b, test.c 
      FROM test
        JOIN tmp 
          ON test.id >= tmp.id
      WHERE ( test.b IS NULL AND tmp.c IS NULL
           OR tmp.b IS NULL AND test.c IS NULL
            )
      ORDER BY test.id ;

SELECT *
FROM tmp ;

Похоже, что в 2008R2 он работает лучше, чем в предыдущем предложении, но хуже в 2012 году (возможно, второе INSERTможно переписать, используя IFответ @ 8kb):

------------------------------------------
Times in ms (2008R2): 416+6 - 1+127 -  1+1   
Times in ms   (2012):  14+1 - 0+27  -  0+29
ypercubeᵀᴹ
источник
0

Когда вы используете EXISTS, SQL Server знает, что вы делаете проверку существования. Когда он находит первое совпадающее значение, он возвращает ИСТИНА и прекращает поиск.

когда вы объединяете 2 столбца, и если любой равен нулю, результат будет нулевым

например

null + 'a' = null

так проверьте этот код

IF EXISTS (SELECT 1 FROM T WHERE B+C is null)
SELECT Top 1 ISNULL(B,'B ') + ISNULL(C,'C') as [Nullcolumn] FROM T WHERE B+C is null
AmmarR
источник
-3

Как насчет:

select 
    exists(T.B is null) as 'B is null',
    exists(T.C is null) as 'C is null'
from T;

Если это сработает (я не проверял), то получится таблица из одной строки с 2 столбцами, каждый из которых имеет значение ИСТИНА или ЛОЖЬ. Я не проверял эффективность.

Дэвид Горовиц
источник
2
Даже если это допустимо в любой другой СУБД, я сомневаюсь, что она имеет правильную семантику. Предполагая, что T.B is nullэто обрабатывается как логический результат, EXISTS(SELECT true)и EXISTS(SELECT false)оба возвращают true. Этот пример MySQL указывает на то, что оба столбца содержат NULL, хотя ни один из них не является фактически
Martin Smith