Dziwny plan zapytań przy użyciu OR w klauzuli JOIN - Stałe skanowanie dla każdego wiersza w tabeli

10

Próbuję stworzyć przykładowy plan zapytań, aby pokazać, dlaczego UNIONing dwóch zestawów wyników może być lepszy niż użycie OR w klauzuli JOIN. Napisany przeze mnie plan zapytań mnie zaskoczył. Korzystam z bazy danych StackOverflow z indeksem nieklastrowanym na Users.Reputation.

Obraz planu zapytania Zapytanie to

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

Plan zapytań znajduje się na stronie https://www.brentozar.com/pastetheplan/?id=BkpZU1MZE , czas trwania zapytania dla mnie wynosi 4:37 min, zwrócono 26612 wierszy.

Nie widziałem wcześniej, aby ten styl ciągłego skanowania był tworzony z istniejącej tabeli - nie jestem pewien, dlaczego ciągły skan jest uruchamiany dla każdego wiersza, gdy stały skan jest zwykle używany dla pojedynczego wiersza wprowadzonego przez użytkownika na przykład SELECT GETDATE (). Dlaczego jest tu używany? Byłbym bardzo wdzięczny za wskazówki dotyczące czytania tego planu zapytań.

Jeśli podzielę to OR na UNION, powstanie standardowy plan działający w 12 sekund z tymi samymi zwróconymi 26612 wierszami.

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

Interpretuję ten plan jako następujący:

  • Zbierz wszystkie 41782500 wierszy z postów (rzeczywista liczba wierszy odpowiada skanowi CI na postach)
  • Dla każdego 41782500 wierszy w postach:
    • Produkuj skalary:
    • Expr1005: OwnerUserId
    • Expr1006: OwnerUserId
    • Expr1004: Wartość statyczna 62
    • Expr1008: LastEditorUserId
    • Expr1009: LastEditorUserId
    • Expr1007: Wartość statyczna 62
  • W konkatenacie:
    • Exp1010: Jeśli Expr1005 (OwnerUserId) nie ma wartości NULL, użyj tego innego, użyj Expr1008 (LastEditorUserID)
    • Expr1011: Jeśli Expr1006 (OwnerUserId) nie ma wartości NULL, użyj tego, w przeciwnym razie użyj Expr1009 (LastEditorUserId)
    • Expr1012: Jeśli Expr1004 (62) ma wartość NULL, użyj tego, w przeciwnym razie użyj Expr1007 (62)
  • W skalarach obliczeniowych: nie wiem, co robi ampersand.
    • Expr1013: 4 [i?] 62 (Expr1012) = 4, a OwnerUserId IS NULL (NULL = Expr1010)
    • Expr1014: 4 [i?] 62 (Expr1012)
    • Expr1015: 16 i 62 (Expr1012)
  • W porządku Sortuj według:
    • Expr1013 Desc
    • Expr1014 Asc
    • Expr1010 Asc
    • Expr1015 Opis
  • W interwale scalania usunięto Expr1013 i Expr1015 (są to dane wejściowe, ale nie wyjściowe)
  • W indeksie Szukaj poniżej łączenie zagnieżdżonych pętli używa Expr1010 i Expr1011 jako predykatów szukania, ale nie rozumiem, w jaki sposób ma do nich dostęp, gdy nie wykonał łączenia zagnieżdżonej pętli od IX_NC_REPUTATION do poddrzewa zawierającego Expr1010 i Expr1011 .
  • Łączenie w zagnieżdżonych pętlach zwraca tylko identyfikatory użytkowników, które pasują do wcześniejszego poddrzewa. Z powodu przepychania predykatu zwracane są wszystkie wiersze zwrócone z wyszukiwania indeksu IX_NC_REPUTATION.
  • Ostatnie dołączone zagnieżdżone pętle: Dla każdego rekordu Postów wyślij Users.Id, gdzie znaleziono dopasowanie w poniższym zestawie danych.
Andrzej
źródło
Czy próbowałeś z podzapytaniem EXISTS lub podzapytaniem? 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ᵀᴹ
jedno podkwerenda: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ᵀᴹ

Odpowiedzi:

10

Plan jest podobny do planu, który omówię bardziej szczegółowo tutaj .

PostsTabela jest skanowany.

Dla każdego wiersza wyodrębnia OwnerUserIdi LastEditorUserId. Jest to podobne do sposobu UNPIVOTdziałania. W poniższym planie widzisz jednego stałego operatora skanowania, który tworzy dwa wiersze wyjściowe dla każdego wiersza wejściowego.

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

W tym przypadku plan jest nieco bardziej złożony, ponieważ semantyka polega na ortym, że jeśli obie wartości kolumn są takie same, tylko jeden wiersz powinien być emitowany z przyłączenia Users(nie dwa)

Są one następnie poddawane interwałowi scalania, tak aby w przypadku, gdy wartości są takie same, zakres jest zawalony i wykonuje się tylko jedno wyszukiwanie Users- w przeciwnym razie wykonywane są dwa wyszukiwania.

Wartością 62jest flaga oznaczająca, że ​​poszukiwanie powinno być poszukiwaniem równości.

Jeżeli chodzi o

Nie rozumiem, w jaki sposób ma do nich dostęp, skoro nie wykonał sprzężenia zagnieżdżonej pętli z IX_NC_REPUTATION do poddrzewa zawierającego Expr1010 i Expr1011

Są one zdefiniowane w podświetlonym na żółto operatorze konkatenacji. Jest to po zewnętrznej stronie podświetlonych na żółto zagnieżdżonych pętli. Dzieje się tak, zanim podświetlona na żółto funkcja wyszukiwania wewnątrz zagnieżdżonych pętli.

wprowadź opis zdjęcia tutaj

Przepisanie, które daje podobny plan (chociaż z interwałem scalania zastąpionym przez połączenie scalające) jest poniżej, na wypadek gdyby to pomogło.

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) 

wprowadź opis zdjęcia tutaj

W zależności od tego, jakie indeksy są dostępne w Poststabeli, wariant tego zapytania może być bardziej wydajny niż proponowane UNION ALLrozwiązanie. (kopia bazy danych, której nie mam, jest przydatna do tego celu, a proponowane rozwiązanie wykonuje dwa pełne skany Posts. Poniżej robi to za jednym razem)

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

wprowadź opis zdjęcia tutaj

Martin Smith
źródło