Як ефективно перевірити 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

Це може обробити обидва стовпчики одночасно, тому в гіршому випадку буде проведено повне сканування. Недоліком є ​​те, що навіть якщо він зустрінеться 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той (план нижче).

Безкорисний

Відповіді:


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

Як я розумію питання, ви хочете знати, чи існує нуль у будь-якому зі значень стовпців, на відміну від фактичного повернення рядків, у яких B або C є недійсними. Якщо це так, то чому б ні:

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

Якщо додати підказку про блокування, результати будуть ще швидшими:

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 Generator 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.

Ось результати, коли в таблиці немає нуля. Зауважте, що два існуючих рішення, надані ypercube, майже ідентичні моїм з точки зору читання та часу виконання. Я (ми) вважаємо, що це пов'язано з перевагами видання Enterprise / Developer, що використовує розширене сканування . Якщо ви використовували лише стандартну версію або нижчу версію, рішення 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      

4

Тестовано у SQL-Fiddle у версіях: 2008 r2 та 2012 з 30K рядками.

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

Запити та терміни. Терміни, де виконано:

  • 1-й без нуля
  • 2-й з стовпчиком, що Bмає по одному NULLмаленькому id.
  • 3-го, причому обидва стовпчика мають по одному NULLна невеликі ідентифікатори.

Ось ми підемо (є питання з планами, я спробую ще раз пізніше. Перейдіть за посиланнями зараз):


Запит із двома підзапросами 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

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

-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)б повернулися в істину. Цей приклад MySQL вказує на те, що обидва стовпці містять NULL, коли насправді це не так
Мартін Сміт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.