Eine Spalte zu ändern, kann in einer kleinen Datenbank unkompliziert sein. In einer großen, aktiv genutzten Tabelle kann derselbe Vorgang eine Sperre erfordern, Indizes neu aufbauen oder mit Produktionsabfragen um Ressourcen konkurrieren. Die Auswirkungen hängen von der Datenbank-Engine, ihrer Version, der Art der Änderung und der Konfiguration ab: Gehe nicht davon aus, dass ein Befehl sofort ausgeführt wird oder keine Sperren verursacht.
Schema-Migrationen in großen Tabellen trennen die strukturelle Änderung von der Datentransformation. Ziel ist es, die Anwendungsversionen während des Übergangs kompatibel zu halten, die Auswirkungen zu messen und den Prozess anhalten zu können. Es gibt kein allgemeingültiges Rezept: Prüfe die Funktionen der Datenbank-Engine und das tatsächliche Verhalten der Anwendung.
Warum eine kleine Änderung Vorgänge blockieren kann

Eine Spalte zu erweitern, eine Einschränkung hinzuzufügen oder einen Datentyp zu ändern, kann Arbeit verursachen, die proportional zur Tabellengröße ist. Je nach Datenbank-Engine kann der Vorgang Sperren halten, die Festplattenlast erhöhen, Replikate beeinträchtigen oder warten müssen, bis offene Transaktionen abgeschlossen sind. Selbst ein als online geltender Vorgang kann kurzzeitig sperren oder Einschränkungen haben.
Bewerte Größe und Wachstum der Tabelle, Lese- und Schreiblast, lange Transaktionen, Indizes und verfügbaren Speicherplatz. Lies die Dokumentation der konkreten Version und teste in einer repräsentativen Umgebung. Lege Betriebsgrenzwerte für Latenz, Sperren, Speicherplatz und Replikationsverzögerung fest.
Eine herkömmliche Schemaaktualisierung verändert Strukturen, beispielsweise durch das Hinzufügen einer Spalte. Eine Datenmigration transformiert oder kopiert bestehende Werte, oft zeilenweise. Beides kann Teil derselben funktionalen Änderung sein, hat aber unterschiedliche Risiken und sollte getrennt ausgeführt und überwacht werden.
Leser, Schreiber und Abhängigkeiten erfassen
Ermittle, wer die Daten liest und schreibt: PHP-Code, SQL-Abfragen, Queue-Jobs, geplante Befehle, Importe, Berichte und externe Dienste. Prüfe, welche Versionen während eines schrittweisen Rollouts nebeneinander bestehen können. Auch dynamische Abfragen und Verbraucher außerhalb des Repositorys sind zu berücksichtigen.
- Dokumentiere die aktuellen und angestrebten Formate, einschließlich NULL-Werten, Standardwerten und Konvertierungsregeln.
- Identifiziere abhängige Indizes, Fremdschlüssel, Einschränkungen und Views.
- Prüfe, welche Komponenten Felder teilweise aktualisieren und welche die Daten schreiben.
- Lege fest, wie ungültige Werte erkannt und repariert werden.
Diese Bestandsaufnahme bestimmt die Reihenfolge des Deployments. Eine alte Version kann fehlschlagen, wenn eine Spalte entfernt wird, die sie noch abfragt. Wenn du nicht alle Verbraucher identifizieren kannst, gehe davon aus, dass alter Code länger aktiv bleiben könnte.
Die Änderung in kompatible Phasen aufteilen
Ein gängiges Muster ist Expand and Contract: Füge die neue Struktur hinzu, ohne die alte zu entfernen, deploye kompatiblen Code, kopiere die Bestandsdaten und stelle die Lesezugriffe um. Erst wenn das Ergebnis überprüft wurde, wird die alte Struktur entfernt.
- Erweitern: Füge die neue Spalte oder Tabelle hinzu, ohne Versionen in der Produktion zu beeinträchtigen. Erwäge, Indizes und Einschränkungen in einem separaten Vorgang anzulegen.
- Kompatibilität deployen: Veröffentliche Leser, die ausstehende Daten tolerieren, und Schreiber, die beide Repräsentationen konsistent halten.
- Bestandsdaten vervollständigen: Führe den Backfill stapelweise aus und verfolge den Fortschritt.
- Nutzung umstellen: Lies hauptsächlich aus der neuen Struktur und beobachte Fehler, Latenz und Abweichungen.
- Verkleinern: Beende zuerst die Schreibzugriffe auf die alte Struktur und entferne in einem späteren Deployment die alte Struktur.
Ein Code-Deployment verpflichtet dich nicht dazu, die Funktionalität sofort freizugeben. Stelle sicher, dass jede Version mit dem Schema jeder Phase funktioniert – auch wenn ein Code-Rollback nötig ist.
Einen kontrollierten Backfill mit PHP ausführen
Vermeide es, die gesamte Tabelle in den Arbeitsspeicher zu laden oder eine globale Transaktion offen zu halten. Ein PHP-Konsolenbefehl ermöglicht es, die Batch-Größe zu steuern, den Fortschritt zu protokollieren und den Prozess anzuhalten, ohne ihn an den Lebenszyklus einer Webanfrage zu binden. Dieses PDO-Beispiel verwendet optimistische Nebenläufigkeitskontrolle. progressStore steht für einen Fortschrittsdatensatz, der in derselben Datenbank gespeichert und in derselben Transaktion wie die Zeilen des Batches bestätigt wird.
$limit = 200;
$maxAttempts = 5;
$cursor = (int) $progressStore->load('backfill');
// Initiale Obergrenze; verspätete Inserts werden damit nicht automatisch erfasst.
$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('Die Zeile hat sich während des Backfills geändert');
}
}
$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));
// Batch mit demselben Cursor erneut lesen; der Fortschritt wurde noch nicht erhöht.
}
}
if (!$done) throw new RuntimeException('Batch ausstehend; Cursor nicht weitergesetzt.');
}VersionConflict muss eine eigene Exception sein und darf keine allgemeine Exception sein, die auch Transformationsfehler umfasst. So hält ein dauerhafter Fehler in transform() den Befehl zur Diagnose an. isRetryableDatabaseError() darf nur vorübergehende Fehler erkennen, die für die verwendete Datenbank-Engine und den Treiber klassifiziert wurden, etwa Deadlocks oder Timeouts. Andere Datenbankfehler halten den Prozess an. Die begrenzte Anzahl an Versuchen und das begrenzte Backoff verhindern unbegrenzte Wiederholungen; sind alle Versuche ausgeschöpft, schlägt der Befehl fehl, ohne den Cursor weiterzusetzen. Prüfe, wie rowCount() bei deiner Kombination aus PDO und Datenbank-Engine arbeitet.
Die optimistische Absicherung setzt voraus, dass alle Schreiber version in derselben Transaktion erhöhen, in der sie die Daten aktualisieren. Dieser Vertrag umfasst PHP-Code, Queues, Importe und externe Dienste und muss implementiert und überprüft sein, bevor der Backfill beginnt. Hält sich ein Schreiber nicht daran, erkennt der Vergleich den Wettlauf möglicherweise nicht: Aktualisiere ihn, leite seine Schreibzugriffe über einen gemeinsamen Mechanismus, deaktiviere ihn vorübergehend oder verwende geeignete Sperren. Gehe erst dann von einer wirksamen Absicherung aus, wenn der Vertrag jedes Schreibers überprüft wurde.
Die initiale Obergrenze verringert den Arbeitsaufwand, garantiert aber nicht, dass sie Inserts einschließt, die erst später bestätigt werden. Führe nach dem Durchlauf einen Abgleich aus, der ausdrücklich nach nicht transformierten oder abweichenden Zeilen sucht, ohne sich darauf zu verlassen, dass ihre ID größer als der Cursor ist. Repariere diese Datensätze und überprüfe sie erneut; wiederhole den Durchlauf, bis unter einem überprüfbaren Kriterium keine ausstehenden Datensätze mehr vorhanden sind und die Schreiber die Kompatibilität aufrechterhalten. Wenn du solche Zeilen nicht zuverlässig erkennen und abgleichen kannst, ziehe Change Data Capture oder ein Wartungsfenster in Betracht. Erkläre den Backfill nicht allein deshalb für abgeschlossen, weil der Cursor die initiale Obergrenze erreicht hat.
Die Transformation muss idempotent sein und der Fortschritt muss atomar mit den Zeilen bestätigt werden. Wenn der Cursor-Speicher keine Transaktion mit den Daten teilt, muss die Wiederaufnahme bereits bestätigte Zeilen sicher erneut verarbeiten können. Ergänze Optionen zum Pausieren, Batch-Grenzen, strukturierte Protokolle und einen Exit-Code, der Fehler widerspiegelt; passe die Batch-Größe anhand von Messungen an, nicht anhand von Annahmen.
Wettläufe bei gleichzeitigen Schreibzugriffen vermeiden
Dual Writes allein verhindern nicht alle Wettläufe. Der Backfill kann einen alten Wert lesen; ein gleichzeitiger Schreibzugriff kann beide Felder aktualisieren; anschließend könnte der Backfill das neue Feld mit einem veralteten Ergebnis überschreiben. Die Versionsbedingung im Beispiel verhindert diese Aktualisierung, wenn der gleichzeitige Schreibzugriff die Version erhöht hat. Der Konflikt bleibt erhalten, weil der Cursor erst nach Bestätigung des gesamten Batches weitergesetzt wird.
Je nach Garantien der Datenbank-Engine und der Arbeitslast kann auch eine bedingte Aktualisierung auf Grundlage des ursprünglichen Werts oder der Einsatz geeigneter Sperren verwendet werden. Jede Alternative hat andere Kosten und Semantiken. Teste die Strategie mit der konkreten Datenbank-Engine und verschachtelten Schreibzugriffen, Deadlocks und Timeouts. Prüfe sowohl die betroffenen Zeilen als auch den Abgleich, bevor du erklärst, dass der Backfill die Bestandsdaten korrekt vervollständigt.
Vor dem Entfernen der alten Struktur überprüfen
Dass der Befehl beendet wurde, beweist nicht, dass die Daten korrekt sind. Prüfe, ob keine ausstehenden Zeilen verbleiben, validiere Einschränkungen und vergleiche die Ergebnisse mit der erwarteten Transformation. Überprüfe NULL-Werte und Grenzfälle und stelle sicher, dass Lesezugriffe das neue Feld verwenden, ohne das funktionale Verhalten oder die Performance zu beeinträchtigen.
Beobachte auch selten ausgeführte Prozesse wie Berichte oder periodische Aufgaben. Behalte die alte Struktur bei, solange davon abhängige Leser oder Schreiber existieren: Ihre Anwesenheit dient der Kompatibilität und beweist nicht, dass die Migration abgeschlossen ist.
Pausen, Rollback und Wartungsfenster festlegen

Lege Kriterien für eine Pause fest: Fehler oder Latenz über dem vereinbarten Grenzwert, Sperren, übermäßige Replikationsverzögerung, Ressourcenengpässe oder zunehmende Abweichungen. Bestimme, wer den Prozess anhält und wie er beim letzten bestätigten Batch fortgesetzt wird. Überwache Batch-Dauer, Fehler, CPU, Festplatte und Sperren.
Code zurückzurollen ist nicht dasselbe wie Daten zurückzurollen. Wenn Schreibzugriffe nur noch im neuen Format akzeptiert werden, kann eine Rücktransformation Informationen verlieren. Die Wiederherstellung könnte darin bestehen, auf eine kompatible Version zurückzugehen und beide Strukturen beizubehalten, statt die Daten rückgängig zu machen. Ein Wartungsfenster kann vorzuziehen sein, wenn die Konsistenz zwischen Versionen nicht gewährleistet werden kann, die erforderlichen Sperren nicht akzeptabel sind oder sich das Ergebnis bei aktiver Anwendung nicht überprüfen lässt.
- Erlauben die Datenbank-Engine und ihre Version die Änderung mit akzeptablen Sperren?
- Können alte Leser und Schreiber mit dem Zwischenschema koexistieren?
- Lässt sich der PHP-Befehl pausieren und fortsetzen, ohne Effekte zu duplizieren oder Zeilen zu überspringen?
- Gibt es eine getestete Strategie für gleichzeitige Schreiber, vorübergehende Fehler und Abweichungen?
- Werden Integrität und Auswirkungen gemessen, bevor die alte Struktur entfernt wird?



