Modyfikacja kolumny może być prosta w małej bazie danych. W dużej, aktywnie używanej tabeli ta sama operacja może wymagać blokady, przebudowy indeksów lub konkurować o zasoby z zapytaniami produkcyjnymi. Jej wpływ zależy od silnika, jego wersji, rodzaju zmiany i konfiguracji: nie należy zakładać, że instrukcja wykona się natychmiast lub nie spowoduje blokady.
Migracje schematu dużych tabel oddzielają zmianę struktury od przekształcania danych. Celem jest zachowanie zgodności wersji aplikacji w trakcie przejścia, mierzenie wpływu i możliwość zatrzymania procesu. Nie ma uniwersalnej recepty: sprawdź możliwości silnika i rzeczywiste zachowanie aplikacji.
Dlaczego niewielka zmiana może blokować operacje

Poszerzenie kolumny, dodanie ograniczenia lub zmiana typu mogą wymagać pracy proporcjonalnej do rozmiaru tabeli. W zależności od silnika operacja może utrzymywać blokady, generować obciążenie dysku, wpływać na repliki albo czekać na zakończenie otwartych transakcji. Nawet operacja uznawana za online może na krótko blokować lub mieć ograniczenia.
Oceń rozmiar i tempo wzrostu tabeli, obciążenie odczytem i zapisem, długie transakcje, indeksy oraz dostępne miejsce. Sprawdź dokumentację konkretnej wersji i przetestuj zmianę w reprezentatywnym środowisku. Określ operacyjne limity opóźnień, blokad, przestrzeni dyskowej i opóźnienia replikacji.
Standardowa aktualizacja schematu zmienia struktury, na przykład przez dodanie kolumny. Migracja danych przekształca lub kopiuje istniejące wartości, często wiersz po wierszu. Mogą być częścią tej samej zmiany funkcjonalnej, ale wiążą się z różnymi zagrożeniami, dlatego warto wykonywać je i monitorować oddzielnie.
Ustal, kto odczytuje i zapisuje dane oraz jakie są zależności
Sprawdź, kto odczytuje i zapisuje dane: kod PHP, zapytania SQL, zadania w kolejce, zaplanowane polecenia, importy, raporty i usługi zewnętrzne. Zweryfikuj, które wersje mogą współistnieć podczas stopniowego wdrożenia. Uwzględnij też zapytania dynamiczne i konsumentów spoza repozytorium.
- Udokumentuj obecny i docelowy format, w tym wartości NULL, wartości domyślne i reguły konwersji.
- Zidentyfikuj zależne indeksy, klucze obce, ograniczenia i widoki.
- Sprawdź, które komponenty aktualizują tylko część pól, a które zapisują dane.
- Określ, jak wykrywać i naprawiać nieprawidłowe wartości.
Ta inwentaryzacja wyznacza kolejność wdrożenia. Starsza wersja może przestać działać, jeśli usunięta zostanie kolumna, z której nadal korzysta. Jeśli nie możesz zidentyfikować wszystkich konsumentów, załóż, że stary kod może pozostać aktywny dłużej.
Podziel zmianę na zgodne fazy
Typowy wzorzec polega na rozszerzeniu, a następnie ograniczeniu schematu: dodaj nową strukturę bez usuwania starej, wdroż zgodny kod, skopiuj istniejące dane i przełącz odczyty. Dopiero po sprawdzeniu wyniku usuń starą strukturę.
- Rozszerzenie: dodaj nową kolumnę lub tabelę bez zakłócania działania wersji używanych produkcyjnie. Rozważ utworzenie indeksów i ograniczeń w osobnej operacji.
- Wdrożenie zgodności: wdroż czytniki tolerujące nieprzeniesione jeszcze dane oraz mechanizmy zapisujące, które utrzymują spójność obu reprezentacji.
- Uzupełnienie istniejących danych: wykonaj backfill partiami i śledź postęp.
- Przełączenie użycia: odczytuj dane przede wszystkim z nowej struktury i obserwuj błędy, opóźnienia oraz rozbieżności.
- Ograniczenie: najpierw wyłącz zapis do starej struktury, a w kolejnym wdrożeniu usuń starą strukturę.
Wdrożenie kodu nie wymaga natychmiastowego udostępnienia funkcjonalności. Upewnij się, że każda wersja działa ze schematem na każdym etapie, również wtedy, gdy konieczne będzie wycofanie kodu.
Wykonaj kontrolowany backfill z PHP
Unikaj ładowania całej tabeli do pamięci lub utrzymywania jednej globalnej transakcji. Polecenie konsolowe PHP pozwala kontrolować rozmiar partii, rejestrować postęp i zatrzymać proces bez wiązania go z cyklem żądania HTTP. Ten wzorzec z PDO wykorzystuje optymistyczną kontrolę współbieżności. progressStore oznacza rekord postępu zapisany w tej samej bazie danych i zatwierdzany w tej samej transakcji co wiersze danej partii.
$limit = 200;
$maxAttempts = 5;
$cursor = (int) $progressStore->load('backfill');
// Początkowa górna granica; sama w sobie nie obejmuje późniejszych wstawień.
$upperId = (int) $pdo->query('SELECT MAX(id) FROM records')->fetchColumn();
while ($cursor < $upperId) {
$done = false;
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
$select = $pdo->prepare(
'SELECT id, old_value, version FROM records
WHERE id > :cursor AND id <= :upper_id
ORDER BY id LIMIT ' . (int) $limit
);
$select->execute([':cursor' => $cursor, ':upper_id' => $upperId]);
$rows = $select->fetchAll(PDO::FETCH_ASSOC);
if (!$rows) {
$cursor = $upperId;
$done = true;
break;
}
try {
$pdo->beginTransaction();
$update = $pdo->prepare(
'UPDATE records SET new_value = :value, version = version + 1
WHERE id = :id AND version = :version'
);
foreach ($rows as $row) {
$update->execute([
':value' => transform($row['old_value']),
':id' => $row['id'], ':version' => $row['version'],
]);
if ($update->rowCount() !== 1) {
throw new VersionConflict('Wiersz zmienił się podczas backfillu');
}
}
$next = (int) end($rows)['id'];
$progressStore->saveWithinTransaction('backfill', $next);
$pdo->commit();
$cursor = $next;
$done = true;
break;
} catch (Throwable $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
$retryable = $e instanceof VersionConflict
|| ($e instanceof PDOException && isRetryableDatabaseError($e));
if (!$retryable || $attempt === $maxAttempts) throw $e;
usleep(min(100000 * (2 ** ($attempt - 1)), 2000000));
// Ponownie odczytaj partię z tym samym kursorem; postęp jeszcze się nie zmienił.
}
}
if (!$done) throw new RuntimeException('Partia oczekuje na przetworzenie; kursor nie został przesunięty.');
}VersionConflict powinien być własnym wyjątkiem, a nie ogólnym wyjątkiem obejmującym błędy transformacji. Dzięki temu trwałe błędy w transform() zatrzymają polecenie i umożliwią diagnostykę. isRetryableDatabaseError() powinno rozpoznawać wyłącznie sklasyfikowane błędy przejściowe dla używanego silnika i sterownika, takie jak zakleszczenia lub przekroczenia czasu oczekiwania. Inne błędy bazy danych zatrzymują proces. Maksymalna liczba prób i ograniczony backoff zapobiegają niekończącym się ponowieniom; po wyczerpaniu prób polecenie kończy się błędem bez przesunięcia kursora. Sprawdź, jak rowCount() działa w używanej kombinacji PDO i silnika.
Optymistyczna kontrola współbieżności wymaga, aby wszyscy zapisujący zwiększali version w tej samej transakcji, w której aktualizują dane. Ten kontrakt obejmuje kod PHP, kolejki, importy i usługi zewnętrzne. Musi być wdrożony i sprawdzony przed rozpoczęciem backfillu. Jeśli któryś zapisujący go nie przestrzega, porównanie może nie wykryć wyścigu: zaktualizuj go, kieruj zapisy przez wspólny mechanizm, tymczasowo go wyłącz albo zastosuj odpowiednie blokady. Nie uznawaj ochrony za skuteczną, dopóki nie sprawdzisz, czy każdy zapisujący przestrzega kontraktu.
Początkowa górna granica ogranicza zakres pracy, ale nie gwarantuje uwzględnienia wstawień zatwierdzonych z opóźnieniem. Po zakończeniu przebiegu wykonaj uzgadnianie, które jawnie wyszuka nieprzekształcone lub niezgodne wiersze, bez zakładania, że ich ID jest większe od kursora. Napraw i ponownie zweryfikuj te rekordy; powtarzaj przebieg, aż nie pozostaną żadne zaległe dane, stosując sprawdzalne kryterium i zapewniając zgodność po stronie zapisujących. Jeśli nie możesz wiarygodnie wykryć i uzgodnić takich wierszy, rozważ przechwytywanie zmian lub okno serwisowe. Nie uznawaj migracji istniejących danych za zakończoną tylko dlatego, że kursor osiągnął początkową granicę.
Transformacja powinna być idempotentna, a postęp musi być zatwierdzany atomowo razem z wierszami. Jeśli magazyn kursora nie uczestniczy w tej samej transakcji co dane, zaprojektuj wznawianie tak, aby bezpiecznie ponawiało przetwarzanie już zatwierdzonych wierszy. Dodaj opcje wstrzymania, limity partii, ustrukturyzowane logi i kod wyjścia odzwierciedlający błędy; dobieraj rozmiar na podstawie pomiarów, a nie założeń.
Unikaj wyścigów z równoległymi zapisami
Samo podwójne zapisywanie nie eliminuje wszystkich wyścigów. Backfill może odczytać starą wartość; równoległy zapis może zaktualizować oba pola; następnie backfill może nadpisać nowe pole nieaktualnym wynikiem. Warunek wersji w przykładzie uniemożliwia taką aktualizację, jeśli równoległy zapis zwiększył wersję; konflikt zostaje zachowany, ponieważ kursor przesuwa się dopiero po zatwierdzeniu całej partii.
W zależności od gwarancji oferowanych przez silnik i obciążenia można też zastosować aktualizację warunkową opartą na pierwotnej wartości albo odpowiednie blokady. Każda z tych metod wiąże się z innymi kosztami i semantyką. Przetestuj wybraną strategię na konkretnym silniku, uwzględniając przeplatające się zapisy, zakleszczenia i przekroczenia czasu oczekiwania. Zanim uznasz, że proces poprawnie uzupełnia istniejące dane, sprawdź zarówno liczbę zmienionych wierszy, jak i wynik uzgadniania.
Zweryfikuj wynik przed usunięciem starej struktury
Zakończenie polecenia nie dowodzi poprawności danych. Sprawdź, czy nie pozostały zaległe wiersze, zweryfikuj ograniczenia i porównaj wyniki z oczekiwaną transformacją. Sprawdź wartości NULL i przypadki brzegowe, a także upewnij się, że odczyty korzystają z nowego pola bez pogorszenia działania funkcjonalnego ani wydajności.
Obserwuj również rzadziej uruchamiane procesy, takie jak raporty czy zadania cykliczne. Zachowaj starą strukturę, dopóki istnieją zależni od niej czytelnicy lub zapisujący: jej obecność jest zabezpieczeniem zgodności, a nie dowodem zakończenia migracji.
Ustal zasady wstrzymania, wycofania i okna serwisowego

Określ sygnały do wstrzymania procesu: błędy lub opóźnienia powyżej uzgodnionego progu, blokady, nadmierne opóźnienie replik, presję na zasoby lub narastające rozbieżności. Ustal, kto zatrzymuje proces i jak wznowić go od ostatniej zatwierdzonej partii. Monitoruj czas przetwarzania partii, błędy, użycie CPU i dysku oraz blokady.
Wycofanie kodu nie jest równoznaczne z wycofaniem zmian w danych. Po dopuszczeniu zapisów wyłącznie w nowym formacie transformacja odwrotna może spowodować utratę informacji. Odzyskiwanie może polegać na powrocie do zgodnej wersji i zachowaniu obu struktur, a nie na odwracaniu zmian w danych. Okno serwisowe może być lepszym rozwiązaniem, jeśli nie da się zachować spójności między wersjami, wymagane blokady są nieakceptowalne albo nie można zweryfikować wyniku przy działającej aplikacji.
- Czy silnik i jego wersja umożliwiają zmianę przy akceptowalnym poziomie blokad?
- Czy starsze czytniki i zapisujący mogą współistnieć ze schematem przejściowym?
- Czy polecenie PHP można wstrzymać i wznowić bez powielania efektów ani pomijania wierszy?
- Czy istnieje przetestowana strategia obsługi równoległych zapisów, błędów przejściowych i rozbieżności?
- Czy integralność i wpływ na system zostaną zmierzone przed usunięciem starej struktury?



