Strano piano di query quando si utilizza OR nella clausola JOIN - Scansione costante per ogni riga della tabella


10

Sto cercando di produrre un piano di query di esempio per mostrare perché UNIONing due set di risultati può essere migliore dell'utilizzo di OR in una clausola JOIN. Un piano di query che ho scritto mi ha lasciato perplesso. Sto usando il database StackOverflow con un indice non cluster su Users.Reputation.

Immagine del piano di query La domanda è

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

Il piano di query è all'indirizzo https://www.brentozar.com/pastetheplan/?id=BkpZU1MZE , la durata della query per me è di 4:37 min, 26612 righe restituite.

Non ho mai visto questo stile di scansione costante creato da una tabella esistente in precedenza: non ho familiarità con il motivo per cui viene eseguita una scansione costante per ogni singola riga, quando di solito viene utilizzata una scansione costante per una singola riga immessa dall'utente ad esempio SELEZIONA GETDATE (). Perché è usato qui? Gradirei davvero alcune indicazioni nella lettura di questo piano di query.

Se lo suddivido in un UNION, produce un piano standard in esecuzione in 12 secondi con le stesse 26612 righe restituite.

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

Interpreto questo piano nel modo seguente:

  • Ricevi tutte le 41782500 righe dai post (il numero effettivo di righe corrisponde alla scansione CI sui post)
  • Per ogni 41782500 righe nei post:
    • Produrre scalari:
    • Expr1005: OwnerUserId
    • Expr1006: OwnerUserId
    • Expr1004: il valore statico 62
    • Expr1008: LastEditorUserId
    • Expr1009: LastEditorUserId
    • Expr1007: il valore statico 62
  • Nel concatenato:
    • Exp1010: Se Expr1005 (OwnerUserId) non è null, utilizzare quell'altro utilizzare Expr1008 (LastEditorUserID)
    • Expr1011: Se Expr1006 (OwnerUserId) non è null, usalo, altrimenti usa Expr1009 (LastEditorUserId)
    • Expr1012: Se Expr1004 (62) è null, usalo, altrimenti usa Expr1007 (62)
  • Nello scalare di calcolo: non so cosa faccia una e commerciale.
    • Expr1013: 4 [e?] 62 (Expr1012) = 4 e OwnerUserId IS NULL (NULL = Expr1010)
    • Expr1014: 4 [e?] 62 (Expr1012)
    • Expr1015: 16 e 62 (Expr1012)
  • Nell'ordine per ordina per:
    • Expr1013 Desc
    • Espr.1014 Asc
    • Expr1010 Asc
    • Expr1015 Desc
  • Nell'intervallo di merge ha rimosso Expr1013 ed Expr1015 (questi sono input ma non output)
  • Nell'indice cerca sotto i loop nidificati join sta usando Expr1010 ed Expr1011 come predicati di ricerca, ma non capisco come abbia accesso a questi quando non ha eseguito il join loop nidificato da IX_NC_REPUTATION alla sottostruttura contenente Expr1010 ed Expr1011 .
  • Il join Nested Loops restituisce solo gli Users.ID che hanno una corrispondenza nella sottostruttura precedente. A causa del pushdown del predicato, vengono restituite tutte le righe restituite dall'indice seek su IX_NC_REPUTATION.
  • Gli ultimi loop nidificati si uniscono: per ogni record di messaggi, genera Users.Id in cui viene trovata una corrispondenza nel set di dati di seguito.

Hai provato con una subquery o una subquery 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ᵀᴹ

una sottoquery: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ᵀᴹ

Risposte:


10

Il piano è simile a quello in cui vado più in dettaglio qui .

La Poststabella viene scansionata.

Per ogni riga estrae il OwnerUserIde LastEditorUserId. Questo è in un modo simile al modo in cui UNPIVOTfunziona. Nel piano di seguito viene visualizzato un singolo operatore di scansione costante che crea le due righe di output per ciascuna riga di input.

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

In questo caso il piano è un po 'più complesso in quanto la semantica orè che se entrambi i valori di colonna sono uguali, dal join deve essere emessa solo una riga Users(non due)

Questi vengono quindi sottoposti all'intervallo di unione in modo che, nel caso in cui i valori siano uguali, l'intervallo viene compresso e viene eseguita una sola ricerca Users, altrimenti vengono eseguite due ricerche.

Il valore 62è una bandiera che significa che la ricerca dovrebbe essere una ricerca di uguaglianza.

per quanto riguarda

Non capisco come abbia accesso a questi quando non ha eseguito il join loop nidificato da IX_NC_REPUTATION alla sottostruttura contenente Expr1010 ed Expr1011

Questi sono definiti nell'operatore di concatenazione evidenziato in giallo. Questo è sul lato esterno dei loop nidificati evidenziati in giallo. Quindi questo corre prima che la ricerca evidenziata in giallo all'interno di quei cicli annidati.

inserisci qui la descrizione dell'immagine

Una riscrittura che fornisce un piano simile (anche se con l'intervallo di unione sostituito da un'unione di unione) è inferiore nel caso in cui ciò aiuti.

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) 

inserisci qui la descrizione dell'immagine

A seconda degli indici disponibili nella Poststabella, una variante di questa query potrebbe essere più efficiente della UNION ALLsoluzione proposta . (la copia del database che ho non ha un indice utile per questo e la soluzione proposta fa due scansioni complete di Posts. Il seguito lo fa in una scansione)

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

inserisci qui la descrizione dell'immagine

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.