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 JOIN
rejet NULL
. Utiliser COUNT(*)
et filtrer sur =0
est 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 EXISTS
me 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 CASE
expressions (et les EXISTS
tests 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/ELSE
clauses 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 ELSE
sous - requête renvoyait plus d'une ligne, si la WHEN
clause était satisfaite. Pour respecter cette sémantique, l'optimiseur génère un plan utilisant des prédicats de transmission:
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 CASE
expressions 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 CASE
sous - 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 EXISTS
test 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 RemoveSubqInPrj
une 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:
Compute Scalar final évalue le résultat de l' CASE
expression à l'aide de la valeur de la colonne de sonde:
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:
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 T2
correspondent à la plage à chaque itération. Avec un plan de fusion ou de hachage, cela pourrait être une optimisation utile.
Si nous ajoutons un BETWEEN
prédicat correspondant à T2
dans 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;
Nous espérons que le BETWEEN
prédicat sera plutôt poussé à T2
aboutir à 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 ( BETWEEN
sur T1
et la jointure entre prédicat T1
et T2
ensemble implique le BETWEEN
sur 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;
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 CASE
de 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.