Проверьте существование с EXISTS превзойти COUNT! ... нет?


36

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

Тем не менее, в нескольких недавних сценариях я измерил улучшение производительности при использовании счетчика.
Шаблон выглядит так:

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

Я не знаком с методами, позволяющими рассказать о том, что происходит «внутри» SQL Server, поэтому мне было интересно, есть ли неявный недостаток в EXISTS, который дал бы смысл в моих измерениях (мог ли EXISTS быть 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 мс.

Ответы:


44

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

Очень редко что-либо всегда может быть правдой, особенно когда речь идет о базах данных. Существует множество способов выразить одну и ту же семантику в SQL. Если есть полезное практическое правило, это может быть для написания запросов с использованием наиболее естественного доступного синтаксиса (и, да, это субъективно), и рассматривать возможность переписывания только в том случае, если план запроса или производительность, которые вы получаете, неприемлемы.

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

Мне было интересно, есть ли неявный недостаток с 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условие выполнено. Для соблюдения этой семантики оптимизатор создает план, который использует сквозные предикаты:

Сквозные предикаты

Внутренняя сторона объединений вложенных циклов оценивается, только когда предикат сквозного соединения возвращает false. Общий эффект состоит в том, что CASEвыражения проверяются по порядку, а подзапросы оцениваются только в том случае, если предыдущее выражение не было удовлетворено.

CASE-выражения с подзапросом EXISTS

Когда используется CASEподзапрос EXISTS, тест логического существования реализуется как полусоединение, но строки, которые обычно отклоняются полусоединением, должны быть сохранены на случай, если они понадобятся более позднему предложению. Строки, проходящие через этот особый тип полусоединения, получают флаг, указывающий, найдено ли полусоединение совпадением или нет. Этот флаг известен как столбец зонда .

Детали реализации состоят в том, что логический подзапрос заменяется коррелированным соединением ('apply') со столбцом исследования. Работа выполняется по правилу упрощения в оптимизаторе запросов 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 Join с зондом

Окончательный вычисляемый скаляр оценивает результат 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результату поиска. Обычно оптимизатор рассматривает возможность сделать это (даже без дополнительного предиката в запросе). Он распознает подразумеваемые предикаты ( BETWEENon T1и предикат соединения между T1и T2вместе подразумевают BETWEENon T2) без их присутствия в исходном тексте запроса. К сожалению, шаблон apply-probe означает, что это не исследовано.

Есть способы написать запрос для создания поиска на обоих входах в объединение с полусоединением. Один из способов заключается в написании запроса довольно неестественным способом (победив причину, которую я обычно предпочитаю 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 (*) против СУЩЕСТВУЕТ» аргумент это делать с проверкой , существует ли запись. Например:

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.