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:
- Metoda dostępu — czy silnik przeskoczył (seek) wprost do potrzebnych wierszy, czy przeskanował (scan) całą tabelę/indeks, żeby je znaleźć?
- Estymaty kontra rzeczywistość — czy optymalizator spodziewał się 10 wierszy, a dostał 10 milionów? Złe estymaty rodzą złe plany.
- 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 ...;
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) kontraIndex Scan,Clustered Index Scan,Table Scan(czyta wszystko). - PostgreSQL:
Index Scan/Index Only Scan/Bitmap Index ScankontraSeq Scan(sekwencyjny = cała tabela). - Oracle:
INDEX RANGE SCAN/UNIQUE SCANkontraTABLE ACCESS FULL. - SQLite:
SEARCH ... USING INDEX(orazUSING COVERING INDEX) kontraSCAN(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) vsactual ... rows=. - Oracle:
E-RowsvsA-Rows. - Plan rzeczywisty SQL Server: „Estimated Number of Rows” vs „Actual Number of Rows” na każdym operatorze.
- MariaDB
ANALYZE:rowsvsr_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) kontraSort 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/DELETEi 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:
- Wygeneruj plan rzeczywisty (
EXPLAIN ANALYZE/ plan rzeczywisty /DISPLAY_CURSOR), nie samą estymatę — prawdziwe liczby biją zgadywanie. - Sprawdź estymaty vs wiersze rzeczywiste. Duży rozjazd to zwykle źródło problemu; podejrzewaj najpierw nieaktualne statystyki.
- Znajdź skany. Pytaj, czy każdy z nich jest uzasadniony liczbą wierszy, których zapytanie naprawdę potrzebuje.
- Szukaj spilli — sortowania na dysku i wielowsadowe hashe oznaczają presję pamięci lub nadmiarowe sortowanie.
- 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ć.