CROSS APPLY produit une jointure externe

17

En réponse au 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(pas OUTER APPLY), alors pourquoi y a-t-il une jointure externe dans le plan d'exécution au lieu d'une jointure interne ?

entrez la description de l'image ici

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

entrez la description de l'image ici

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)
Paul White réintègre Monica
la source

Réponses:

23

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 BYclause correspondante est un agrégat scalaire .
  • Un agrégat avec une GROUP BYclause 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' COUNTagré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 cest zéro , car il COUNT_BIGs'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 BYdans la table dérivée (sinon il ne pourrait pas y avoir de Acolonne sur laquelle se joindre). La jointure doit être une jointure externe afin que chaque ligne de la table @Acontinue à produire une ligne dans la sortie. La jointure gauche produira une NULLcolonne for clorsque le prédicat de jointure n'est pas évalué à true. Cela NULLdoit être traduit à zéro par COALESCEpour effectuer une transformation correcte à partir de l' application .

La démo ci-dessous montre comment la jointure externe et COALESCEsont 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_BIGest 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 APPLYrejette 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 ApplyHandleretc.) 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é):

Plan d'application de la bobine d'indexation

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 BYclause.

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

Paul White réintègre Monica
la source
-3

L'application croisée est une opération logique sur les données. Pour décider comment obtenir ces données, SQL Server choisit l'opérateur physique approprié pour obtenir les données souhaitées.

Il n'existe aucun opérateur d'application physique et SQL Server le traduit en l'opérateur de jointure approprié et, espérons-le, efficace.

Vous pouvez trouver une liste des opérateurs physiques dans le lien ci-dessous.

https://docs.microsoft.com/en-us/sql/relational-databases/showplan-logical-and-physical-operators-reference?view=sql-server-2017

L'optimiseur de requêtes crée un plan de requête sous la forme d'une arborescence composée d'opérateurs logiques. Une fois que l'optimiseur de requêtes a créé le plan, l'optimiseur de requêtes choisit l'opérateur physique le plus efficace pour chaque opérateur logique. L'optimiseur de requêtes utilise une approche basée sur les coûts pour déterminer quel opérateur physique implémentera un opérateur logique.

Habituellement, une opération logique peut être implémentée par plusieurs opérateurs physiques. Cependant, dans de rares cas, un opérateur physique peut également implémenter plusieurs opérations logiques.

modifier / Il semble que j'ai mal compris votre question. Le serveur SQL choisira normalement l'opérateur le plus approprié. Votre requête n'a pas besoin de renvoyer de valeurs pour toutes les combinaisons des deux tables, c'est-à-dire lorsqu'une jointure croisée est utilisée. Il suffit de calculer la valeur souhaitée pour chaque ligne, ce qui se fait ici.

J. Maes
la source