kolejność klauzul w „ISTNIEJE (…) LUB ISTNIEJE (…)”

11

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$1257927703tabela (w tym przykładzie) zawsze będzie miała większą lub równą liczbie wierszy niż dbf_1162761$z$dd$1257927703tabela - 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)
jr
źródło
1
Powiązane pytania i odpowiedzi: Fizyczna operacja konkatenacji: Czy gwarantuje kolejność wykonania?
Paul White 9

Odpowiedzi:

11

Zasadniczo SQL Server wykonuje części CASEinstrukcji w kolejności, ale może zmieniać kolejność ORwarunków. W przypadku niektórych zapytań można stale uzyskać lepszą wydajność, zmieniając kolejność WHENwyrażeń w CASEinstrukcji. Czasami można również uzyskać lepszą wydajność, zmieniając kolejność warunków w ORinstrukcji, 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:

CREATE TABLE dbo.X_HEAP (ID INT NOT NULL, FLUFF VARCHAR(100));

INSERT INTO dbo.X_HEAP  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

CREATE TABLE dbo.X_CI (ID INT NOT NULL, FLUFF VARCHAR(100), PRIMARY KEY (ID));

INSERT INTO dbo.X_CI  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

Rozważ następujące zapytanie:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

Wiemy, że ocena pod-zapytania X_CIbędzie znacznie tańsza niż pod-zapytanie X_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:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
ELSE 0 END;

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:

Tabela „X_CI”. Liczba skanów 0, logiczne odczyty 3, fizyczne odczyty 0

Czasy wykonania programu SQL Server: czas procesora = 0 ms, czas, który upłynął = 0 ms.

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:

dobre zapytanie

Następujące zapytanie jest znacznie mniej wydajne:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
ELSE 0 END
OPTION (MAXDOP 1);

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:

zły plan zapytania

Wyniki pokazują, że:

Tabela „X_HEAP”. Liczba skanów 1, logiczne odczyty 7247

Czasy wykonania programu SQL Server: czas procesora = 15 ms, czas, który upłynął = 22 ms.

Wracając do pierwotnego zapytania, dla tego zapytania widzę wyszukiwanie i skanowanie oceniane w kolejności, która jest dobra dla wydajności:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

W tym zapytaniu są one oceniane w odwrotnej kolejności:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
THEN 1 ELSE 0 END;

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 CASEinstrukcji lub innej metody wymuszenia kolejności. W przeciwnym razie możesz zamówić podkwerendy w dowolnie wybranym ORstanie, 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:

SELECT CASE
  WHEN EXISTS (
    SELECT 1
    FROM (
        SELECT TOP 2 1 t
        FROM 
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_HEAP 
            WHERE ID = 50000 
        ) h
        CROSS JOIN
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_CI
            WHERE ID = 50000
        ) ci
    ) cnt
    HAVING COUNT(*) = 2
)
THEN 1 ELSE 0 END;

Można znaleźć db skrzypce demo tutaj . Zmiana kolejności tabel pochodnych nie zmienia planu zapytań. W obu zapytaniach X_HEAPtabela 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.

Joe Obbish
źródło
4
Lub 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 ENDmoż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.
Martin Smith