Vérifier l'existence avec EXISTS surperformer COUNT! … Ne pas?


36

J'ai souvent lu quand il fallait vérifier l'existence d'une ligne devrait toujours être fait avec EXISTS plutôt qu'avec un COUNT.

Pourtant, dans plusieurs scénarios récents, j’ai mesuré une amélioration des performances lors de l’utilisation de count.
Le motif va comme ceci:

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

Je ne connais pas bien les méthodes permettant de savoir ce qui se passe "à l'intérieur" de SQL Server. Je me demandais donc s'il existait une faille inconnue avec EXISTS qui donnait tout son sens aux mesures que j'ai effectuées (EXISTS est-il RBAR?!).

Avez-vous une explication à ce phénomène?

MODIFIER:

Voici un script complet que vous pouvez exécuter:

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 

Sur SQL Server 2008R2 (Seven 64bits), j'obtiens ce résultat

COUNT Version:

Tableau '# 455F344D'. Nombre de balayages 1, lectures logiques 8, lectures physiques 0, lectures anticipées 0, lectures logiques lob 0, lectures physiques lob 0, lectures lob
ultérieures 0. Table '# 492FC531'. Nombre de balayages 1, lectures logiques 30, lectures physiques 0, lectures anticipées 0, lectures logiques lobées 0, lectures physiques lob 0, lectures anticipées lobées 0.

Temps d'exécution SQL Server:
temps CPU = 0 ms, temps écoulé = 81 ms.

EXISTS Version:

Tableau '# 492FC531'. Nombre de balayages 1, lectures logiques 96, lectures physiques 0, lectures anticipées 0, lectures logiques lob 0, lectures physiques lob 0, lectures lob
ultérieures 0. Table '# 455F344D'. Nombre de balayages 1, lectures logiques 8, lectures physiques 0, lectures anticipées 0, lectures logiques lob 0, lectures physiques lob 0, lectures anticipées lobées 0.

Temps d'exécution SQL Server:
temps CPU = 0 ms, temps écoulé = 76 ms.

Réponses:


44

J'ai souvent lu quand il fallait vérifier l'existence d'une ligne devrait toujours être fait avec EXISTS plutôt qu'avec un COUNT.

Il est très rare que quelque chose soit toujours vrai, en particulier en ce qui concerne les bases de données. Il y a plusieurs façons d'exprimer la même sémantique en SQL. S'il existe une règle empirique utile, il pourrait être d'écrire des requêtes en utilisant la syntaxe la plus naturelle disponible (et, oui, cela est subjectif) et d'envisager des réécritures uniquement si le plan de requête ou les performances que vous obtenez sont inacceptables.

Pour ce que cela vaut, mon propre point de vue sur le problème est que les requêtes d’existence s’expriment le plus naturellement EXISTS. Selon mon expérience, l' EXISTS optimisation a tendance à être meilleure que l' alternative de OUTER JOINrejet NULL. Utiliser COUNT(*)et filtrer sur =0est une autre alternative, qui trouve un support dans l'optimiseur de requêtes SQL Server, mais j'ai personnellement trouvé que cela n'était pas fiable dans des requêtes plus complexes. En tout cas, cela EXISTSme semble beaucoup plus naturel que l'une ou l'autre de ces alternatives.

Je me demandais s'il existait une faille inconnue avec EXISTS qui donnait un sens parfait aux mesures que j'avais effectuées.

Votre exemple particulier est intéressant car il met en évidence la manière dont l'optimiseur traite les sous-requêtes dans les CASEexpressions (et les EXISTStests en particulier).

Sous-requêtes dans les expressions CASE

Considérez la requête suivante (parfaitement légale):

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;

La sémantique deCASE sont que les WHEN/ELSEclauses sont généralement évaluées dans l'ordre textuel. Dans la requête ci-dessus, il serait incorrect pour SQL Server de renvoyer une erreur si la ELSEsous - requête renvoyait plus d'une ligne, si la WHENclause était satisfaite. Pour respecter cette sémantique, l'optimiseur génère un plan utilisant des prédicats de transmission:

Prédicats de passage

Le côté interne des jointures de boucle imbriquées n'est évalué que lorsque le prédicat d'intercommunication renvoie false. L’effet général est que les CASEexpressions sont testées dans l’ordre et que les sous-requêtes ne sont évaluées que si aucune expression précédente n’a été satisfaite.

Expressions CASE avec une sous-requête EXISTS

Lorsqu'une CASEsous - requête utilise EXISTS, le test d'existence logique est implémenté en tant que semi-jointure, mais les lignes qui seraient normalement rejetées par la semi-jointure doivent être conservées au cas où une clause ultérieure en aurait besoin. Les rangées traversant ce type particulier de demi-jointure acquièrent un drapeau pour indiquer si la demi-jointure a trouvé une correspondance ou non. Ce drapeau est connu comme la colonne de sonde .

Les détails de l'implémentation sont les suivants: la sous-requête logique est remplacée par une jointure corrélée ("apply") avec une colonne de sonde. Le travail est effectué par une règle de simplification dans l'optimiseur de requête appelée RemoveSubqInPrj(supprimer la sous-requête dans la projection). Nous pouvons voir les détails en utilisant l'indicateur de trace 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);

Une partie de l’arbre d’entrée affichant le EXISTStest est présentée ci-dessous:

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

Cela se transforme en RemoveSubqInPrjune structure dirigée par:

LogOp_Apply (x_jtLeftSemi probe PROBE:COL: Expr1008)

Ceci est la semi-jointure gauche appliquée avec la sonde décrite précédemment. Cette transformation initiale est la seule disponible dans les optimiseurs de requêtes SQL Server à ce jour et la compilation échouera simplement si cette transformation est désactivée.

L'une des formes de plan d'exécution possibles pour cette requête est une implémentation directe de cette structure logique:

NLJ Semi Join avec sonde

Compute Scalar final évalue le résultat de l' CASEexpression à l'aide de la valeur de la colonne de sonde:

Calculer l'expression scalaire

La forme de base de l'arborescence de plan est préservée lorsque l'optimisation considère d'autres types de jointure physique pour la semi-jointure. Seule la jointure de fusion prend en charge une colonne de test, de sorte qu'une demi-jointure de hachage, bien que ce soit logiquement possible, n'est pas prise en compte:

Fusionner avec la colonne de sonde

Notez que la fusion génère une expression libellée Expr1008(le nom identique à celui d’avant étant une coïncidence) bien qu’aucune définition n’apparaisse pour aucun opérateur du plan. Ceci est juste la colonne de sonde à nouveau. Comme auparavant, Compute Scalar final utilise cette valeur de sonde pour évaluer le fichier CASE.

Le problème est que l'optimiseur n'explore pas pleinement les alternatives qui ne valent la peine d'être gagnées que par la fusion (ou hash) semi-join. Dans le plan de boucles imbriquées, il n'y a aucun avantage à vérifier si les lignes T2correspondent à la plage à chaque itération. Avec un plan de fusion ou de hachage, cela pourrait être une optimisation utile.

Si nous ajoutons un BETWEENprédicat correspondant à T2dans la requête, tout ce qui se passe est que cette vérification est effectuée pour chaque ligne en tant que résidu de la semi-jointure de fusion (difficile à repérer dans le plan d'exécution, mais il est là):

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;

Prédicat résiduel

Nous espérons que le BETWEENprédicat sera plutôt poussé à T2aboutir à une recherche. Normalement, l'optimiseur envisagerait de procéder de la sorte (même sans le prédicat supplémentaire dans la requête). Il reconnaît les prédicats implicites ( BETWEENsur T1et la jointure entre prédicat T1et T2ensemble implique le BETWEENsur T2) sans qu'ils soient présents dans le texte de la requête originale. Malheureusement, le motif apply-probe signifie que cela n’est pas exploré.

Il existe des moyens d'écrire la requête pour produire des recherches sur les deux entrées d'une semi-jointure de fusion. L’une des méthodes consiste à écrire la requête d’une manière peu naturelle (en éliminant la raison que je préfère en général 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 plan de tour

Je ne serais pas heureux d'écrire cette requête dans un environnement de production, c'est simplement pour démontrer que la forme souhaitée du plan est possible. Si la requête réelle que vous devez écrire utilise CASEde cette manière, et que les performances souffrent du fait qu’il n’ya pas de recherche du côté de la sonde d’une semi-jointure de fusion, vous pouvez envisager d’écrire la requête en utilisant une syntaxe différente qui produit les bons résultats et une plan d'exécution plus efficace.


6

L' argument "COUNT (*) vs EXISTS" concerne la vérification de l'existence d'un enregistrement. Par exemple:

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

contre

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

Votre script SQL n'utilise pas COUNT(*)comme vérification de l'existence d'un enregistrement et par conséquent, je ne dirais pas que c'est applicable dans votre scénario.


Toute conclusion / conclusion basée sur le script que j'ai posté?
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.