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.
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.
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) ) ;
Odpowiedzi:
Plan jest podobny do planu, który omówię bardziej szczegółowo tutaj .
Posts
Tabela jest skanowany.Dla każdego wiersza wyodrębnia
OwnerUserId
iLastEditorUserId
. Jest to podobne do sposobuUNPIVOT
działania. W poniższym planie widzisz jednego stałego operatora skanowania, który tworzy dwa wiersze wyjściowe dla każdego wiersza wejściowego.W tym przypadku plan jest nieco bardziej złożony, ponieważ semantyka polega na
or
tym, że jeśli obie wartości kolumn są takie same, tylko jeden wiersz powinien być emitowany z przyłączeniaUsers
(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ą
62
jest flaga oznaczająca, że poszukiwanie powinno być poszukiwaniem równości.Jeżeli chodzi o
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.
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.
W zależności od tego, jakie indeksy są dostępne w
Posts
tabeli, wariant tego zapytania może być bardziej wydajny niż proponowaneUNION ALL
rozwiązanie. (kopia bazy danych, której nie mam, jest przydatna do tego celu, a proponowane rozwiązanie wykonuje dwa pełne skanyPosts
. Poniżej robi to za jednym razem)źródło