Indeksy i wydajność SQL — dlaczego zapytanie jest wolne

To samo zapytanie potrafi wykonać się w 5 milisekund albo w 5 sekund. Różnica prawie zawsze sprowadza się do jednego: czy baza ma indeks, którego może użyć. „Masz wolne zapytanie w produkcji — co robisz?” to pytanie, które pada na rozmowie na juniora i na mid-level. Po opanowaniu zapytań i JOIN-ów to temat, który odróżnia kogoś, kto „umie SQL”, od kogoś, kto rozumie, co baza robi z jego zapytaniem.
Czym jest indeks
Wyobraź sobie szukanie hasła w książce bez skorowidza — musisz przejrzeć każdą stronę. To full table scan: baza czyta każdy wiersz, żeby znaleźć pasujące. Indeks to skorowidz — posortowana struktura (najczęściej B-tree), która pozwala przeskoczyć od razu do właściwego miejsca, zamiast przeglądać całość.
CREATE INDEX IX_Produkty_Kategoria ON Produkty(Kategoria);Od tej chwili WHERE Kategoria = 'Peryferia' może użyć indeksu (index seek) zamiast skanować całą tabelę (table scan). Na tabeli z milionem wierszy to różnica między odczytem kilkunastu stron a odczytem wszystkich.
Indeks nie przyspiesza „bazy”. Przyspiesza konkretne zapytania, których warunki pasują do jego kolumn. Indeks na
Kategorianic nie da zapytaniu filtrującemu poCena.
Gdzie indeksy pomagają
Indeks warto rozważyć dla kolumn używanych w:
WHERE— filtrowanie po kolumnieJOIN ... ON— łączenie po kluczu (klucze obce prawie zawsze warto indeksować)ORDER BY— sortowanie po kolumnie- ograniczeniach
UNIQUE— baza tworzy indeks automatycznie
Klucz główny dostaje indeks automatycznie. Klucze obce — nie (w większości baz), a to właśnie one są najczęściej pomijaną przyczyną wolnych JOIN-ów.
Clustered vs non-clustered (SQL Server)
- Clustered — określa fizyczną kolejność wierszy w tabeli. Może być tylko jeden (bo dane da się ułożyć na dysku tylko raz); domyślnie to klucz główny.
- Non-clustered — osobna struktura ze wskaźnikami do wierszy. Może być ich wiele.
W praktyce większość indeksów, które tworzysz ręcznie, to non-clustered na kolumnach z WHERE/JOIN.
Plan zapytania — jak sprawdzić, co baza naprawdę robi
Nie zgaduj, czy indeks jest używany — sprawdź plan wykonania:
EXPLAIN SELECT * FROM Produkty WHERE Kategoria = 'Kable'; -- PostgreSQL / MySQL
-- SQL Server: włącz "Include Actual Execution Plan" w SSMSW planie szukasz słów Seek (dobrze — baza skoczyła po indeksie) kontra Scan (baza przeszła całą tabelę). Index Seek na dużej tabeli to cel; Table Scan przy selektywnym WHERE to sygnał brakującego indeksu.
Indeks złożony i kolejność kolumn
Indeks może obejmować kilka kolumn — i kolejność ma znaczenie:
CREATE INDEX IX_Zam_Klient_Data ON Zamowienia(KlientId, Data);Ten indeks wspiera WHERE KlientId = 5 oraz WHERE KlientId = 5 AND Data > '2026-01-01', ale nie samo WHERE Data > '2026-01-01' — bo baza czyta indeks od lewej kolumny. To jak szukanie w książce telefonicznej po samym imieniu: posortowana jest po nazwisku, więc imię nic nie daje.
Cena indeksów
Indeksy nie są darmowe. Każdy INSERT, UPDATE i DELETE musi zaktualizować także indeksy — więc zapisy zwalniają, a indeksy zajmują miejsce na dysku. Dlatego nie indeksujesz „wszystkiego na wszelki wypadek”: indeks zakłada się pod realne, wolne zapytania, nie profilaktycznie.
Most do .NET — problem N+1
Najczęstszy problem wydajnościowy w aplikacji .NET nie jest brakiem indeksu — jest N+1 zapytaniami z EF Core. Pętla, która dla każdego elementu osobno sięga do bazy, generuje setki zapytań zamiast jednego:
foreach (var z in db.Zamowienia.ToList()) // 1 zapytanie
var imie = z.Klient.Imie; // +1 zapytanie na każdą iteracjęŻaden indeks tego nie naprawi — bo problemem jest liczba round-tripów do bazy, nie prędkość pojedynczego. Rozwiązanie to Include (jeden JOIN), opisane przy JOIN-ach. Indeks i N+1 to dwie różne warstwy tego samego pytania „czemu wolno”.
Pułapki rekrutacyjne
Pułapka #1 — funkcja na kolumnie zabija indeks. WHERE YEAR(Data) = 2026 nie użyje indeksu na Data — bo baza musi policzyć YEAR(...) dla każdego wiersza, więc znów skanuje całość. Zapisz to jako zakres: WHERE Data >= '2026-01-01' AND Data < '2027-01-01'. To samo dotyczy WHERE UPPER(Nazwa) = ... i konwersji typów.
Pułapka #2 — SELECT * blokuje indeks pokrywający. Indeks pokrywający (covering) zawiera wszystkie kolumny potrzebne zapytaniu, więc baza nie musi w ogóle sięgać do tabeli. SELECT * pobiera kolumny spoza indeksu i wymusza dodatkowy odczyt (key lookup) — kolejny powód, by nie używać *.
Pułapka #3 — indeks na kolumnie o niskiej selektywności. Indeks na kolumnie bool (Aktywny: true/false) albo Plec prawie nic nie daje — połowa tabeli i tak pasuje, więc baza wybierze skan. Indeksy działają na kolumnach o wielu różnych wartościach (email, id, data).
Pułapka #4 — mylenie „wolnego SQL” z N+1. Kandydat dodaje indeks, a aplikacja dalej muli, bo prawdziwym problemem było 400 zapytań z pętli w kodzie C#. Najpierw ustal, ile zapytań idzie do bazy (profiler EF Core, logi), potem czy pojedyncze są wolne.
Kiedy indeks pomaga, a kiedy nie
| Sytuacja | Indeks pomaga? |
|---|---|
WHERE / JOIN / ORDER BY po kolumnie | ✅ tak |
| klucz obcy używany w JOIN | ✅ tak (a domyślnie go nie ma) |
funkcja na kolumnie (YEAR(Data)) | ❌ nie — skan całości |
| kolumna o niskiej selektywności (bool, płeć) | ❌ nie |
| tabela z intensywnymi zapisami | ⚠️ kompromis — wolniejszy INSERT/UPDATE |
Podsumowanie
Indeks to posortowana struktura (B-tree), która zamienia przeglądanie całej tabeli (scan) w skok do właściwego miejsca (seek) — i to on decyduje, czy zapytanie trwa milisekundy czy sekundy. Zakładasz go na kolumny z WHERE, JOIN i ORDER BY (zwłaszcza klucze obce), w indeksie złożonym pilnujesz kolejności kolumn, a to, czy działa, sprawdzasz w planie zapytania (Seek vs Scan) — nie zgadujesz. Indeksy kosztują przy zapisie, więc zakładasz je pod realne wolne zapytania. A w aplikacji .NET pamiętasz, że najczęstszy problem wydajności to nie brak indeksu, lecz N+1 zapytań z EF Core — inna warstwa, inne rozwiązanie.
Co dalej
Zapytania są szybkie i poprawne — teraz muszą być bezpieczne. SQL injection i parametryzacja w .NET pokazuje, jak wysyłać SQL z aplikacji, nie otwierając bazy na atak. Warto też wrócić do podstaw zapytań, jeśli GROUP BY jeszcze nie leży, i zobaczyć, jak Entity Framework Core pozwala podglądać wygenerowany SQL, żeby wychwycić brakujące indeksy.
🚀 Co dalej?
Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
Zamień wiedzę w umiejętności
Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.
Pobieram roadmapę →