Verifique a existência com EXISTS supera COUNT! … Não?

35

Eu sempre li quando era preciso verificar a existência de uma linha sempre deve ser feita com EXISTS, em vez de com COUNT.

No entanto, em vários cenários recentes, medi uma melhoria de desempenho ao usar count.
O padrão é assim:

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

Eu não estou familiarizado com os métodos para saber o que está acontecendo "dentro" do SQL Server, então eu queria saber se havia uma falha não anunciada com EXISTS que dava perfeitamente sentido às medidas que eu fiz (EXISTS podem ser RBAR ?!).

Você tem alguma explicação para esse fenômeno?

EDITAR:

Aqui está um script completo que você pode executar:

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 

No SQL Server 2008R2 (Seven 64bits), recebo este resultado

COUNT Versão:

Tabela '# 455F344D'. Contagem de varredura 1, leituras lógicas 8, leituras físicas 0, leituras de leitura antecipada 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de leitura antecipada de 0.
Tabela '# 492FC531'. Contagem de varredura 1, leituras lógicas 30, leituras físicas 0, leituras de read-ahead 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de read-ahead de lob 0.

Tempos de execução do SQL Server:
tempo de CPU = 0 ms, tempo decorrido = 81 ms.

EXISTS Versão:

Tabela '# 492FC531'. Contagem de varredura 1, leituras lógicas 96, leituras físicas 0, leituras de leitura antecipada 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras de leitura antecipada de
lobes lê 0. Tabela '# 455F344D'. Contagem de varredura 1, leituras lógicas 8, leituras físicas 0, leituras antecipadas 0, leituras lógicas lob 0, leituras lógicas lob 0, leituras físicas lob 0, leituras antecipadas lob.

Tempos de execução do SQL Server:
tempo de CPU = 0 ms, tempo decorrido = 76 ms.


fonte

Respostas:

43

Eu sempre li quando era preciso verificar a existência de uma linha sempre deve ser feita com EXISTS, em vez de com COUNT.

É muito raro que algo sempre seja verdade, especialmente quando se trata de bancos de dados. Existem inúmeras maneiras de expressar a mesma semântica no SQL. Se houver uma regra prática útil, talvez seja para escrever consultas usando a sintaxe mais natural disponível (e, sim, isso é subjetivo) e considere reescrever apenas se o plano ou desempenho da consulta que você obtiver for inaceitável.

Pelo que vale, minha opinião é que as consultas de existência são mais naturalmente expressas usando EXISTS. Também tem sido minha experiência que EXISTS tende a otimizar melhor do que a alternativa OUTER JOINrejeitada NULL. Usar COUNT(*)e filtrar =0é outra alternativa, que tem algum suporte no otimizador de consultas do SQL Server, mas eu pessoalmente achei isso não confiável em consultas mais complexas. De qualquer forma, EXISTSparece muito mais natural (para mim) do que qualquer uma dessas alternativas.

Fiquei me perguntando se havia uma falha não anunciada com EXISTS que dava perfeitamente sentido às medições que eu fiz

Seu exemplo em particular é interessante, porque destaca a maneira como o otimizador lida com subconsultas em CASEexpressões (e EXISTStestes em particular).

Subconsultas em expressões CASE

Considere a seguinte consulta (perfeitamente legal):

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;

A semântica deCASE que as WHEN/ELSEcláusulas são geralmente avaliadas em ordem textual. Na consulta acima, seria incorreto para o SQL Server retornar um erro se a ELSEsubconsulta retornasse mais de uma linha, se a WHENcláusula fosse atendida. Para respeitar essas semânticas, o otimizador produz um plano que usa predicados de passagem:

Predicados de passagem

O lado interno das junções de loop aninhado é avaliado apenas quando o predicado de passagem retorna false. O efeito geral é que as CASEexpressões são testadas em ordem e as subconsultas são avaliadas apenas se nenhuma expressão anterior for satisfeita.

Expressões CASE com uma subconsulta EXISTS

Onde uma CASEsubconsulta é usada EXISTS, o teste de existência lógica é implementado como uma semi-junção, mas as linhas que normalmente seriam rejeitadas pela semi-junção precisam ser retidas caso uma cláusula posterior precise delas. As linhas que fluem através desse tipo especial de semi-junção adquirem uma bandeira para indicar se a semi-junção encontrou ou não uma correspondência. Este sinalizador é conhecido como coluna do probe .

Os detalhes da implementação são que a subconsulta lógica é substituída por uma junção correlacionada ('aplicar') por uma coluna de análise. O trabalho é realizado por uma regra de simplificação no otimizador de consulta chamado RemoveSubqInPrj(remover subconsulta na projeção). Podemos ver os detalhes usando o sinalizador de rastreamento 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);

Parte da árvore de entrada que mostra o EXISTSteste é mostrada abaixo:

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

Isso é transformado em RemoveSubqInPrjpara uma estrutura encabeçada por:

LogOp_Apply (x_jtLeftSemi probe PROBE:COL: Expr1008)

Esta é a semi-junção esquerda aplicada com o probe descrito anteriormente. Essa transformação inicial é a única disponível nos otimizadores de consulta do SQL Server até o momento e a compilação simplesmente falhará se essa transformação estiver desabilitada.

Uma das formas possíveis do plano de execução para esta consulta é uma implementação direta dessa estrutura lógica:

Semi-junção NLJ com sonda

O Compute Scalar final avalia o resultado da CASEexpressão usando o valor da coluna do probe:

Calcular expressão escalar

A forma básica da árvore do plano é preservada quando a otimização considera outros tipos de junção física para a semi junção. Somente a junção de mesclagem suporta uma coluna de análise, portanto, uma junção semi-hash, embora logicamente possível, não é considerada:

Mesclar com a coluna do probe

Observe que a mesclagem gera uma expressão rotulada Expr1008(que o nome é o mesmo de antes é uma coincidência), embora nenhuma definição para ela apareça em qualquer operador do plano. Esta é apenas a coluna do probe novamente. Como antes, o Compute Scalar final usa esse valor de análise para avaliar o CASE.

O problema é que o otimizador não explora completamente alternativas que só valem a pena com semi-junção de mesclagem (ou hash). No plano de loops aninhados, não há vantagem em verificar se as linhas T2correspondem ao intervalo em todas as iterações. Com um plano de mesclagem ou hash, isso pode ser uma otimização útil.

Se adicionarmos um BETWEENpredicado correspondente a T2na consulta, tudo o que acontece é que essa verificação é executada para cada linha como um resíduo na semi-junção de mesclagem (difícil de identificar no plano de execução, mas existe):

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;

Predicado residual

Esperamos que o BETWEENpredicado seja empurrado para baixo, T2resultando em uma busca. Normalmente, o otimizador consideraria fazer isso (mesmo sem o predicado extra na consulta). Ele reconhece predicados implícitos ( BETWEENon T1e o predicado de junção entre T1e T2juntos implicam BETWEENon T2) sem que eles estejam presentes no texto da consulta original. Infelizmente, o padrão aplicar sonda significa que isso não é explorado.

Existem maneiras de escrever a consulta para produzir buscas em ambas as entradas em uma semi junção de mesclagem. Uma maneira envolve escrever a consulta de uma maneira bastante natural (derrotar o motivo pelo qual geralmente prefiro 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;

TOP truque plano

Eu não ficaria feliz em escrever essa consulta em um ambiente de produção, apenas para demonstrar que a forma desejada do plano é possível. Se a consulta real que você precisa escrever usa CASEdessa maneira específica, e o desempenho é prejudicado por não haver uma pesquisa no lado do probe de uma semi-junção de mesclagem, considere escrever a consulta usando uma sintaxe diferente que produz os resultados certos e um plano de execução mais eficiente.

Paul White diz que a GoFundMonica
fonte
6

O argumento "COUNT (*) vs EXISTS" tem a ver com a verificação da existência de um registro. Por exemplo:

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

vs

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

Seu script SQL não está usando COUNT(*)como uma verificação de existência de registro e, portanto, não diria que é aplicável no seu cenário.

Curt
fonte
Qualquer pensamento / conclusão com base no script que publiquei?