Sommaire
SQL Server utilise la jointure correcte (interne ou externe) et ajoute des projections si nécessaire pour honorer toute la sémantique de la requête d'origine lors de l'exécution des traductions internes entre appliquer et joindre .
Les différences dans les plans peuvent toutes être expliquées par la sémantique différente des agrégats avec et sans clause group by dans SQL Server.
Détails
Rejoindre ou appliquer
Nous devrons être capables de faire la distinction entre une candidature et une jointure :
Appliquer
L'entrée intérieure (inférieure) de l' application est exécutée pour chaque ligne de l'entrée extérieure (supérieure), avec une ou plusieurs valeurs de paramètre côté intérieur fournies par la ligne extérieure actuelle. Le résultat global de l' application est la combinaison (union tout) de toutes les lignes produites par les exécutions côté intérieur paramétrées. La présence de paramètres signifie que l' application est parfois appelée jointure corrélée.
Une application est toujours implémentée dans les plans d'exécution par l' opérateur Nested Loops . L'opérateur aura une propriété Références externes plutôt que de joindre des prédicats. Les références externes sont les paramètres transmis du côté externe au côté interne à chaque itération de la boucle.
Joindre
Une jointure évalue son prédicat de jointure à l'opérateur de jointure. La jointure peut généralement être implémentée par les opérateurs Hash Match , Merge ou Nested Loops dans SQL Server.
Lorsque les boucles imbriquées sont choisies, elles peuvent être distinguées d'une application par l'absence de références externes (et généralement la présence d'un prédicat de jointure). L'entrée interne d'une jointure ne fait jamais référence aux valeurs de l'entrée externe - le côté interne est toujours exécuté une fois pour chaque ligne externe, mais les exécutions côté interne ne dépendent d'aucune valeur de la ligne externe actuelle.
Pour plus de détails, voir mon article Appliquer contre les boucles imbriquées .
... pourquoi y a-t-il une jointure externe dans le plan d'exécution au lieu d'une jointure interne ?
La jointure externe apparaît lorsque l'optimiseur transforme une application en jointure (à l'aide d'une règle appelée ApplyHandler
) pour voir s'il peut trouver un plan basé sur une jointure moins cher. La jointure doit être une jointure externe pour être correcte lorsque l' application contient un agrégat scalaire . Une jointure interne ne serait pas garantie de produire les mêmes résultats que l' application d' origine, comme nous le verrons.
Agrégats scalaires et vectoriels
- Un agrégat sans
GROUP BY
clause correspondante est un agrégat scalaire .
- Un agrégat avec une
GROUP BY
clause correspondante est un agrégat vectoriel .
Dans SQL Server, un agrégat scalaire produira toujours une ligne, même si aucune ligne à agréger ne lui est attribuée. Par exemple, l' COUNT
agrégat scalaire d'aucune ligne est zéro. Un ensemble vectoriel COUNT
sans lignes est l'ensemble vide (pas de lignes du tout).
Les requêtes de jouets suivantes illustrent la différence. Vous pouvez également en savoir plus sur les agrégats scalaires et vectoriels dans mon article Fun with Scalar and Vector Aggregates .
-- Produces a single zero value
SELECT COUNT_BIG(*) FROM #MyTable AS MT WHERE 0 = 1;
-- Produces no rows
SELECT COUNT_BIG(*) FROM #MyTable AS MT WHERE 0 = 1 GROUP BY ();
démo db <> violon
Transformer demander à rejoindre
J'ai mentionné auparavant que la jointure doit être une jointure externe pour être correcte lorsque l' application d' origine contient un agrégat scalaire . Pour montrer pourquoi c'est le cas en détail, je vais utiliser un exemple simplifié de la requête de question:
DECLARE @A table (A integer NULL, B integer NULL);
DECLARE @B table (A integer NULL, B integer NULL);
INSERT @A (A, B) VALUES (1, 1);
INSERT @B (A, B) VALUES (2, 2);
SELECT * FROM @A AS A
CROSS APPLY (SELECT c = COUNT_BIG(*) FROM @B AS B WHERE B.A = A.A) AS CA;
Le résultat correct pour la colonne c
est zéro , car il COUNT_BIG
s'agit d'un agrégat scalaire . Lors de la traduction de cette requête d'application en formulaire de jointure, SQL Server génère une alternative interne qui ressemblerait à la suivante si elle était exprimée en T-SQL:
SELECT A.*, c = COALESCE(J1.c, 0)
FROM @A AS A
LEFT JOIN
(
SELECT B.A, c = COUNT_BIG(*)
FROM @B AS B
GROUP BY B.A
) AS J1
ON J1.A = A.A;
Pour réécrire l'application en tant que jointure non corrélée, nous devons introduire un GROUP BY
dans la table dérivée (sinon il ne pourrait pas y avoir de A
colonne sur laquelle se joindre). La jointure doit être une jointure externe afin que chaque ligne de la table @A
continue à produire une ligne dans la sortie. La jointure gauche produira une NULL
colonne for c
lorsque le prédicat de jointure n'est pas évalué à true. Cela NULL
doit être traduit à zéro par COALESCE
pour effectuer une transformation correcte à partir de l' application .
La démo ci-dessous montre comment la jointure externe et COALESCE
sont nécessaires pour produire les mêmes résultats en utilisant la jointure que la requête d' application d' origine :
démo db <> violon
Avec le GROUP BY
... pourquoi le fait de ne pas commenter la clause group by entraîne une jointure interne?
Poursuivant l'exemple simplifié, mais en ajoutant un GROUP BY
:
DECLARE @A table (A integer NULL, B integer NULL);
DECLARE @B table (A integer NULL, B integer NULL);
INSERT @A (A, B) VALUES (1, 1);
INSERT @B (A, B) VALUES (2, 2);
-- Original
SELECT * FROM @A AS A
CROSS APPLY
(SELECT c = COUNT_BIG(*) FROM @B AS B WHERE B.A = A.A GROUP BY B.A) AS CA;
Le COUNT_BIG
est maintenant un agrégat vectoriel , donc le résultat correct pour un jeu d'entrées vide n'est plus zéro, ce n'est plus du tout une ligne . En d'autres termes, l'exécution des instructions ci-dessus ne produit aucune sortie.
Ces sémantiques sont beaucoup plus faciles à respecter lors de la conversion de appliquer à joindre , car CROSS APPLY
rejette naturellement toute ligne extérieure qui ne génère aucune ligne latérale intérieure. Nous pouvons donc maintenant utiliser en toute sécurité une jointure interne, sans projection d'expression supplémentaire:
-- Rewrite
SELECT A.*, J1.c
FROM @A AS A
JOIN
(
SELECT B.A, c = COUNT_BIG(*)
FROM @B AS B
GROUP BY B.A
) AS J1
ON J1.A = A.A;
La démo ci-dessous montre que la réécriture de jointure interne produit les mêmes résultats que l'application d'origine avec agrégat vectoriel:
démo db <> violon
L'optimiseur arrive à choisir une jointure interne de fusion avec la petite table car il trouve rapidement un plan de jointure bon marché (assez bon plan trouvé). L'optimiseur basé sur les coûts peut continuer à réécrire la jointure dans une application - peut-être trouver un plan d'application moins cher, comme il le fera ici si une jointure en boucle ou un indice forceseek est utilisé - mais cela ne vaut pas la peine dans ce cas.
Remarques
Les exemples simplifiés utilisent différentes tables avec des contenus différents pour montrer plus clairement les différences sémantiques.
On pourrait faire valoir que l'optimiseur devrait être capable de raisonner sur le fait qu'une auto-jointure ne peut pas générer de lignes incompatibles (non jointives), mais elle ne contient pas cette logique aujourd'hui. De toute façon, l'accès à la même table plusieurs fois dans une requête n'est pas garanti pour produire les mêmes résultats en général, selon le niveau d'isolement et l'activité simultanée.
L'optimiseur s'inquiète de ces sémantiques et des cas marginaux pour que vous n'ayez pas à le faire.
Bonus: plan d' application interne
SQL Server peut produire un plan d' application interne (pas un plan de jointure interne !) Pour l'exemple de requête, il choisit simplement de ne pas le faire pour des raisons de coût. Le coût du plan de jointure externe indiqué dans la question est de 0,02898 unités sur l'instance SQL Server 2017 de mon ordinateur portable.
Vous pouvez forcer un plan d' application (jointure corrélée) en utilisant l'indicateur de trace 9114 non documenté et non pris en charge (qui désactive ApplyHandler
etc.) juste à titre d'illustration:
SELECT *
FROM #MyTable AS mt
CROSS APPLY
(
SELECT COUNT_BIG(DISTINCT mt2.Col_B) AS dc
FROM #MyTable AS mt2
WHERE mt2.Col_A = mt.Col_A
--GROUP BY mt2.Col_A
) AS ca
OPTION (QUERYTRACEON 9114);
Cela produit un plan d' application de boucles imbriquées avec une bobine d'index paresseux. Le coût total estimé est de 0,0463983 (supérieur au plan sélectionné):
Notez que le plan d'exécution utilisant des boucles imbriquées apply produit des résultats corrects en utilisant la sémantique de "jointure interne", quelle que soit la présence de la GROUP BY
clause.
Dans le monde réel, nous aurions généralement un index pour prendre en charge une recherche sur le côté interne de l' application pour encourager SQL Server à choisir cette option naturellement, par exemple:
CREATE INDEX i ON #MyTable (Col_A, Col_B);
démo db <> violon