Spis treści
Cześć, w tym artykule spróbuje umieścić streszczenie czym są indeksy w bazie danych z większym naciskiem jak to jest w Postgres. Otóż zacznijmy 🙂
Czym w ogóle jest indeks?
No dobra, wyobraź sobie że masz książkę telefoniczną. Jak szukasz kogoś po nazwisku to jest git, bo wszystko jest po kolei alfabetycznie. Ale jak szukasz kogoś po numerze telefonu? Musisz przeglądać całą książkę od deski do deski. I właśnie tak działa baza danych bez indeksu.
Indeks to taka dodatkowa struktura, która pomaga bazie szybciej znajdować dane. Tak jakbyś miał drugą książkę telefoniczną posortowaną po numerach telefonu zamiast po nazwiskach.
Co to znaczy „dodatkowa struktura”?
Baza danych naprawdę tworzy sobie dodatkowe dane na dysku. W PostgreSQL to będzie osobny plik, w MySQL to osobna część w tym samym pliku, ale zasada jest jedna – baza trzyma twoje dane dwa razy: raz w normalnej tabeli, raz w indeksie posortowane inaczej.
Przykład – masz tabelę:
ID | Email | Imię
1 | zosia@wp.pl | Zosia
2 | adam@gmail.com | Adam
3 | bartek@o2.pl | Bartek
```
Jak zrobisz indeks na `email`, baza stworzy coś takiego:
```
adam@gmail.com → wiersz 2
bartek@o2.pl → wiersz 3
zosia@wp.pl → wiersz 1
Teraz jak szukasz WHERE email = 'bartek@o2.pl', baza patrzy w indeks (który jest posortowany), szybko znajduje i skacze od razu do właściwego wiersza. Bez indeksu musiałaby sprawdzić każdy wiersz po kolei.
Kiedy indeks pomaga?
Jak robisz zapytania typu WHERE email = 'jan@example.com' i nie masz indeksu na kolumnie email, to baza musi sprawdzić każdy wiersz w tabeli. Jak masz milion użytkowników to sprawdzi milion wierszy. Z indeksem? Znajdzie od razu.
Ale indeksy to nie jest za darmo
Jak dodajesz nowy rekord do bazy, to musi zaktualizować też wszystkie indeksy. Więc jak masz 10 indeksów na tabeli, każdy INSERT jest wolniejszy. Plus indeksy zajmują miejsce na dysku – czasem nawet więcej niż sama tabela!
Kiedy indeksy są pomocne
Indeksy się sprawdzają jak dużo czytasz, mało piszesz. Przykład:
1. Tabela użytkowników w aplikacji:
SELECT * FROM users WHERE email = 'jan@wp.pl';
Robisz to przy każdym logowaniu. Użytkownik loguje się 10 razy dziennie, ale rejestruje się raz w życiu. Tutaj indeks na email to strzał w dziesiątkę.
2. Wyszukiwarki produktów:
SELECT * FROM products WHERE category = 'laptopy' AND price < 3000;
Ludzie cały czas przeglądają produkty, a nowe produkty dodajesz raz na jakiś czas. Indeks na category i price to must have.
3. Raporty i statystyki:
SELECT COUNT(*) FROM orders WHERE created_at > '2024-01-01';
Jak generujesz raporty co godzinę z milionów zamówień, indeks na created_at oszczędzi ci nerwów.
Kiedy indeksy szkodzą
Indeksy są problemem jak dużo piszesz, mało czytasz. Przykłady:
1. Logi systemowe:
INSERT INTO logs (message, created_at) VALUES (...);
Jak wrzucasz 1000 logów na sekundę, każdy indeks spowalnia zapis. A logi zazwyczaj czytasz rzadko (jak coś się zepsuje). Tutaj lepiej mieć mniej indeksów.
A więc zawsze zapytaj siebie:
- Jak często czytam z tej kolumny? (SELECT, WHERE, JOIN)
- Jak często piszę do tej tabeli? (INSERT, UPDATE, DELETE)
Jeśli czytasz 100x częściej niż piszesz → rób indeks
Jeśli piszesz 100x częściej niż czytasz → nie rób indeksu
Mam indeks, a baza go nie używa
Czasem robisz indeks, patrzysz w EXPLAIN i widzisz Seq Scan zamiast Index Scan. Baza ignoruje twój indeks. Dlaczego?
Powód 1: Tabela jest za mała
Jak masz 100 wierszy w tabeli, to baza stwierdza „po co mi indeks, szybciej przeczytam wszystko”. I ma rację! Dla małych tabel (poniżej ~1000 wierszy) indeks często nie ma sensu.
Powód 2: Zwracasz za dużo danych
Baza jest mądra – wie że jak zwrócisz więcej niż ~10-15% wierszy, to szybciej jest przeczytać całą tabelę naraz niż skakać przez indeks.
Powód 3: Używasz funkcji na kolumnie
Jak opakowujesz kolumnę w funkcję (LOWER, UPPER, DATE, etc.), indeks nie działa. Baza musi policzyć funkcję dla każdego wiersza.
Powód 4: Typ danych się nie zgadza
— user_id to INTEGER, ale…
— Nie użyje indeksu
SELECT * FROM orders WHERE user_id = '123′; — string!
— Użyje indeksu
SELECT * FROM orders WHERE user_id = 123; — integer
PostgreSQL jest szczególnie wybredny – jak typ się nie zgadza, może nie użyć indeksu.
Jak debugować użycie indeksu
-- Sprawdź czy baza w ogóle widzi indeks
\d users -- PostgreSQL
SHOW INDEX FROM users; -- MySQL
-- Wymuś użycie indeksu (do testów!)
-- PostgreSQL
SET enable_seqscan = OFF;
-- Teraz zobacz czy coś się zmieniło
EXPLAIN SELECT * FROM users WHERE email = 'jan@wp.pl';
Otóż nie zawsze Seq Scan = źle. Czasem to jest po prostu najszybsza opcja.
Problem dużych tabelek
Jak masz tabelę z 100 milionami wierszy w produkcji, to zwykłe CREATE INDEX może:
- Zablokować tabelę na godziny
- Zatrzymać zapisy (INSERT/UPDATE/DELETE)
- Wykończyć aplikację
Ale najpierw może wytłumaczmy sobie dlaczego mamy to blokowanie tabelki. Więc jak sobie tworzy się indeks to można to zobrazować tak:
Wiersz 1: jan@wp.pl → dodaj do indeksu
Wiersz 2: anna@gmail.com → dodaj do indeksu
Wiersz 3: piotr@o2.pl → dodaj do indeksu
... (miliony wierszy)
Jak baza czyta wiersz milionowy, a w tym samym czasie ktoś robi INSERT, to ten nowy wiersz nie trafi do indeksu. Indeks będzie niekompletny i zepsuty. Dlatego baza musi:
- Zablokować zapisy (WRITE LOCK)
- Przeczytać całą tabelę
- Zbudować indeks
- Odblokować zapisy
**Normalny CREATE INDEX:** ``` [LOCK TABLE] → czytaj dane → buduj indeks → [UNLOCK] ↓ Aplikacja czeka... czeka... czeka...
Więc tu jest tak naprawdę kilka rozwiązań:
Rozwiązanie 1: CONCURRENT index (PostgreSQL) – https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY – w skrócie chodzi o to by nie blokować operacjia zapisu, lecz Postgres będzie śledził zmiany i potem zaaktualizuje o nich indeks.
Rozwiązanie 2: godziny nocne
To klasyczne podejście: planowany przestój lub okno konserwacyjne, gdzie świadomie blokujesz część funkcjonalności na krótki czas. Zwykle najlepsze okna to:
- Święta (jeśli nie jesteś e-commerce)
- 2:00-5:00 w nocy w strefie czasowej użytkowników
- Weekendy (dla systemów B2B)
I naprawdę takie podejście jest stosowane dość często, pewnie też dostawaliście np. smsy z banku że w nocy nie da się korzystać z aplikacji.
Rozwiązanie 3: Partial Index (częściowy indeks)
To sprytny trick: zamiast indeksować wszystkie dane, indeksujesz tylko interesującą część.
Przykładowo mamy tabelkie z milionami zamówień:
SELECT * FROM orders;
-- id | status | created_at | total
-- ---|-------------|------------|-------
-- 1 | completed | 2024-01-15 | 150.00
-- 2 | completed | 2024-01-16 | 89.50
-- 3 | pending | 2024-01-17 | 200.00
-- 4 | completed | 2024-01-17 | 45.00
-- ...
-- 100 000 000 wierszy
Chcesz szybko znajdować zamówienia do realizacji:
SELECT * FROM orders WHERE status = 'pending';
Więc można zrobić to w sposób prostego tworzenia indeksu
CREATE INDEX idx_status ON orders(status);
-- Indeks na 100M wierszy
-- Rozmiar: ~2 GB
-- Czas tworzenia: 2 godziny
-- PROBLEM
SELECT * FROM orders WHERE status = 'pending';
-- Zwraca 50 000 wierszy (0.05% tabeli)
SELECT * FROM orders WHERE status = 'completed';
-- Zwraca 99 950 000 wierszy (99.95% tabeli)
I teraz indeks jest gigantyczny, ale głównie przez completed które stanowią 99.95% danych, i prawie nigdy nie są wyszukiwane no bo są już zrealizowane.
A więc można tu skorzystać z partial indeksu i poindeksować to co potrzebujemy (czyli te na statusie pending)
CREATE INDEX idx_pending_orders
ON orders(status, created_at)
WHERE status = 'pending';
-- Indeks tylko na 50k wierszy!
-- Rozmiar: ~5 MB (400x mniej!)
-- Czas tworzenia: 30 sekund (240x szybciej!)
```
**Jak to działa?**
Baza tworzy indeks **tylko** dla wierszy spełniających warunek `WHERE status = 'pending'`.
```
Normalny indeks (2 GB):
├─ pending: 50k wierszy
├─ completed: 99.9M wierszy ← 99.95% indeksu!
└─ cancelled: 50k wierszy
Partial index (5 MB):
└─ pending: 50k wierszy ← Tylko to!
Więc reasumując – indeksy w bazach danych to dodatkowe struktury, które przyspieszają odczyt danych, ale spowalniają zapis i zajmują miejsce na dysku. Kluczem do sukcesu jest tworzenie indeksów tylko tam gdzie często szukasz (WHERE, JOIN), unikanie ich na tabelach z dużą liczbą zapisów (logi, sesje), i regularne monitorowanie które indeksy są faktycznie używane.
Pamiętaj: lepiej mieć 3 dobrze dobrane indeksy niż 10 losowych.