Jak zoptymalizować wolny SELECT w SQL Server: praktyczna checklista
Praktyczna checklista, jak przyspieszyć wolny SELECT w SQL Server: mierz przez STATISTICS IO i plan, pisz predykaty sargowalne i indeksuj właściwe kolumny.
Wolny SELECT rzadko bywa zagadką, gdy przestaniesz zgadywać, a zaczniesz mierzyć. SQL Server już wie, gdzie poszedł czas — Twoim zadaniem jest zadać mu właściwe pytania, we właściwej kolejności, i zmieniać jedną rzecz naraz. To praktyczna checklista, jak zrobić dokładnie to w SQL Server i SSMS, z krótkimi uwagami, gdzie inne silniki różnią się od reszty. Przechodź ją z góry na dół — większość wolnych zapytań poddaje się po pierwszych dwóch–trzech krokach.
Od czego zacząć, gdy SELECT jest wolny?
Nie od przepisywania zapytania. Zacznij od pomiaru, żebyś miał punkt odniesienia do pobicia i liczbę, która powie, czy zmiana naprawdę pomogła.
Dwa ustawienia sesji dają Ci tę liczbę wprost w SSMS:
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
-- tutaj uruchom swój SELECT
SET STATISTICS IO „wyświetla informacje o ilości fizycznej i logicznej aktywności IO generowanej przez instrukcje Transact-SQL”; logical reads to „liczba stron odczytanych z pamięci podręcznej danych” (data cache), a physical reads to „liczba stron odczytanych z dysku” (SET STATISTICS IO). SET STATISTICS TIME „wyświetla liczbę milisekund potrzebnych do sparsowania, skompilowania i wykonania każdej instrukcji” (SET STATISTICS TIME).
Logical reads to metryka, na której warto się zakotwiczyć. Nie skaczą zależnie od tego, jak ciepły jest cache ani jak obciążony serwer — więc gdy ta liczba spada po zmianie, zapytanie realnie wykonuje mniej pracy, nie tylko mniej pracy dzisiaj. Zestaw to z planem wykonania, który pokazuje, dlaczego odczytów jest tyle. Czytanie planu to osobna umiejętność — jeśli scan kontra seek, estymaty kontra wiersze rzeczywiste i spille nie są jeszcze Twoim odruchem, zacznij od tekstu jak czytać plan zapytania SQL i wróć tutaj.
Dlaczego SQL Server nie chce użyć mojego indeksu?
Najczęstsza odpowiedź: Twój predykat nie jest sargowalny (sargable). To termin samego Microsoftu — „predykat typu Search ARGumentable, który potrafi użyć indeksu, by przyspieszyć wykonanie zapytania” (przewodnik projektowania indeksów).
Reguła jest prosta: trzymaj indeksowaną kolumnę nagą po jej stronie porównania. W chwili, gdy owiniesz ją funkcją lub wyrażeniem, optymalizator nie może już użyć indeksu jako wyszukiwania i zjeżdża do skanowania całości.
-- Non-sargable: funkcja ukrywa kolumnę przed indeksem
WHERE YEAR(order_date) = 2026
-- Sargowalnie: zakres, po którym indeks zrobi seek
WHERE order_date >= '2026-01-01' AND order_date < '2027-01-01'
Ta sama pułapka to WHERE UPPER(email) = @e, albo WHERE price * 1.23 > 100, albo niejawna konwersja, gdy porównujesz kolumnę nvarchar z parametrem varchar (SQL Server może konwertować kolumnę, nie literał). Przepisz predykat tak, by kolumna stała sama; a jeśli funkcja jest naprawdę potrzebna — od tego jest kolumna wyliczana (computed column) z indeksem na niej. Ten mechanizm jest wspólny dla silników — w PostgreSQL objawem jest Seq Scan tam, gdzie spodziewałeś się Index Scan, a lekarstwo jest to samo albo indeks funkcyjny.
Czy warto unikać SELECT *?
Tak, ale z ostrzejszego powodu niż „bo nieelegancko”. To, o jakie kolumny prosisz, decyduje, czy indeks może zaspokoić zapytanie samodzielnie. „Gdy indeks zawiera wszystkie kolumny, do których odwołuje się zapytanie, mówi się zwykle, że pokrywa zapytanie (covering the query)“, a zysk jest konkretny: „optymalizator potrafi zlokalizować wszystkie wartości kolumn w samym indeksie; dane tabeli ani indeksu klastrowego nie są odwiedzane, co skutkuje mniejszą liczbą operacji dyskowych I/O” (indeksy z kolumnami included).
SELECT * wciąga kolumny, których indeks nie ma, co zmusza SQL Server do powrotu do tabeli po każdy wiersz (key lookup) — często pojedyncze największe źródło zbędnych odczytów w skądinąd dobrze zaindeksowanym zapytaniu. Proś o kolumny, których faktycznie używasz, a potem zbuduj indeks, który je pokrywa: kolumny wyszukiwania i joinów w kluczu, resztę w INCLUDE:
CREATE NONCLUSTERED INDEX IX_Orders_Customer_Date
ON dbo.Orders (customer_id, order_date)
INCLUDE (status, total_amount);
Kolumny klucza (customer_id, order_date) czynią predykat zdatnym do seeka; kolumny included jadą na gapę, więc zapytanie nigdy nie dotyka tabeli bazowej. Tylko uczciwie — Microsoft ostrzega, by „unikać tworzenia indeksu pokrywającego ze zbyt wieloma kolumnami, bo to osłabia jego korzyść, a jednocześnie napełnia miejsce na dysku, I/O i pamięć”.
Czy po prostu stworzyć indeks, który sugeruje SQL Server?
SSMS często pokaże w planie zieloną podpowiedź „Missing Index”, a te same sugestie żyją w DMV sys.dm_db_missing_index_details, sys.dm_db_missing_index_groups i sys.dm_db_missing_index_group_stats (referencja DMV brakujących indeksów). Traktuj je jako trop, nigdy jako polecenie. Microsoft jest wprost co do ograniczeń:
- Sugestie są „oparte na estymatach zrobionych podczas optymalizacji pojedynczego zapytania, przed jego wykonaniem” — nie są weryfikowane po fakcie.
- „Kolumny klucza są sugerowane, ale sugestia nie określa ich kolejności” — a to kolejność kolumn decyduje o zdatności indeksu do seeka.
- Funkcja „sugeruje wyłącznie nieklastrowe, dyskowe indeksy rowstore”, pomija plany trywialne i jest „mniej dokładna dla zapytań z samymi predykatami nierównościowymi”.
Ich konkluzja jest tą do zapamiętania: „sugestie brakujących indeksów najlepiej traktować jako jedno z kilku źródeł informacji… nie są receptą na tworzenie indeksów dokładnie tak, jak zasugerowano” (strojenie indeksów nieklastrowych). Zanim uruchomisz sugerowane CREATE INDEX, sprawdź, czy istniejący indeks już go pokrywa po małej korekcie, uporządkuj kolumny klucza wg selektywności (najbardziej selektywne pierwsze) i pamiętaj, że każdy dodany indeks spowalnia każdy INSERT, UPDATE i DELETE.
Dlaczego to samo zapytanie jest raz szybkie, raz wolne?
Dwaj zwykli podejrzani — oba o liczby, na których planuje optymalizator.
Nieaktualne statystyki. „Optymalizator zapytań używa statystyk do tworzenia planów, które poprawiają wydajność zapytań” — konkretnie do „oszacowania kardynalności, czyli liczby wierszy w wyniku zapytania”, co pozwala mu wybrać np. „operator index seek zamiast bardziej zasobożernego index scan” (Statystyki). Gdy statystyki zostają daleko w tyle za danymi, estymata jest błędna, a każda kolejna decyzja dziedziczy ten błąd. Duży rozjazd między wierszami szacowanymi a rzeczywistymi w planie to sygnał; lekarstwem jest UPDATE STATISTICS (albo pozwolenie, by AUTO_UPDATE_STATISTICS zrobił swoje).
Parameter sniffing. „Wrażliwość na parametry, znana też jako »parameter sniffing«, to proces, w którym SQL Server »wącha« bieżące wartości parametrów podczas kompilacji… aby mogły posłużyć do wygenerowania potencjalnie wydajniejszych planów wykonania” (przewodnik architektury przetwarzania zapytań). Zwykle pomocne — ale buforowany plan jest ukształtowany przez te wartości, które go skompilowały. Plan zbudowany dla parametru pasującego do 10 wierszy może źle leżeć, gdy następne wywołanie trafia w 10 milionów, i na odwrót. Jeśli procedura składowana jest szybka dla jednych wejść, a wolna dla innych bez zmiany danych — to tutaj szukaj.
A co z joinami i problemem N+1?
Jedno dobrze napisane zapytanie bije pętlę małych. Wzorzec „N+1” — pobierz listę, a potem odpal osobny SELECT na każdy wiersz — zamienia jeden round trip w setki i niweczy każdy zbudowany indeks. Zastąp pętlę pojedynczym joinem i pozwól optymalizatorowi dobrać kolejność i metodę joina; planuje całą instrukcję naraz, czego nie zrobi przez N osobnych wywołań. Upewnij się, że kolumny joina są zaindeksowane po obu stronach, i sprawdź, czy plan nie rozdmuchuje wierszy przez join, zanim późny filtr wyrzuci większość z nich. Jeśli estymaty wierszy wyglądają tu źle — wracasz do kroku ze statystykami powyżej.
Checklista
Dla każdego wolnego SELECT, po kolei:
- Najpierw mierz —
SET STATISTICS IO/TIME ON, zapisz logical reads i plan rzeczywisty jako punkt odniesienia. - Uczyń predykaty sargowalnymi — trzymaj indeksowane kolumny nagie; zamień
YEAR(col) = …na zakres. - Przestań wybierać kolumny, których nie używasz — a potem pokryj te, które zostają, indeksem klucz +
INCLUDE. - Podpowiedź o brakującym indeksie traktuj jak hipotezę — uporządkuj klucz wg selektywności, scal z istniejącymi indeksami, zważ koszt zapisu.
- Sprawdź estymaty — nieaktualne statystyki i parameter sniffing tłumaczą większość zapytań „czasem wolnych”.
- Zwiń N+1 do jednego joina — i zaindeksuj obie jego strony.
Zmień jedną rzecz, uruchom ponownie, porównaj logical reads. Jeśli liczba nie drgnęła — cofnij zmianę i idź dalej; strojenie to sprawdzanie, nie zgadywanie. (Zanim sięgniesz po zmiany schematu, jak nowy indeks, warto też wiedzieć, co jeszcze zależy od obiektów, które zaraz ruszysz.)
Ujawnienie: ten blog wydaje LumaSoft (LUMA sp. z o.o.), a ja pracuję przy opisanym tu narzędziu.
SQL Sage dla SSMS to add-in do SQL Server Management Studio 22 na Windows 10/11, który pisze, objaśnia i optymalizuje SQL wewnątrz edytora, którego już używasz — tego samego, w którym uruchamiasz SET STATISTICS IO i czytasz plan. Kilka faktów wprost: jest keyless (logujesz się własnym kontem Claude lub ChatGPT/Codex, bez klucza API — albo użyj własnego klucza Anthropic), działa read-only domyślnie (SELECT się wykonuje; każdy UPDATE/DELETE/DDL czeka na Twoje jawne potwierdzenie) i wysyła do modelu tylko Twój schemat i tekst SQL — nigdy wierszy danych, chyba że włączysz to na daną sesję. Jest 30-dniowy trial bez karty, potem darmowy tier; płatne plany Personal od 29 USD/rok lub 39 USD dożywotnio, sprzedawane przez Paddle. Nie jest powiązany z Microsoftem, Anthropic ani OpenAI. Ale prawdziwym wnioskiem jest checklista powyżej — działa niezależnie od tego, czym stroisz.