Mam klasę zapytań, które sprawdzają istnienie jednej z dwóch rzeczy. Ma formę
SELECT CASE
WHEN EXISTS (SELECT 1 FROM ...)
OR EXISTS (SELECT 1 FROM ...)
THEN 1 ELSE 0 END;
Rzeczywista instrukcja jest generowana w C i wykonywana jako zapytanie ad-hoc przez połączenie ODBC.
Niedawno wyszło na jaw, że w większości przypadków drugi WYBÓR będzie prawdopodobnie szybszy niż pierwszy WYBÓR oraz że zmiana kolejności dwóch klauzul EXISTS spowodowała drastyczne przyspieszenie w co najmniej jednym nadużywanym przypadku testowym, który właśnie stworzyliśmy.
Oczywistą rzeczą do zrobienia jest po prostu zmiana dwóch klauzul, ale chciałem zobaczyć, czy ktoś bardziej obeznany z programem SQL Server zechce to rozważyć. Wydaje mi się, że polegam na zbiegu okoliczności i „szczegółach implementacyjnych”.
(Wydaje się również, że gdyby SQL Server był mądrzejszy, wykonałby obie klauzule EXISTS równolegle i pozwoliłby, aby jedno z nich zakończyło się pierwszym zwarciem.)
Czy istnieje lepszy sposób, aby SQL Server konsekwentnie poprawiał czas wykonywania takiego zapytania?
Aktualizacja
Dziękuję za poświęcony mi czas i zainteresowanie moim pytaniem. Nie spodziewałem się pytań o rzeczywiste plany zapytań, ale chętnie je podzielę.
Dotyczy to komponentu oprogramowania obsługującego SQL Server 2008R2 i nowsze wersje. Kształt danych może być różny w zależności od konfiguracji i zastosowania. Mój współpracownik pomyślał o wprowadzeniu tej zmiany w zapytaniu, ponieważ dbf_1162761$z$rv$1257927703
tabela (w tym przykładzie) zawsze będzie miała większą lub równą liczbie wierszy niż dbf_1162761$z$dd$1257927703
tabela - czasami znacznie więcej (rzędów wielkości).
Oto nadużycie, o którym wspominałem. Pierwsze zapytanie jest wolne i trwa około 20 sekund. Drugie zapytanie kończy się natychmiast.
Dla tego, co jest warte, ostatnio dodano również bit „OPTYMALIZUJ NIEZNANY”, ponieważ wąchanie parametrów niszczy niektóre przypadki.
Oryginalne zapytanie:
SELECT CASE
WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)
Oryginalny plan:
|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
|--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
|--Constant Scan
|--Concatenation
|--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
| |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
| |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)
|--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
|--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]), WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
|--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]), WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)
Naprawione zapytanie:
SELECT CASE
WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)
Naprawiono plan:
|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
|--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
|--Constant Scan
|--Concatenation
|--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
| |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]), WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
| |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]), WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)
|--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
|--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
|--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)
źródło
Odpowiedzi:
Zasadniczo SQL Server wykonuje części
CASE
instrukcji w kolejności, ale może zmieniać kolejnośćOR
warunków. W przypadku niektórych zapytań można stale uzyskać lepszą wydajność, zmieniając kolejnośćWHEN
wyrażeń wCASE
instrukcji. Czasami można również uzyskać lepszą wydajność, zmieniając kolejność warunków wOR
instrukcji, ale nie jest to gwarantowane zachowanie.Prawdopodobnie najlepiej jest przejść przez prosty przykład. Testuję pod kątem SQL Server 2016, więc możliwe, że nie uzyskasz dokładnie takich samych wyników na swoim komputerze, ale o ile wiem, obowiązują te same zasady. Najpierw wstawię milion liczb całkowitych od 1 do 1000000 w dwóch tabelach, jedną z indeksem klastrowym i jedną jako stos:
Rozważ następujące zapytanie:
Wiemy, że ocena pod-zapytania
X_CI
będzie znacznie tańsza niż pod-zapytanieX_HEAP
, zwłaszcza gdy nie ma pasującego wiersza. Jeśli nie ma pasującego wiersza, musimy wykonać tylko kilka logicznych odczytów dla tabeli z indeksem klastrowym. Musimy jednak zeskanować wszystkie wiersze sterty, aby stwierdzić, że nie ma pasującego wiersza. Optymalizator też to wie. Mówiąc ogólnie, użycie indeksu klastrowego do wyszukania jednego wiersza jest bardzo tanie w porównaniu ze skanowaniem tabeli.Dla tych przykładowych danych napisałbym zapytanie w następujący sposób:
To skutecznie zmusza SQL Server do uruchomienia podkwerendy względem tabeli z indeksem klastrowym w pierwszej kolejności. Oto wyniki z
SET STATISTICS IO, TIME ON
:Patrząc na plan zapytań, jeśli wyszukiwanie na etykiecie 1 zwraca jakiekolwiek dane niż skanowanie na etykiecie 2 nie jest wymagane i nie nastąpi:
Następujące zapytanie jest znacznie mniej wydajne:
Patrząc na plan zapytań, widzimy, że skanowanie na etykiecie 2 zawsze się dzieje. Jeśli wiersz zostanie znaleziony, wyszukiwanie w etykiecie 1 zostanie pominięte. Nie takiej kolejności chcieliśmy:
Wyniki pokazują, że:
Wracając do pierwotnego zapytania, dla tego zapytania widzę wyszukiwanie i skanowanie oceniane w kolejności, która jest dobra dla wydajności:
W tym zapytaniu są one oceniane w odwrotnej kolejności:
Jednak w przeciwieństwie do poprzedniej pary zapytań, nic nie zmusza optymalizatora zapytań SQL Server do oceny jednego przed drugim. Nie powinieneś polegać na tym zachowaniu w niczym ważnym.
Podsumowując, jeśli potrzebujesz jednego podzapytania do oceny przed drugim, użyj
CASE
instrukcji lub innej metody wymuszenia kolejności. W przeciwnym razie możesz zamówić podkwerendy w dowolnie wybranymOR
stanie, ale wiedz, że nie ma gwarancji, że optymalizator wykona je w kolejności zgodnej z opisem .Uzupełnienie:
Naturalnym pytaniem jest to, co możesz zrobić, jeśli chcesz, aby SQL Server zdecydował, które zapytanie jest tańsze i najpierw go wykonał? Wszystkie dotychczasowe metody wydają się być zaimplementowane przez SQL Server w kolejności zapisywania zapytania, nawet jeśli nie jest to gwarantowane zachowanie niektórych z nich.
Oto jedna opcja, która wydaje się działać w przypadku prostych tabel demonstracyjnych:
Można znaleźć db skrzypce demo tutaj . Zmiana kolejności tabel pochodnych nie zmienia planu zapytań. W obu zapytaniach
X_HEAP
tabela nie jest dotykana. Innymi słowy, optymalizator zapytań wydaje się najpierw wykonywać tańsze zapytanie. Nie mogę polecić używania czegoś takiego w produkcji, więc jest tutaj głównie ze względu na ciekawość. Może być znacznie prostszy sposób na osiągnięcie tego samego.źródło
CASE WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000 UNION ALL SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 ELSE 0 END
może być alternatywą, choć nadal zależy to od ręcznego decydowania, które zapytanie jest szybsze i stawiania go na pierwszym miejscu. Nie jestem pewien, czy istnieje sposób na wyrażenie tego, aby program SQL Server automatycznie zmienił kolejność, tak aby tani był automatycznie oceniany jako pierwszy.