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 ANALYZE przed zakupem dodatkowego sprzętu.
  • Projektuj indeksy pod kątem zapytań, które faktycznie wykonujesz.

Nie skaluj najpierw serwerów. Skaluj zapytania.

Źródło: https://dev.to/mia_keller_ffd2584c046ecb/how-an-unindexed-column-silently-killed-our-database-under-load-and-the-5-minute-fix-m32