web-dev-qa-db-fra.com

CROSS APPLY produit une jointure externe

En réponse à comptage SQL distinct sur la partition Erik Darling a publié ce code pour contourner le manque de COUNT(DISTINCT) OVER ():

SELECT      *
FROM        #MyTable AS mt
CROSS APPLY (   SELECT COUNT(DISTINCT mt2.Col_B) AS dc
                FROM   #MyTable AS mt2
                WHERE  mt2.Col_A = mt.Col_A
                -- GROUP BY mt2.Col_A 
            ) AS ca;

La requête utilise CROSS APPLY (ne pas OUTER APPLY) alors pourquoi y a-t-il une jointure externe dans le plan d'exécution au lieu d'une interne rejoindre?

enter image description here

De plus, pourquoi la mise en commentaire de la clause group by entraîne-t-elle une jointure interne?

enter image description here

Je ne pense pas que les données soient importantes mais en copiant celles données par kevinwhat sur l'autre question:

create table #MyTable (
Col_A varchar(5),
Col_B int
)

insert into #MyTable values ('A',1)
insert into #MyTable values ('A',1)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',3)

insert into #MyTable values ('B',4)
insert into #MyTable values ('B',4)
insert into #MyTable values ('B',5)
18
Paul White 9

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 traductions internes entre appliquer et rejoignez.

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 un appliquer et un rejoindre:

  • Appliquer

    L'entrée intérieure (inférieure) de appliquer 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 appliquer 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 appliquer est parfois appelé jointure corrélée.

    Un appliquer est toujours implémenté 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.

  • Rejoindre

    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 Boucles imbriquées est choisi, il peut être distingué d'un appliquer par l'absence de Références externes (et généralement le présence d'un prédicat de jointure). L'entrée interne d'un join 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 du 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 intérieure?

La jointure externe apparaît lorsque l'optimiseur transforme un appliquer en un joint (en utilisant une règle appelée ApplyHandler) pour voir s'il peut trouver un plan de jointure moins cher. La jointure doit être une jointure externe pour exactitude lorsque le appliquer contient un agrégat scalaire. Une jointure interne ne serait pas garantie pour produire les mêmes résultats que l'original appliquer comme nous le verrons.

Agrégats scalaires et vectoriels

  • Un agrégat sans clause GROUP BY Correspondante est un agrégat scalar.
  • Un agrégat avec une clause GROUP BY Correspondante est un agrégat vector.

Dans SQL Server, un agrégat scalar produira toujours une ligne, même si aucune ligne à agréger ne lui est attribuée. Par exemple, l'agrégat scalaire COUNT d'aucune ligne est nul. Un vecteurCOUNT l'agrégat de pas de 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 exactitude lorsque l'original s'applique 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 COUNT_BIG Est 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 y avoir aucune colonne A sur laquelle se joindre). La jointure doit être une externe jointure afin que chaque ligne de la table @A Continue de produire une ligne dans la sortie. La jointure gauche produira un NULL pour la colonne c lorsque le prédicat de jointure ne sera pas évalué à true. Ce NULL doit être traduit à zéro par COALESCE pour terminer une transformation correcte de appliquer.

La démo ci-dessous montre comment la jointure externe et COALESCE sont nécessaires pour produire les mêmes résultats en utilisant join comme la requête originale appliquer:

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;

COUNT_BIG Est maintenant un agrégat vector, donc le résultat correct pour un jeu d'entrées vide n'est plus zéro, il est pas de ligne du tout. 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 traduction 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émonstration 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 join bon marché (un plan suffisamment bon a été 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 Edge pour que vous n'ayez pas à le faire.


Bonus: Inner Postulez Plan

SQL Server peut ​​produire un plan interne appliquer (pas un plan interne joindre!) Pour l'exemple de requête, il choisit simplement de ne pas pour des raisons de coût. Le coût du plan de jointure externe indiqué dans la question est de ,02898 unités sur l'instance SQL Server 2017 de mon ordinateur portable.

Vous pouvez forcer un plan appliquer (jointure corrélée) en utilisant l'indicateur de trace non documenté et non pris en charge 9114 (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 de boucles imbriquées appliquer avec une bobine d'index paresseux. Le coût total estimé est ,046398 (supérieur au plan sélectionné):

Index Spool apply plan

Notez que le plan d'exécution utilisant appliquer les boucles imbriquées produit des résultats corrects en utilisant la sémantique de "jointure interne" indépendamment de la présence de la clause GROUP BY.

Dans le monde réel, nous aurions généralement un index pour prendre en charge une recherche à l'intérieur du appliquer pour encourager SQL Server à choisir cette option naturellement, par exemple:

CREATE INDEX i ON #MyTable (Col_A, Col_B);

démo db <> violon

24
Paul White 9