Plan de requête étrange lors de l'utilisation de la clause OR dans JOIN - Analyse constante pour chaque ligne du tableau

10

J'essaie de produire un exemple de plan de requête pour montrer pourquoi UNIONner deux ensembles de résultats peut être meilleur que d'utiliser OR dans une clause JOIN. Un plan de requête que j'ai écrit m'a laissé perplexe. J'utilise la base de données StackOverflow avec un index non cluster sur Users.Reputation.

Image du plan de requête La requête est

CREATE NONCLUSTERED INDEX IX_NC_REPUTATION ON dbo.USERS(Reputation)
SELECT DISTINCT Users.Id
FROM dbo.Users
INNER JOIN dbo.Posts  
    ON Users.Id = Posts.OwnerUserId
    OR Users.Id = Posts.LastEditorUserId
WHERE Users.Reputation = 5

Le plan de requête est à https://www.brentozar.com/pastetheplan/?id=BkpZU1MZE , la durée de la requête pour moi est de 4:37 min, 26612 lignes retournées.

Je n'ai jamais vu ce style de scan constant créé à partir d'une table existante auparavant - je ne sais pas pourquoi il y a un scan constant exécuté pour chaque ligne, alors qu'un scan constant est généralement utilisé pour une seule ligne entrée par l'utilisateur par exemple SELECT GETDATE (). Pourquoi est-il utilisé ici? J'apprécierais vraiment quelques conseils pour lire ce plan de requête.

Si je divise ce OU en UNION, cela produit un plan standard exécuté en 12 secondes avec les mêmes 26612 lignes renvoyées.

SELECT Users.Id
FROM dbo.Users
    INNER JOIN dbo.Posts
       ON Users.Id = Posts.OwnerUserId
WHERE Users.Reputation = 5
UNION 
SELECT Users.Id
FROM dbo.Users
    INNER JOIN dbo.Posts
       ON  Users.Id = Posts.LastEditorUserId
WHERE Users.Reputation = 5

J'interprète ce plan comme ceci:

  • Obtenez toutes les 41782500 lignes des publications (le nombre réel de lignes correspond à l'analyse CI des publications)
  • Pour chaque 41782500 lignes dans les messages:
    • Produire des scalaires:
    • Expr1005: OwnerUserId
    • Expr1006: OwnerUserId
    • Expr1004: La valeur statique 62
    • Expr1008: LastEditorUserId
    • Expr1009: LastEditorUserId
    • Expr1007: La valeur statique 62
  • Dans le concaténé:
    • Exp1010: Si Expr1005 (OwnerUserId) n'est pas nul, utilisez-le sinon utilisez Expr1008 (LastEditorUserID)
    • Expr1011: Si Expr1006 (OwnerUserId) n'est pas nul, utilisez-le, sinon utilisez Expr1009 (LastEditorUserId)
    • Expr1012: Si Expr1004 (62) est nul, utilisez cela, sinon utilisez Expr1007 (62)
  • Dans le scalaire de calcul: je ne sais pas ce que fait une esperluette.
    • Expr1013: 4 [et?] 62 (Expr1012) = 4 et OwnerUserId IS NULL (NULL = Expr1010)
    • Expr1014: 4 [et?] 62 (Expr1012)
    • Expr1015: 16 et 62 (Expr1012)
  • Dans l'ordre Trier par:
    • Expr1013 Desc
    • Expr1014 Asc
    • Expr1010 Asc
    • Expr1015 Desc
  • Dans l'intervalle de fusion, il a supprimé Expr1013 et Expr1015 (ce sont des entrées mais pas des sorties)
  • Dans la recherche d'index sous la jointure de boucles imbriquées, il utilise Expr1010 et Expr1011 comme prédicats de recherche, mais je ne comprends pas comment il y accède lorsqu'il n'a pas effectué la jointure de boucle imbriquée de IX_NC_REPUTATION au sous-arbre contenant Expr1010 et Expr1011 .
  • La jointure des boucles imbriquées renvoie uniquement les utilisateurs.ID qui ont une correspondance dans la sous-arborescence précédente. En raison du pushdown de prédicat, toutes les lignes renvoyées par la recherche d'index sur IX_NC_REPUTATION sont retournées.
  • La dernière jointure de boucles imbriquées: pour chaque enregistrement de publications, affichez Users.Id où une correspondance se trouve dans l'ensemble de données ci-dessous.
Andrew
la source
Avez-vous essayé avec une ou des sous-requêtes EXISTS? SELECT Users.Id FROM dbo.Users WHERE Users.Reputation = 5 AND ( EXISTS (SELECT 1 FROM dbo.Posts WHERE Users.Id = Posts.OwnerUserId) OR EXISTS (SELECT 1 FROM dbo.Posts WHERE Users.Id = Posts.LastEditorUserId) ) ;
ypercubeᵀᴹ
une sous-requête:SELECT Users.Id FROM dbo.Users WHERE Users.Reputation = 5 AND EXISTS (SELECT 1 FROM dbo.Posts WHERE Users.Id IN (Posts.OwnerUserId, Posts.LastEditorUserId) ) ;
ypercubeᵀᴹ

Réponses:

10

Le plan est similaire à celui que j'aborde plus en détail ici .

Le Poststableau est numérisé.

Pour chaque ligne, il extrait le OwnerUserIdet LastEditorUserId. C'est d'une manière similaire à la façon dont UNPIVOTfonctionne. Vous voyez un seul opérateur de balayage constant dans le plan pour la création ci-dessous les deux lignes de sortie pour chaque ligne d'entrée.

SELECT *
FROM dbo.Posts
UNPIVOT (X FOR U IN (OwnerUserId,LastEditorUserId)) Unpvt

Dans ce cas, le plan est un peu plus complexe car la sémantique pour orest que si les deux valeurs de colonne sont les mêmes, une seule ligne doit être émise depuis la jointure Users(pas deux)

Ceux-ci sont ensuite soumis à l'intervalle de fusion de sorte que dans le cas où les valeurs sont les mêmes, la plage est réduite et une seule recherche est exécutée contre Users- sinon deux recherches sont exécutées contre elle.

La valeur 62est un indicateur signifiant que la recherche doit être une recherche d'égalité.

En ce qui concerne

Je ne comprends pas comment il a accès à ceux-ci lorsqu'il n'a pas effectué la jointure de boucle imbriquée de IX_NC_REPUTATION au sous-arbre contenant Expr1010 et Expr1011

Ceux-ci sont définis dans l'opérateur de concaténation surligné en jaune. C'est sur le côté extérieur des boucles imbriquées surlignées en jaune. Donc, cela court avant la recherche en surbrillance jaune à l'intérieur de ces boucles imbriquées.

entrez la description de l'image ici

Une réécriture qui donne un plan similaire (mais avec l'intervalle de fusion remplacé par une union de fusion) est ci-dessous au cas où cela aiderait.

SELECT DISTINCT D2.UserId
FROM   dbo.Posts p
       CROSS APPLY (SELECT Users.Id AS UserId
                    FROM   (SELECT p.OwnerUserId
                            UNION /*collapse duplicate to single row*/
                            SELECT p.LastEditorUserId) D1(UserId)
                           JOIN Users
                             ON Users.Id = D1.UserId) D2
OPTION (FORCE ORDER) 

entrez la description de l'image ici

En fonction des index disponibles sur la Poststable, une variante de cette requête peut être plus efficace que votre UNION ALLsolution proposée . (La copie de la base de données que je possède n'a pas d'index utile pour cela et la solution proposée fait deux analyses complètes de Posts. La ci-dessous le fait en une seule analyse)

WITH Unpivoted AS
(
SELECT UserId
FROM dbo.Posts
UNPIVOT (UserId FOR U IN (OwnerUserId,LastEditorUserId)) Unpivoted
)
SELECT DISTINCT Users.Id
FROM dbo.Users INNER HASH JOIN Unpivoted
       ON  Users.Id = Unpivoted.UserId
WHERE Users.Reputation = 5

entrez la description de l'image ici

Martin Smith
la source