Перевірте існування з EXISTS перевершує COUNT! … Ні?


35

Я часто читав, коли треба перевіряти наявність рядка завжди слід робити з EXISTS, а не з COUNT.

Але в кількох останніх сценаріях я вимірював поліпшення продуктивності при використанні підрахунку.
Шаблон виглядає так:

LEFT JOIN (
    SELECT
        someID
        , COUNT(*)
    FROM someTable
    GROUP BY someID
) AS Alias ON (
    Alias.someID = mainTable.ID
)

Я не знайомий з методами розповісти про те, що відбувається "всередині" SQL Server, тому мені було цікаво, чи існує нечувана вада з ІСНУЮЧИМИ, що надає ідеальний сенс вимірюванням, які я робив (чи можуть ІСНОВНІ бути RBAR ?!).

Чи є у вас якесь пояснення цим явищам?

Редагувати:

Ось повний сценарій, який ви можете запустити:

SET NOCOUNT ON
SET STATISTICS IO OFF

DECLARE @tmp1 TABLE (
    ID INT UNIQUE
)


DECLARE @tmp2 TABLE (
    ID INT
    , X INT IDENTITY
    , UNIQUE (ID, X)
)

; WITH T(n) AS (
    SELECT
        ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM master.dbo.spt_values AS S
) 
, tally(n) AS (
    SELECT
        T2.n * 100 + T1.n
    FROM T AS T1
    CROSS JOIN T AS T2
    WHERE T1.n <= 100
    AND T2.n <= 100
)
INSERT @tmp1
SELECT n
FROM tally AS T1
WHERE n < 10000


; WITH T(n) AS (
    SELECT
        ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM master.dbo.spt_values AS S
) 
, tally(n) AS (
    SELECT
        T2.n * 100 + T1.n
    FROM T AS T1
    CROSS JOIN T AS T2
    WHERE T1.n <= 100
    AND T2.n <= 100
)
INSERT @tmp2
SELECT T1.n
FROM tally AS T1
CROSS JOIN T AS T2
WHERE T1.n < 10000
AND T1.n % 3 <> 0
AND T2.n < 1 + T1.n % 15

PRINT '
COUNT Version:
'

WAITFOR DELAY '00:00:01'

SET STATISTICS IO ON
SET STATISTICS TIME ON

SELECT
    T1.ID
    , CASE WHEN n > 0 THEN 1 ELSE 0 END AS DoesExist
FROM @tmp1 AS T1
LEFT JOIN (
    SELECT
        T2.ID
        , COUNT(*) AS n
    FROM @tmp2 AS T2
    GROUP BY T2.ID
) AS T2 ON (
    T2.ID = T1.ID
)
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (RECOMPILE) -- Required since table are filled within the same scope

SET STATISTICS TIME OFF

PRINT '

EXISTS Version:'

WAITFOR DELAY '00:00:01'

SET STATISTICS TIME ON

SELECT
    T1.ID
    , CASE WHEN EXISTS (
        SELECT 1
        FROM @tmp2 AS T2
        WHERE T2.ID = T1.ID
    ) THEN 1 ELSE 0 END AS DoesExist
FROM @tmp1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (RECOMPILE) -- Required since table are filled within the same scope

SET STATISTICS TIME OFF 

На SQL Server 2008R2 (сім 64 біт) я отримую цей результат

COUNT Версія:

Таблиця '# 455F344D'. Кількість сканувань 1, логічне зчитування 8, фізичне зчитування 0, зчитування вперед-зчитування 0, логічне зчитування лобі 0, лобічне фізичне зчитування 0, лобічне зчитування вперед-0.
лобі Таблиця '# 492FC531'. Кількість сканувань 1, логічне зчитування 30, фізичне зчитування 0, зчитування вперед-зчитування 0, логічне зчитування лобі 0, лобічне фізичне зчитування 0, лобічне зчитування попереднє зчитування 0.

Часи виконання SQL Server:
час процесора = 0 мс, минулий час = 81 мс.

EXISTS Версія:

Таблиця '# 492FC531'. Кількість сканувань 1, логічне зчитування 96, фізичне зчитування 0, зчитування вперед читання 0, логічне зчитування
лобі 0, лобічне фізичне зчитування 0, лобічне зчитування лобове читання 0. Таблиця '# 455F344D'. Кількість сканувань 1, логічне зчитування 8, фізичне зчитування 0, зчитування вперед-зчитування 0, логічне зчитування лобі 0, лобічне фізичне зчитування 0, лобічне зчитування попереднє зчитування 0.

Часи виконання SQL Server:
час процесора = 0 мс, минулий час = 76 мс.

Відповіді:


43

Я часто читав, коли треба було перевірити наявність рядка, завжди слід робити з EXISTS, а не з COUNT.

Дуже рідко завжди може бути правдою, особливо якщо це стосується баз даних. Існує будь-яка кількість способів виразити ту саму семантичну в SQL. Якщо є корисне правило, може бути запит, використовуючи найприродніший доступний синтаксис (і, так, це суб'єктивно), і розглядати лише переписування, якщо план запиту чи ефективність, яку ви отримуєте, неприйнятні.

Що я того вартий, я вважаю, що запити про існування найбільш природно виражаються за допомогою EXISTS. Також мій досвід EXISTS має тенденцію до оптимізації краще, ніж альтернатива OUTER JOINвідхилення NULL. Використання COUNT(*)та фільтрація =0- це ще одна альтернатива, яка має певну підтримку в оптимізаторі запитів SQL Server, але я особисто визнав це ненадійним у складніших запитах. У будь-якому випадку, EXISTSмені здається набагато більш природним (ніж для мене), ніж будь-яка з цих альтернатив.

Мені було цікаво, чи існує нечувана вада з ІСНУЮЧИМИ, що надає ідеальний сенс вимірам, які я робив

Ваш конкретний приклад цікавий тим, що він підкреслює те, як оптимізатор обробляє підзапити в CASEвиразах (і EXISTSтестах зокрема).

Підзапити в виразах CASE

Розглянемо наступний (цілком законний) запит:

DECLARE @Base AS TABLE (a integer NULL);
DECLARE @When AS TABLE (b integer NULL);
DECLARE @Then AS TABLE (c integer NULL);
DECLARE @Else AS TABLE (d integer NULL);

SELECT
    CASE
        WHEN (SELECT W.b FROM @When AS W) = 1
            THEN (SELECT T.c FROM @Then AS T)
        ELSE (SELECT E.d FROM @Else AS E)
    END
FROM @Base AS B;

У СемантикаCASE тому , що WHEN/ELSEзастереження , як правило оцінені в текстовому порядку. У вищезазначеному запиті було б неправильним повернення помилки SQL Server, якщо ELSEпідзапит повертав більше ніж одну рядок, якщо WHENумова була задоволена. Для поваги до цієї семантики оптимізатор виробляє план, який використовує предикати прохідних даних:

Прохідні предикати

Внутрішня сторона вкладеного циклу приєднується лише тоді, коли предикат, що проходить, повертається помилково. Загальний ефект полягає в тому, що CASEвирази перевіряються в порядку, а підзапити оцінюються лише в тому випадку, якщо попереднє вираження не було задоволено.

Вирази CASE із запитом EXISTS

Якщо використовується CASEпідзапит EXISTS, тест логічного існування реалізується як напівз'єднання, але рядки, які, як правило, відхиляються напівз'єднанням, повинні бути збережені у випадку необхідності наступного пункту. Рядки, що протікають через цей спеціальний вид напівз'єднання, набувають прапор, щоб вказати, чи знайдено напівз'єднання чи ні. Цей прапор відомий як стовпчастий стовпчик .

Деталі реалізації полягають у тому, що логічний підзапит замінюється співвіднесеним об'єднанням ('застосовувати') зі стовпчиком зонда. Робота виконується за допомогою правила спрощення в оптимізаторі запитів, який називається RemoveSubqInPrj(видалити підзапит в проекції). Ми можемо побачити деталі, використовуючи прапор трас 8606:

SELECT
    T1.ID,
    CASE
        WHEN EXISTS 
        (
            SELECT 1
            FROM #T2 AS T2
            WHERE T2.ID = T1.ID
        ) THEN 1 
    ELSE 0
    END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (QUERYTRACEON 3604, QUERYTRACEON 8606);

Частина дерева вводу, що показує EXISTSтест, показана нижче:

ScaOp_Exists 
    LogOp_Project
        LogOp_Select
            LogOp_Get TBL: #T2
            ScaOp_Comp x_cmpEq
                ScaOp_Identifier [T2].ID
                ScaOp_Identifier [T1].ID

Це перетворюється на RemoveSubqInPrjструктуру, яку очолює:

LogOp_Apply (x_jtLeftSemi probe PROBE:COL: Expr1008)

Це ліве напівприєднання із застосуванням зонда, описаного раніше. Ця початкова трансформація є єдиною на сьогодні доступною в оптимізаторах запитів SQL Server, і компіляція просто не вдасться, якщо це перетворення буде відключено.

Однією з можливих форм плану виконання цього запиту є пряма реалізація цієї логічної структури:

NLJ Semi Приєднуйтесь до зонда

Кінцевий Compute Scalar оцінює результат CASEвираження, використовуючи значення стовпця зонда:

Обчислити скалярний вираз

Основна форма дерева плану зберігається, коли оптимізація враховує інші фізичні типи з'єднання для напівз'єднання. Лише об'єднання об'єднань підтримує стовпчастий зонд, тому хеш-напівз'єднання, хоча й логічно можливо, не враховується:

Об’єднати з зондовою колоною

Зауважте, що злиття видає вираз, позначений міткою Expr1008(що ім'я таке ж, як і раніше, є збігом), хоча жодне визначення для нього не з’являється на жодному операторі в плані. Це знову лише стовпчастий стовпчик. Як і раніше, остаточний Скаляр обчислень використовує це значення зонду для оцінки CASE.

Проблема полягає в тому, що оптимізатор не повністю вивчає альтернативи, які стають корисними лише при об'єднанні (або хеш) напівз'єднання. У вкладеному плані циклів немає переваги перевіряти відповідність рядків T2діапазону на кожній ітерації. З планом злиття або хеш-програми це може бути корисною оптимізацією.

Якщо до запиту додати BETWEENприсудок відповідності T2, все, що трапляється, полягає в тому, що ця перевірка виконується для кожного ряду як залишковий при напівз’єднанні об'єднання (важко помітити в плані виконання, але він є):

SELECT
    T1.ID,
    CASE
        WHEN EXISTS 
        (
            SELECT 1
            FROM #T2 AS T2
            WHERE T2.ID = T1.ID
            AND T2.ID BETWEEN 5000 AND 7000 -- New
        ) THEN 1 
    ELSE 0
    END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;

Залишковий присудок

Ми сподіваємось, що BETWEENприсудок замість цього буде витіснений вниз, що T2призведе до пошуку. Зазвичай оптимізатор вважає за краще це робити (навіть без зайвого предиката в запиті). Він визнає припускаються предикати ( BETWEENна T1і предиката між T1і T2разом означають BETWEENна T2) без їх присутності в початковому тексті запиту. На жаль, шаблон застосування зонду означає, що це не досліджено.

Існують способи написання запиту для створення запитів на обох входах до об'єднання напівз'єднання. Один із способів передбачає написання запиту досить неприродним чином (перемагаючи причину, яку я, як правило, віддаю перевагу EXISTS):

WITH T2 AS
(
    SELECT TOP (9223372036854775807) * 
    FROM #T2 AS T2 
    WHERE ID BETWEEN 5000 AND 7000
)
SELECT 
    T1.ID, 
    DoesExist = 
        CASE 
            WHEN EXISTS 
            (
                SELECT * FROM T2 
                WHERE T2.ID = T1.ID
            ) THEN 1 ELSE 0 END
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;

План ТОП-трюку

Я б не радий писати цей запит у виробничому середовищі, це просто продемонструвати, що потрібна форма плану можлива. Якщо справжній запит, який потрібно писати, використовує саме CASEцей спосіб, а продуктивність страждає тим, що він не шукає з боку зонда напівз'єднання злиття, ви можете розглянути можливість запиту, використовуючи інший синтаксис, який дає правильні результати та більш ефективний план виконання.


6

Аргумент "COUNT (*) проти EXISTS" пов'язаний з перевіркою наявності запису. Наприклад:

WHERE (SELECT COUNT(*) FROM Table WHERE ID=@ID)>0

проти

WHERE EXISTS(SELECT ID FROM Table WHERE ID=@ID)

Ваш скрипт SQL не використовує COUNT(*)як перевірку існування запису, і тому я б не сказав, що його застосовано у вашому сценарії.


Хоча / висновок на основі написаного мною сценарію?
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.