Een kolom aanpassen kan eenvoudig zijn in een kleine database. In een grote, actieve tabel kan dezelfde bewerking een lock vereisen, indexen opnieuw opbouwen of om resources concurreren met productiequeries. Het effect hangt af van de database-engine, de versie, het type wijziging en de configuratie: ga er niet van uit dat een instructie onmiddellijk wordt uitgevoerd of geen locks veroorzaakt.
Schemamigraties in grote tabellen scheiden de structurele wijziging van de datatransformatie. Het doel is om de applicatieversies tijdens de overgang compatibel te houden, de impact te meten en het proces te kunnen stoppen. Er bestaat geen universeel recept: controleer de mogelijkheden van de database-engine en het daadwerkelijke gedrag van de applicatie.
Waarom een kleine wijziging bewerkingen kan blokkeren

Een kolom uitbreiden, een constraint toevoegen of een type wijzigen kan werk vereisen dat evenredig is aan de omvang van de tabel. Afhankelijk van de database-engine kan de bewerking locks vasthouden, schijfbelasting veroorzaken, replica’s beïnvloeden of wachten tot openstaande transacties zijn afgerond. Zelfs een bewerking die als online wordt beschouwd, kan kortstondig blokkeren of beperkingen hebben.
Beoordeel de omvang en groei van de tabel, de lees- en schrijfbelasting, lange transacties, indexen en beschikbare ruimte. Raadpleeg de documentatie voor de specifieke versie en test in een representatieve omgeving. Stel operationele grenswaarden vast voor latency, locks, opslag en replicatievertraging.
Een conventionele schemaupdate wijzigt structuren, bijvoorbeeld door een kolom toe te voegen. Een datamigratie transformeert of kopieert bestaande waarden, vaak rij voor rij. Ze kunnen deel uitmaken van dezelfde functionele wijziging, maar brengen verschillende risico’s met zich mee en kunnen het beste afzonderlijk worden uitgevoerd en gemonitord.
Lezers, schrijvers en afhankelijkheden inventariseren
Breng in kaart wie de gegevens leest en schrijft: PHP-code, SQL-queries, queuejobs, geplande opdrachten, imports, rapportages en externe services. Controleer welke versies naast elkaar kunnen bestaan tijdens een gefaseerde uitrol. Ook dynamische queries en consumers buiten de repository tellen mee.
- Leg de huidige en beoogde indeling vast, inclusief null-waarden, standaardwaarden en conversieregels.
- Identificeer afhankelijke indexen, foreign keys, constraints en views.
- Controleer welke componenten velden gedeeltelijk bijwerken en welke de gegevens schrijven.
- Bepaal hoe ongeldige waarden kunnen worden opgespoord en hersteld.
Deze inventaris bepaalt de volgorde van de uitrol. Een oude versie kan fouten geven als een kolom wordt verwijderd die nog wordt geraadpleegd. Als je niet alle consumers kunt identificeren, ga er dan van uit dat oude code langer actief kan blijven.
De wijziging opsplitsen in compatibele fasen
Een gebruikelijk patroon is eerst uitbreiden en daarna inkrimpen: voeg de nieuwe structuur toe zonder de oude te verwijderen, rol compatibele code uit, kopieer de historische gegevens en schakel de leesbewerkingen om. Verwijder de oude structuur pas nadat je het resultaat hebt geverifieerd.
- Uitbreiden: voeg de nieuwe kolom of tabel toe zonder versies die in productie draaien te breken. Overweeg indexen en constraints in een afzonderlijke bewerking aan te maken.
- Compatibiliteit uitrollen: publiceer lezers die ontbrekende gegevens aankunnen en schrijvers die beide representaties consistent houden.
- Historische gegevens aanvullen: voer de backfill in batches uit en volg de voortgang.
- Het gebruik omschakelen: lees voornamelijk uit de nieuwe structuur en houd fouten, latency en afwijkingen in de gaten.
- Inkrimpen: stop eerst met schrijven naar de oude structuur en verwijder die vervolgens in een latere uitrol.
Code uitrollen betekent niet dat je de functionaliteit meteen hoeft te activeren. Bevestig dat elke versie werkt met het schema van elke fase, ook als je de code moet terugdraaien.
Een gecontroleerde backfill uitvoeren vanuit PHP
Voorkom dat je de hele tabel in het geheugen laadt of één globale transactie openhoudt. Met een PHP-consoleopdracht kun je de batchgrootte beheren, de voortgang registreren en het proces stoppen zonder het te koppelen aan de levenscyclus van een webrequest. Dit PDO-patroon gebruikt optimistic concurrency. progressStore staat voor een voortgangsrecord dat in dezelfde database wordt opgeslagen en in dezelfde transactie als de batchrijen wordt vastgelegd.
$limit = 200;
$maxAttempts = 5;
$cursor = (int) $progressStore->load('backfill');
// Initiële bovengrens; deze dekt op zichzelf geen laat bevestigde inserts.
$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('De rij is gewijzigd tijdens de backfill');
}
}
$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));
// Lees de batch opnieuw met dezelfde cursor; de voortgang is nog niet opgeschoven.
}
}
if (!$done) throw new RuntimeException('Batch niet verwerkt; cursor is niet opgeschoven.');
}VersionConflict moet een eigen exception zijn, geen algemene exception waarin transformatiefouten worden samengevoegd. Zo stoppen permanente fouten in transform() de opdracht voor diagnose. isRetryableDatabaseError() mag alleen tijdelijke fouten herkennen die zijn geclassificeerd voor de gebruikte database-engine en driver, zoals deadlocks of time-outs. Andere databasefouten stoppen het proces. Het maximumaantal pogingen en de begrensde backoff voorkomen eindeloze retries; als de pogingen op zijn, mislukt de opdracht zonder de cursor op te schuiven. Controleer hoe jouw combinatie van PDO en database-engine rowCount() rapporteert.
Optimistic concurrency vereist dat alle schrijvers version verhogen in dezelfde transactie waarin ze de gegevens bijwerken. Deze afspraak geldt voor PHP-code, queues, imports en externe services en moet zijn ingevoerd en gecontroleerd voordat de backfill begint. Als een schrijver zich er niet aan houdt, kan de vergelijking de race niet detecteren: werk die schrijver bij, leid de writes via een gemeenschappelijk mechanisme, schakel hem tijdelijk uit of gebruik geschikte locks. Ga er pas van uit dat de bescherming werkt nadat je de afspraak met elke schrijver hebt gecontroleerd.
De initiële bovengrens beperkt het werk, maar garandeert niet dat inserts worden meegenomen die pas later worden bevestigd. Voer na de eerste run een reconciliatie uit die expliciet zoekt naar niet-getransformeerde of afwijkende rijen, zonder ervan afhankelijk te zijn dat hun ID hoger is dan de cursor. Herstel die records en controleer ze opnieuw; herhaal de run totdat er geen openstaande gevallen meer zijn, volgens een verifieerbaar criterium en met schrijvers die de compatibiliteit behouden. Als je deze rijen niet betrouwbaar kunt detecteren en reconciliëren, overweeg dan change capture of een onderhoudsvenster. Verklaar de historische gegevens niet voltooid alleen omdat de cursor de initiële bovengrens heeft bereikt.
De transformatie moet idempotent zijn en de voortgang moet atomair met de rijen worden vastgelegd. Als de cursoropslag geen transactie deelt met de gegevens, ontwerp dan het hervatten zo dat reeds bevestigde rijen veilig opnieuw kunnen worden verwerkt. Voeg opties toe om te pauzeren, batchlimieten, gestructureerde logs en een exitcode die fouten weerspiegelt; stel de grootte in op basis van metingen, niet van aannames.
Races met gelijktijdige schrijfbewerkingen voorkomen
Dubbel schrijven voorkomt op zichzelf niet alle races. De backfill kan een oude waarde lezen; een gelijktijdige schrijfbewerking kan beide velden bijwerken; daarna kan de backfill het nieuwe veld overschrijven met een verouderd resultaat. De versievoorwaarde in het voorbeeld voorkomt die update als de gelijktijdige schrijfbewerking de versie heeft verhoogd; het conflict blijft behouden omdat de cursor pas opschuift nadat de volledige batch is bevestigd.
Afhankelijk van de garanties van de database-engine en de workload kun je ook een conditionele update op basis van de oorspronkelijke waarde gebruiken, of geschikte locks. Elke optie heeft andere kosten en semantiek. Test de strategie op de specifieke database-engine met verweven writes, deadlocks en time-outs, en controleer zowel de betrokken rijen als de reconciliatie voordat je stelt dat de historische gegevens correct zijn verwerkt.
Controleren voordat je de oude structuur verwijdert
Dat de opdracht is voltooid, bewijst niet dat de gegevens correct zijn. Controleer of er geen rijen meer openstaan, valideer constraints en vergelijk de resultaten met de verwachte transformatie. Controleer null-waarden en randgevallen, en bevestig dat leesbewerkingen het nieuwe veld gebruiken zonder dat het functionele gedrag of de prestaties verslechteren.
Houd ook weinig frequente processen in de gaten, zoals rapportages of periodieke taken. Behoud de oude structuur zolang er afhankelijke lezers of schrijvers bestaan: de aanwezigheid ervan is een compatibiliteitsmaatregel, geen bewijs dat de migratie is voltooid.
Pauzeren, terugdraaien en een onderhoudsvenster bepalen

Bepaal wanneer het proces moet worden gepauzeerd: bij fouten of latency boven de afgesproken drempel, locks, buitensporige replicatievertraging, resourceproblemen of toenemende afwijkingen. Bepaal wie het proces stopt en hoe het vanaf de laatst bevestigde batch wordt hervat. Monitor de batchduur, fouten, CPU, schijf en locks.
Code terugdraaien is niet hetzelfde als gegevens terugdraaien. Nadat writes uitsluitend in de nieuwe indeling worden geaccepteerd, kan een omgekeerde transformatie informatie verliezen. Herstel kan betekenen dat je teruggaat naar een compatibele versie en beide structuren behoudt, niet dat je de gegevens ongedaan maakt. Een onderhoudsvenster kan de voorkeur hebben als compatibiliteit tussen versies niet kan worden gehandhaafd, als de benodigde locks onaanvaardbaar zijn of als je het resultaat niet met de actieve applicatie kunt verifiëren.
- Kunnen de database-engine en de versie de wijziging uitvoeren met aanvaardbare locks?
- Kunnen oude lezers en schrijvers naast het tussentijdse schema blijven werken?
- Kan de PHP-opdracht worden gepauzeerd en hervat zonder effecten te dupliceren of rijen over te slaan?
- Is er een geteste strategie voor gelijktijdige schrijvers, tijdelijke fouten en afwijkingen?
- Worden integriteit en impact gemeten voordat de oude structuur wordt verwijderd?



