Jak jedna nieindeksowana kolumna zabiła naszą bazę danych
Jeden brakujący indeks zmienił API o czasie odpowiedzi 8 ms w 8-sekundowy koszmar.
Przeprowadziliśmy test obciążeniowy przed ważnym wydaniem. W środowisku lokalnym wszystko działało poprawnie przy 100 rekordach. Następnie zasymulowaliśmy 500 użytkowników operujących na 1,5 miliona wierszy w danych o rozmiarze produkcyjnym.
Wyniki były fatalne:
- Czas odpowiedzi API wzrósł z 45 ms do 8 000 ms.
- Obciążenie procesora bazy danych osiągnęło 100%.
- Pule połączeń zostały wyczerpane, co spowodowało błędy timeout.
Sprawdziliśmy logi wolnych zapytań (slow-query log) i znaleźliśmy endpoint pobierający historię zamówień na podstawie ID użytkownika i statusu.
Uruchomienie EXPLAIN ANALYZE w PostgreSQL wykazało Sequential Scan. Silnik czytał wszystkie 1,5 miliona wierszy przy każdym zapytaniu, ponieważ nie było indeksu na user_id.
Przy 100 jednoczesnych zapytaniach, system skanował 150 milionów wierszy naraz.
Naprawa zajęła pięć minut.
Zamiast prostego indeksu na user_id, utworzyliśmy indeks złożony na (user_id, status, created_at DESC). Pozwoliło to bazie danych:
- Filtrować po
user_id. - Filtrować po
status. - Natychmiast zwracać najnowsze wiersze.
- Pominąć dodatkowe kroki sortowania.
Użyliśmy CREATE INDEX CONCURRENTLY, aby tabela pozostała odblokowana podczas operacji.
Po naprawie:
- Czas zapytania spadł z 8 150 ms do 0,14 ms.
- Opóźnienie API spadło z 8 sekund do 12 ms.
- Zużycie procesora spadło ze 100% do poniżej 8%.
Wyciągnięte wnioski:
- Testy lokalne bywają mylące; 100 wierszy nie reprezentuje miliona.
- Indeksuj klucze obce — większość ORM-ów pomija ten krok.
- Uruchamiaj
EXPLAIN ANALYZEprzed zakupem dodatkowego sprzętu. - Projektuj indeksy pod kątem zapytań, które faktycznie wykonujesz.
Nie skaluj najpierw serwerów. Skaluj zapytania.
