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.
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.
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) ) ;
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) ) ;
Réponses:
Le plan est similaire à celui que j'aborde plus en détail ici .
Le
Posts
tableau est numérisé.Pour chaque ligne, il extrait le
OwnerUserId
etLastEditorUserId
. C'est d'une manière similaire à la façon dontUNPIVOT
fonctionne. 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.Dans ce cas, le plan est un peu plus complexe car la sémantique pour
or
est que si les deux valeurs de colonne sont les mêmes, une seule ligne doit être émise depuis la jointureUsers
(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
62
est un indicateur signifiant que la recherche doit être une recherche d'égalité.En ce qui concerne
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.
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.
En fonction des index disponibles sur la
Posts
table, une variante de cette requête peut être plus efficace que votreUNION ALL
solution 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 dePosts
. La ci-dessous le fait en une seule analyse)la source