Jak czytać plan zapytania SQL: znajdź, dlaczego zapytanie jest wolne (i napraw to) w pięciu silnikach

Czytaj plan zapytania SQL, by znaleźć, czemu jest wolne: scan vs seek, estymaty vs rzeczywistość, spille i brakujące indeksy — w pięciu silnikach.

Wolne zapytanie rzadko samo mówi, dlaczego jest wolne. Plan zapytania — owszem. To przepis krok po kroku, który optymalizator bazy wybrał dla Twojego SQL-a: które tabele odwiedza, w jakiej kolejności, których indeksów użył (a które zignorował) i gdzie naprawdę poszedł czas. Umiejętność czytania planu to pojedyncza najbardziej dźwigniowa kompetencja w strojeniu SQL — a jej podstawy przenoszą się między wszystkimi silnikami.

Ten przewodnik pokazuje, jak wygenerować plan w PostgreSQL, SQL Server, MySQL/MariaDB, Oracle i SQLite oraz — co ważniejsze — na co patrzeć, gdy już go masz.

Dlaczego moje zapytanie SQL jest wolne? Czytaj plan, nie zapytanie

Wpatrywanie się w tekst SQL i zgadywanie to najczęstszy i najmniej produktywny sposób strojenia. Optymalizator już podjął decyzje — Twoim zadaniem jest je przejrzeć. Plan odpowiada na trzy pytania, na które sam tekst zapytania nie odpowie:

  1. Metoda dostępu — czy silnik przeskoczył (seek) wprost do potrzebnych wierszy, czy przeskanował (scan) całą tabelę/indeks, żeby je znaleźć?
  2. Estymaty kontra rzeczywistość — czy optymalizator spodziewał się 10 wierszy, a dostał 10 milionów? Złe estymaty rodzą złe plany.
  3. Kosztowne operacje — sortowania, hash joiny i agregacje, które wylały się na dysk (spill), albo nested loop wykonany miliony razy.

Niemal każde wolne zapytanie sprowadza się do jednej z tych trzech rzeczy. Trzymaj je w głowie podczas lektury.

Jak wygenerować plan zapytania w każdym silniku?

Jest tu kluczowy podział: polecenia estymujące nie uruchamiają zapytania, a warianty „analyze” wykonują je i raportują prawdziwe liczby. Przy DELETE czy UPDATE ta różnica nie jest akademicka.

PostgreSQL — EXPLAIN estymuje; EXPLAIN ANALYZE uruchamia i dodaje rzeczywiste czasy. Dodaj BUFFERS, by zobaczyć I/O:

EXPLAIN SELECT ...;                       -- plan + estymaty, NIE uruchamia
EXPLAIN ANALYZE SELECT ...;               -- uruchamia: rzeczywisty czas + wiersze
EXPLAIN (ANALYZE, BUFFERS) SELECT ...;    -- + rozliczenie buforów shared/read

(Dokumentacja PostgreSQL: EXPLAIN, Using EXPLAIN. Od PostgreSQL 18 BUFFERS jest domyślnie włączone razem z ANALYZE.)

SQL Server — plan szacowany nigdy nie uruchamia zapytania; plan rzeczywisty tak:

SET SHOWPLAN_XML ON;      -- plan szacowany jako XML, NIE uruchamia zapytania
SET STATISTICS XML ON;    -- uruchamia każdą instrukcję, zwraca plan rzeczywisty + metryki

W SSMS naciśnij Ctrl+L dla planu szacowanego albo Ctrl+M („Include Actual Execution Plan”) i uruchom zapytanie (SET STATISTICS XML, Display an actual execution plan).

MySQL 8.0.18+ — EXPLAIN estymuje; EXPLAIN ANALYZE uruchamia zapytanie i drukuje drzewo, w którym liczby szacowane i rzeczywiste stoją obok siebie:

EXPLAIN SELECT ...;              -- estymata (dodaj FORMAT=TREE lub FORMAT=JSON)
EXPLAIN ANALYZE SELECT ...;      -- uruchamia: rzeczywisty czas, wiersze, pętle na węzeł

EXPLAIN ANALYZE pojawiło się w 8.0.18 (MySQL: wpis o EXPLAIN ANALYZE, dokumentacja EXPLAIN). MariaDB formę wykonującą zapisuje jako ANALYZE SELECT ... i dokłada kolumnę r_rows (rzeczywiste) obok rows (szacowane).

Oracle — dwa kroki dla estymaty; hint plus DISPLAY_CURSOR dla wartości rzeczywistych:

-- Tylko estymata:
EXPLAIN PLAN FOR SELECT ...;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

-- Rzeczywiste (uruchom z hintem, potem wyświetl kursor):
SELECT /*+ GATHER_PLAN_STATISTICS */ ... FROM ...;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(FORMAT=>'ALLSTATS LAST'));

Forma rzeczywista daje obok siebie E-Rows (szacowane) i A-Rows (rzeczywiste) dla każdego kroku — najcenniejsza rzecz w planie Oracle (blog Oracle Optimizer: jak wygenerować użyteczny plan, use-the-index-luke: Oracle EXPLAIN PLAN).

SQLite — lekki i tylko estymujący:

EXPLAIN QUERY PLAN SELECT ...;

(SQLite: EXPLAIN QUERY PLAN)

Czym różni się scan od seek?

To najbardziej fundamentalne rozróżnienie w każdym planie. Seek używa indeksu (lub klucza), by skoczyć wprost do pasujących wierszy — praca proporcjonalna do liczby zwróconych wierszy. Scan czyta całą tabelę lub indeks i odrzuca to, co nie pasuje — praca proporcjonalna do rozmiaru tabeli.

  • SQL Server: Index Seek / Clustered Index Seek (celny, zwykle dobry) kontra Index Scan, Clustered Index Scan, Table Scan (czyta wszystko).
  • PostgreSQL: Index Scan / Index Only Scan / Bitmap Index Scan kontra Seq Scan (sekwencyjny = cała tabela).
  • Oracle: INDEX RANGE SCAN/UNIQUE SCAN kontra TABLE ACCESS FULL.
  • SQLite: SEARCH ... USING INDEX (oraz USING COVERING INDEX) kontra SCAN (cała tabela).

Scan nie jest automatycznie zły. Gdy zapytanie faktycznie potrzebuje większości tabeli — raport za cały miniony rok — scan jest właściwym wyborem, a seek po indeksie byłby wolniejszy. Scan jest problemem, gdy spodziewałeś się seeka: selektywne WHERE id = 42, które skanuje milionową tabelę, oznacza brak indeksu, jego nieużywalność (np. funkcja opakowuje kolumnę: WHERE UPPER(email) = ...) albo złą estymatę optymalizatora, który wybrał źle.

Estymaty vs rzeczywistość: liczba, która tłumaczy większość złych planów

Optymalizator dobiera strategię na podstawie estymat. Jeśli estymata jest dramatycznie błędna, każda kolejna decyzja — typ joina, kolejność joinów, przydział pamięci — opiera się na fałszywym założeniu. Dlatego porównanie szacowane-kontra-rzeczywiste to pierwsza rzecz, na którą patrzą doświadczeni stroiciele:

  • PostgreSQL EXPLAIN ANALYZE: rows= (estymata) vs actual ... rows=.
  • Oracle: E-Rows vs A-Rows.
  • Plan rzeczywisty SQL Server: „Estimated Number of Rows” vs „Actual Number of Rows” na każdym operatorze.
  • MariaDB ANALYZE: rows vs r_rows.

Rozjazd o rząd czy dwa to dymiący pistolet. Typowe przyczyny: nieaktualne statystyki (napraw przez ANALYZE / UPDATE STATISTICS / DBMS_STATS), skorelowane kolumny, które optymalizator uznaje za niezależne, albo predykaty non-sargable. Tu estymata Postgresa myli się 1000-krotnie:

Seq Scan on orders  (cost=0.00..18334.00 rows=1000 width=52)
                    (actual time=0.015..512.3 rows=1000000 loops=1)

Spodziewano się 1 000 wierszy, padło 1 000 000 — i do tego wybrano skan sekwencyjny. Ten rozjazd to Twój trop.

Co mówią spille, sortowania i hashe?

Gdy operator potrzebuje więcej pamięci roboczej, niż mu przydzielono, wylewa się na dysk (spill) — a dysk jest o rzędy wielkości wolniejszy od pamięci. Plany mówią o tym wprost:

  • PostgreSQL pokazuje metodę sortowania: Sort Method: quicksort Memory: 25kB (dobrze) kontra Sort Method: external merge Disk: 10240kB (spill). Hash joiny raportują Batches: większe niż 1, gdy się wylewają.
  • SQL Server oznacza spill żółtym trójkątem ostrzegawczym na operatorze Sort lub Hash w planie rzeczywistym („operator used tempdb to spill data”).

Wylewające się sortowanie zwykle znaczy albo że zapytanie sortuje znacznie więcej, niż trzeba (sortujesz przed filtrowaniem?), albo że silnik niedoszacował liczby wierszy i przydzielił za mało pamięci — co znów prowadzi do problemu z estymatami powyżej. Lekarstwem bywa indeks dostarczający wiersze już posortowane, który eliminuje sortowanie w całości.

Co plan mówi o brakujących indeksach?

Niektóre silniki sugerują indeksy wprost. SQL Server drukuje w planie rzeczywistym zieloną podpowiedź „Missing Index” z gotową instrukcją CREATE INDEX; PostgreSQL nie ma wbudowanego doradcy, ale Seq Scan z selektywnym filtrem na nieindeksowanej kolumnie to ten sam sygnał. Traktuj sugestie jako punkt wyjścia, nigdy jako ślepe CREATE:

  • Sugerowany indeks może dublować istniejący z nieco inną kolejnością kolumn.
  • Każdy dodany indeks spowalnia każdy INSERT/UPDATE/DELETE i kosztuje miejsce.
  • Prawdziwym fiksem bywa przepisanie predykatu non-sargable (WHERE UPPER(email) = ... → przechowuj znormalizowane albo dodaj indeks funkcyjny), by istniejący indeks stał się użyteczny.

Jak czytać plany spójnie w różnych silnikach?

Koncepcje powyżej są uniwersalne, ale polecenia, format wyjścia i słowa kluczowe — już nie. Seq Scan, TABLE ACCESS FULL i SCAN znaczą to samo, a liczby rzeczywiste-kontra-szacowane mieszkają w innej kolumnie w każdym narzędziu. Jeśli pracujesz na wielu silnikach, ten podatek od tłumaczenia jest realny — i nie dotyczy tylko planów zapytań: paginacja, cytowanie identyfikatorów i typy danych różnią się między silnikami równie mocno.

Ujawnienie: ten blog wydaje LumaSoft (LUMA sp. z o.o.), a ja pracuję przy opisanym tu narzędziu.

SQL Sage for DBeaver to plugin, który czyta plan zapytania wewnątrz DBeavera i jest świadomy dialektu w tych samych pięciu silnikach — PostgreSQL, MySQL/MariaDB, Oracle, SQL Server i SQLite — więc pokazywany i objaśniany plan pasuje do silnika, z którym jesteś połączony. Kilka faktów wprost: jest tylko dla Windows (Linux i macOS nie są wspierane), jest keyless (logujesz się własnym kontem AI, bez klucza API) i działa read-only domyślnie. Nie jest powiązany z DBeaverem, Oracle ani Microsoftem.

Wnioski: lista kontrolna do każdego planu

Niezależnie od silnika i narzędzia, czytaj każdy plan tak samo:

  1. Wygeneruj plan rzeczywisty (EXPLAIN ANALYZE / plan rzeczywisty / DISPLAY_CURSOR), nie samą estymatę — prawdziwe liczby biją zgadywanie.
  2. Sprawdź estymaty vs wiersze rzeczywiste. Duży rozjazd to zwykle źródło problemu; podejrzewaj najpierw nieaktualne statystyki.
  3. Znajdź skany. Pytaj, czy każdy z nich jest uzasadniony liczbą wierszy, których zapytanie naprawdę potrzebuje.
  4. Szukaj spilli — sortowania na dysku i wielowsadowe hashe oznaczają presję pamięci lub nadmiarowe sortowanie.
  5. Podpowiedź o brakującym indeksie traktuj jak hipotezę, nie polecenie.

Zrób to kilkanaście razy, a czytanie planów przestanie być magią. Zapytanie przestaje być tajemnicą i staje się przepisem, który umiesz przeczytać — i naprawić.

← Wróć do bloga