Sichere Datenbankmigrationen

Datenbankmigrationen auf einer Produktionsdatenbank mit laufendem Traffic sind die häufigste Ursache für ungeplante Downtimes. Die meisten Probleme sind vermeidbar.

Was gefährlich ist

-- Sperrt die gesamte Tabelle, blockiert alle Reads und Writes
ALTER TABLE orders ADD COLUMN discount_pct DECIMAL(5,2) NOT NULL DEFAULT 0;

-- Noch schlimmer auf großen Tabellen
ALTER TABLE events ADD INDEX idx_created (created_at);

MySQL/MariaDB sperrt bei manchen ALTERs die Tabelle für die gesamte Dauer. Bei 50 Millionen Zeilen dauert das.

Online DDL prüfen

-- Prüfen ob die Operation Online geht (MySQL 5.6+)
ALTER TABLE orders
    ADD COLUMN discount_pct DECIMAL(5,2) NOT NULL DEFAULT 0,
    ALGORITHM=INPLACE, LOCK=NONE;

Schlägt fehl wenn Online nicht möglich ist statt einfach zu sperren.

Schrittweise vorgehen

Statt ALTER TABLE ... NOT NULL in einem Schritt:

  1. Spalte als nullable hinzufügen (schnell, kein Lock):
ALTER TABLE orders ADD COLUMN discount_pct DECIMAL(5,2) NULL DEFAULT NULL;
  1. Code deployen der beide Spalten befüllt.
  1. Daten befüllen (in Batches):
UPDATE orders SET discount_pct = 0 WHERE discount_pct IS NULL LIMIT 10000;
-- Wiederholen bis kein NULL mehr vorhanden
  1. NOT NULL-Constraint setzen (jetzt schnell weil kein NULL mehr):
ALTER TABLE orders MODIFY COLUMN discount_pct DECIMAL(5,2) NOT NULL DEFAULT 0;

pt-online-schema-change

Für wirklich kritische Tabellen: Percona Toolkit's pt-osc.

pt-online-schema-change \
    --alter "ADD COLUMN discount_pct DECIMAL(5,2) NOT NULL DEFAULT 0" \
    --execute \
    D=myapp,t=orders

Erstellt eine Schattentabelle, kopiert Daten in Batches, hält über Trigger synchron, benennt um.
Keine Downtime, aber Overhead durch Trigger.

Migrationen versionieren

migrations/
  001_initial_schema.sql
  002_add_orders_table.sql
  003_add_discount_column.sql
$applied = $pdo->fetchAll('SELECT version FROM schema_migrations ORDER BY version');
$appliedVersions = array_column($applied, 'version');

foreach (glob('migrations/*.sql') as $file) {
    $version = basename($file, '.sql');
    if (!in_array($version, $appliedVersions)) {
        $pdo->exec(file_get_contents($file));
        $pdo->prepare('INSERT INTO schema_migrations (version) VALUES (?)')->execute([$version]);
    }
}

Migrationen nie rückwirkend ändern — immer neue hinzufügen.