🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

Indeksy i wydajność SQL — dlaczego zapytanie jest wolne

Indeksy SQL — Table Scan bez indeksu (~5 s) kontra Index Seek z indeksem B-tree (~5 ms)

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 Kategoria nic nie da zapytaniu filtrującemu po Cena.

Gdzie indeksy pomagają

Indeks warto rozważyć dla kolumn używanych w:

  • WHERE — filtrowanie po kolumnie
  • JOIN ... 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 SSMS

W 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

SytuacjaIndeks 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.

👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak NFZ, Kamsoft, Diagnostyka, Hermes Reply Polska czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

🚀 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.

Dodaj komentarz

czytanie to początek

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ę →