Optymalizacja migracji bazy Postgres w środowisku Docker
AUTHOR: Kamil M. | DATE: 2026-08-07 | READ_TIME: 6 min | STATUS: wdrożoneMigracja zakładająca indeks na tabeli faktur w ksefv.pl wywracała się na produkcji za każdym podejściem. Osiemnaście milionów wierszy, kontener z limitem pamięci i domyślny lock_timeout ustawiony na zero — to przepis na wstrzymanie zapisu na kilka minut w środku dnia roboczego.
Poniżej opisuję, co dokładnie blokowało wdrożenie, jak wygląda bezpieczna wersja tej migracji i jakie parametry kontenera trzeba było zmienić, żeby budowanie indeksu nie kończyło się zabiciem procesu przez OOM killera.
Skąd brała się blokada
Zwykłe CREATE INDEX zakłada blokadę SHARE na tabeli. Odczyty przechodzą, ale każdy INSERT z kolejki wysyłki do KSeF czeka. Przy tej wielkości tabeli budowanie indeksu zajmowało prawie cztery minuty, więc kolejka rosła szybciej, niż zdążyła się rozładować.
Migracja, która działa na kopii z dziesięcioma tysiącami wierszy, nie mówi nic o produkcji. Mierzalny jest tylko czas blokady, nie czas zapytania.
— notatka z retrospektywy, 22.07.2026Pomiar przed zmianą
Pierwsze, co trzeba było ustalić, to ile realnie trwa blokada, a nie ile trwa całe wdrożenie. Widok pg_locks w połączeniu z pg_stat_activity pokazuje to bez zgadywania.
Wersja, która przeszła bez blokady
Rozwiązanie to CONCURRENTLY plus wyjście z transakcji. Doctrine domyślnie owija migrację w transakcję, a Postgres nie pozwala budować indeksu współbieżnie w jej wnętrzu — dlatego migracja musi jawnie zadeklarować, że transakcji nie chce.
Limity kontenera
Podniesienie maintenance_work_mem do 512 MB skróciło budowanie indeksu o połowę, ale przy limicie 512 MB na kontener proces natychmiast padał. Docker liczy pamięć współdzieloną razem z resztą, więc shm_size trzeba podnieść osobno.
Wynik
Migracja przeszła w okno wdrożeniowe bez ani jednej odrzuconej wysyłki. Czas budowania indeksu wydłużył się względem wersji blokującej, co przy CONCURRENTLY jest normalne — baza wykonuje dwa przejścia po tabeli zamiast jednego.
Czego nie robić
Indeks budowany współbieżnie może zostać oznaczony jako nieprawidłowy, jeśli operacja przerwie się w połowie. Wtedy trzeba go usunąć i powtórzyć — sam się nie naprawi, a planer go ignoruje, więc łatwo tego nie zauważyć.
pg_index o pozycje z indisvalid = false. Trzy linijki w skrypcie poddeployowym, oszczędzają tydzień szukania powodu wolnych zapytań.