Een databaseschema wijzigen terwijl applicaties schrijven en lezen, vraagt om meer dan een migratiescript. Je leest hoe migraties, versiecompatibiliteit, dataverwerking en herstelstrategieën samen bepalen of een schemawijziging beheersbaar blijft.
Databasewijzigingen behandelen als onderdeel van applicatiedeployments
Een schemawijziging raakt niet alleen de database. Applicatieversies, achtergrondtaken, rapportages en externe integraties kunnen allemaal afhankelijk zijn van kolommen, constraints en indexen. Een deploy waarbij code en schema niet op elkaar zijn afgestemd, kan daarom fouten veroorzaken, ook als beide onderdelen afzonderlijk correct lijken. Behandel een wijziging aan het schema als een verandering in een gedeeld contract tussen alle onderdelen die de database gebruiken.
Breng vóór de wijziging in kaart welke processen het betrokken schema lezen of aanpassen. Denk aan webapplicaties, workers, geplande taken, beheertools en data-exporten. Vooral langlopende processen zijn gemakkelijk over het hoofd te zien: een worker kan na een deploy nog een oud procesmodel gebruiken terwijl de database al is gewijzigd. Ook meerdere applicatieversies kunnen tijdelijk gelijktijdig actief zijn tijdens een geleidelijke uitrol.
Leg vast welke schema-versie op elk moment beschikbaar moet zijn en welke applicatieversies daarmee overweg kunnen. Dat maakt de volgorde van stappen expliciet: eerst een compatibele uitbreiding, daarna code die de nieuwe structuur gebruikt, en pas later het verwijderen van oude structuren. Een wijziging die op één omgeving werkt, is niet automatisch veilig in productie. Productiedata, gelijktijdige transacties en afwijkende werkbelasting veranderen het gedrag van dezelfde DDL-instructie.
Door databasewijzigingen als deploymentonderdeel te plannen, worden afhankelijkheden zichtbaar voordat ze incidenten veroorzaken. Het schema is geen statische achtergrondlaag, maar een actief onderdeel van het systeem waarvan de levenscyclus met applicatiecode moet worden beheerd.
Expand-contract-migraties voor wijzigingen zonder downtime
Een expand-contract-migratie verdeelt een ingrijpende schemawijziging in stappen die gedurende de overgang naast elkaar kunnen bestaan. In de expand-fase voeg je de nieuwe structuur toe zonder de bestaande structuur direct te verwijderen. Dat kan bijvoorbeeld een nieuwe nullable kolom, tabel of index zijn. De oude applicatieversie blijft werken, terwijl de database alvast ruimte biedt voor de nieuwe implementatie.
Vervolgens pas je de applicatie aan om de nieuwe structuur te gebruiken. Bij een overgang van de kolom naam naar weergavenaam kan de code tijdelijk naar beide kolommen schrijven en, afhankelijk van de migratiefase, uit een van beide lezen. Na een backfill controleer je of de nieuwe kolom volledig en correct is gevuld. Pas wanneer alle actieve applicatieversies en achtergrondprocessen de oude kolom niet meer gebruiken, begint de contract-fase: oude codepaden en uiteindelijk de oude kolom kunnen verdwijnen.
Dubbel schrijven lijkt eenvoudig, maar introduceert risico’s. Een fout kan waarden in beide kolommen uiteen laten lopen, en elke schrijfroutine moet dezelfde regels toepassen. Bepaal daarom welke kolom leidend is, hoe afwijkingen worden opgespoord en wanneer dubbel schrijven stopt. Een feature flag kan helpen om leesgedrag gefaseerd om te zetten, maar vervangt geen controle op data-integriteit.
Deze aanpak kost meer stappen en tijdelijk extra opslag of code. Daar staat tegenover dat elke tussenfase een werkende combinatie van applicatie en schema kan behouden. Dat is vooral relevant wanneer deploys niet atomair zijn of verschillende instanties niet tegelijk kunnen worden bijgewerkt.
Migraties versioneren en veilig uitvoerbaar maken
Een migratiebestand beschrijft een specifieke overgang van de ene databasestructuur naar de volgende. Geef iedere migratie een vaste, unieke volgorde en sla de bestanden op in versiebeheer, samen met de applicatiecode waarop ze betrekking hebben. Een migratietabel in de database registreert welke stappen zijn uitgevoerd. Daardoor kan tooling vaststellen welke migraties nog ontbreken en voorkom je dat een script bij elke applicatiestart opnieuw wordt uitgevoerd.
Houd migraties klein en doelgericht. Een script dat tegelijk tabellen hernoemt, data transformeert en constraints aanscherpt, is lastig te testen en moeilijk te herstellen. Splits zulke veranderingen op in stappen met herkenbare tussenstanden. Controleer bovendien hoe de gebruikte database omgaat met transacties rond DDL. Sommige schemawijzigingen zijn transactioneel; andere veroorzaken impliciete commits of hebben beperkingen. Het gedrag verschilt per database en soms ook per versie.
Maak onderscheid tussen herhaalbaarheid en omkeerbaarheid. Een herhaalbare migratie kan veilig opnieuw worden gestart na een onderbreking, bijvoorbeeld doordat zij controleert of een object al bestaat. Dat betekent niet automatisch dat de wijziging zonder gegevensverlies teruggedraaid kan worden. Een verwijderde kolom kan niet worden hersteld met alleen een omgekeerd DDL-commando als de oorspronkelijke waarden verdwenen zijn.
Voer migraties uit met een expliciet deploymentproces en beperk wie ze kan starten. Laat de applicatie niet onverwacht schemawijzigingen uitvoeren bij het opstarten van iedere instantie: bij gelijktijdige starts kunnen processen elkaar blokkeren of concurrerende migraties proberen uit te voeren. Bewaar de migratiestatus en uitvoerlogs, zodat achteraf duidelijk is welke stap is gestart, voltooid of afgebroken.
Compatibiliteit tussen oude en nieuwe applicatieversies
Tijdens een rolling deployment draaien oude en nieuwe applicatieversies vaak enige tijd tegelijk. Een schemawijziging moet daarom niet alleen passen bij de nieuwste code, maar ook bij de versie die nog actief is. Een kolom verwijderen terwijl een oude instantie er nog queries op uitvoert, veroorzaakt fouten zodra die query opnieuw wordt uitgevoerd. Omgekeerd kan een nieuwe applicatie die verplicht een kolom schrijft onbruikbaar zijn zolang de database die kolom nog niet kent.
Maak voor iedere stap een compatibiliteitsmatrix: welke applicatieversies kunnen met de oude structuur werken, welke met de uitgebreide structuur en welke vereisen de nieuwe structuur? Neem daarin ook workers, cronjobs en consumenten van berichten op. Een webserver kan al vernieuwd zijn terwijl een worker uit een oudere release nog berichten verwerkt die op een oud datamodel zijn gebaseerd. Databasecompatibiliteit gaat dus verder dan het versienummer van de hoofdapplicatie.
Bij toevoegingen zijn nullable kolommen of defaults vaak geschikt om oude schrijvers te laten functioneren. Toch is een default niet altijd neutraal: databases kunnen defaults verschillend toepassen op bestaande rijen en nieuwe inserts, en applicatiecode kan aannemen dat een waarde altijd expliciet is aangeleverd. Test daarom zowel inserts van oude als nieuwe code. Bij verwijderingen is het veiliger eerst alle verwijzingen uit code en queries te verwijderen, de nieuwe versie uit te rollen en pas daarna de structuur op te ruimen.
Contracttests en integratietests kunnen vastleggen welke databasevorm een applicatieversie verwacht. Ze voorkomen niet elke productieafwijking, maar maken impliciete aannames controleerbaar. Besteed daarbij aandacht aan query’s buiten de ORM: handgeschreven SQL, dashboards en rapportages blijven vaak langer afhankelijk van oude velden dan de primaire applicatie.
Grote datamigraties uitvoeren met gecontroleerde backfills
Een schema-aanpassing is vaak klein, maar het vullen of transformeren van bestaande records kan groot werk zijn. Een enkele update over tientallen miljoenen rijen kan lang locks vasthouden, veel transactielog produceren en replica’s laten achterlopen. Verdeel een backfill daarom in beheersbare batches, bijvoorbeeld op een oplopende primaire sleutel. Een batch commit afzonderlijk, zodat locks korter blijven en een proces na een onderbreking verder kan vanaf een bekende positie.
Maak de update idempotent: opnieuw uitvoeren mag een record niet verkeerd transformeren of de uitkomst veranderen. Een filter zoals “alleen waar de nieuwe kolom nog leeg is” kan daarbij helpen, mits die aanname klopt. Bewaar voortgang in een controleerbare checkpoint of leid die af uit de data zelf. Een herstart moet niet afhankelijk zijn van een vluchtige procesvariabele.
Beperk de belasting op basis van meetbare signalen. Batchgrootte en pauzes kunnen worden afgestemd op querylatentie, databasebelasting, transactielog en replicatievertraging. Een vaste hoge snelheid kan op een rustige testdatabase goed werken en tijdens piekverkeer productie verstoren. Een backfill die lang draait, moet ook rekening houden met gelijktijdige writes. Als nieuwe records ondertussen alleen de oude kolom vullen, blijft de nieuwe kolom achter. Laat de applicatie tijdelijk beide velden bijwerken of ontwerp de migratie zo dat de backfill en live writes elkaar aanvullen.
Valideer de uitkomst met tellingen en gerichte controles, niet alleen met de melding dat het proces voltooid is. Vergelijk bijvoorbeeld aantallen ontbrekende waarden en controleer steekproeven op inhoudelijke juistheid. Bij complexe transformaties is een apart validatieproces verstandig, zodat fouten zichtbaar worden voordat de nieuwe kolom de enige bron van waarheid wordt.
Locking, indexen en de belasting van DDL-wijzigingen
Schemawijzigingen kunnen locks aanvragen die andere transacties tijdelijk blokkeren. Het effect hangt af van de database, de gekozen DDL en de activiteit op de tabel. Een kolom toevoegen kan relatief licht zijn, terwijl het wijzigen van een datatype of het toevoegen van een constraint een tabelscan of herschrijving vereist. Een korte migratie op een lege ontwikkelomgeving zegt weinig over de impact op een drukke productietabel met grote rijen en lange transacties.
Onderzoek voor elke DDL-instructie welke lockmodus nodig is, hoe lang die kan worden vastgehouden en of de database de wijziging online kan uitvoeren. Sommige systemen bieden opties om een index gelijktijdig op te bouwen. Dat vermindert blokkering van normale writes, maar kan meerdere scans uitvoeren, langer duren en alsnog conflicteren met andere DDL. Een mislukte indexbouw kan bovendien een gedeeltelijk object achterlaten dat eerst moet worden opgespoord en opgeruimd.
Gebruik waar beschikbaar een korte lock-timeout. Dan faalt de migratie liever gecontroleerd dan dat zij lang wacht terwijl nieuwe transacties zich opstapelen. Een timeout maakt de operatie niet vanzelf veilig: bepaal hoe vaak een retry mag plaatsvinden en controleer of een eerdere poging al een deel van het werk heeft verricht. Plan zware wijzigingen op basis van gebruikspatronen, maar beschouw een rustig tijdstip niet als vervanging voor een online strategie.
Monitor tijdens de uitvoering querylatentie, lockwachtrijen, CPU, schijfactiviteit en replicatieachterstand. Stopcriteria moeten vooraf zijn afgesproken, zodat een operator weet wanneer voortzetten meer risico oplevert dan onderbreken. Ook een kleine schemawijziging verdient observatie wanneer zij op een centrale tabel wordt uitgevoerd.
Rollbackstrategieën wanneer een migratie problemen veroorzaakt
Rollback betekent niet altijd dat een migratie met een omgekeerd script ongedaan kan worden gemaakt. Het toevoegen van een ongebruikte kolom is doorgaans eenvoudig terug te draaien. Een kolom verwijderen, waarden samenvoegen of data normaliseren kan informatie vernietigen die nodig is om de eerdere toestand te reconstrueren. Bepaal daarom vóór de uitvoering welk type herstel mogelijk is: applicatiecode terugzetten, een schema-aanpassing terugdraaien, data herstellen uit een back-up of vooruit migreren met een corrigerende wijziging.
Bij een expand-contract-aanpak is code rollback vaak minder riskant zolang de oude structuur nog bestaat. Als de nieuwe applicatie problemen geeft, kan verkeer terug naar de vorige versie terwijl de database compatibel blijft. Dat werkt alleen als de nieuwe versie geen onomkeerbare datamutaties heeft uitgevoerd en oude code de inmiddels gewijzigde data nog kan lezen. Leg die voorwaarden vast en test de terugschakelroute, in plaats van rollback uitsluitend als deploymentknop te beschouwen.
Voor destructieve veranderingen kan een back-up of point-in-time recovery nodig zijn. Herstel van een volledige database kan echter ook geldige transacties terugzetten of vereist dat verkeer tijdelijk wordt stilgelegd. Een selectieve correctie op basis van een auditlog of tijdelijke kopie is soms preciezer, maar vraagt om voldoende herleidbare gegevens. Een snapshot is pas bruikbaar als herstelprocedure en afhankelijkheden zijn getest.
Als een migratie halverwege stopt, inspecteer dan eerst de werkelijke toestand: migratieregistratie, tabellen, indexen, constraints en data. Voer niet blind hetzelfde script opnieuw uit en registreer de migratie ook niet handmatig als voltooid zonder verificatie. Een gerichte herstelmigratie kan veiliger zijn dan het geforceerd uitvoeren van een theoretische rollback die niet meer overeenkomt met de productiegegevens.
Migraties testen op productieachtige data en omstandigheden
Een migratie testen op een kleine, schone database controleert vooral syntaxis en basisgedrag. Het zegt weinig over uitvoertijd, lockgedrag of afwijkingen die in productie zijn ontstaan. Gebruik daarom representatieve datasets met vergelijkbare tabelgroottes, verdelingen, null-waarden en indexen. Gevoelige productiegegevens kunnen worden geanonimiseerd, maar de statistische kenmerken die de uitvoering beïnvloeden moeten zo veel mogelijk behouden blijven.
Test zowel de migratie zelf als de applicatieversies die tijdens de overgang gelijktijdig kunnen draaien. Een bruikbare scenario-test omvat de oude versie, de uitgebreide schemafase, de nieuwe versie en eventuele verwijdering van oude velden. Controleer reads en writes op elke tussenstap. Voeg ook gevallen toe voor lege waarden, dubbele records, onverwachte enumwaarden en records die buiten de normale applicatieroute zijn aangemaakt.
Test onderbreking en herstart. Stop een backfill midden in een reeks batches en controleer of die zonder dubbele of ontbrekende verwerking kan doorgaan. Laat een DDL-stap falen op een locktimeout en verifieer dat de database in een herkenbare toestand achterblijft. Een test van herstel is vooral belangrijk wanneer de wijziging data transformeert: vergelijk voor en na de operatie aantallen, constraints en domeinregels.
Maak testresultaten reproduceerbaar door databaseversie, configuratie en migratieversie vast te leggen. Verschillen tussen lokale, staging- en productieomgevingen kunnen gedrag veranderen, bijvoorbeeld door andere time-outs of extensies. Een migratie die in meerdere representatieve omgevingen dezelfde tussenstanden oplevert, is beter beoordeeld dan een script dat alleen een succesvolle eindstatus rapporteert.
Migraties volgen met logs, metrics en expliciete controlepunten
Een migratie is pas operationeel beheersbaar wanneer duidelijk is wat zij doet en hoe ver zij is. Registreer de migratienaam, starttijd, eindtijd, uitvoerende versie en resultaat. Voor een backfill zijn aantallen verwerkte en resterende records, de laatst verwerkte sleutel en fouten per batch nuttige signalen. Log geen gevoelige rij-inhoud; voortgang en diagnostiek zijn meestal voldoende om problemen te onderzoeken.
Koppel migratiemonitoring aan databasegedrag. Een voortgangsteller kan blijven oplopen terwijl de applicatie trager wordt, dus volg ook querylatentie, foutpercentages, lockwachttijden, CPU, schijfgebruik en replica-achterstand. Stel grenzen vast waarbij uitvoering wordt gepauzeerd of gestopt. Die grenzen moeten aansluiten op de normale belasting en op de gevolgen voor gebruikers en downstreamsystemen. Een monitor zonder eigenaar of afgesproken respons helpt weinig tijdens een incident.
Werk met controlepunten tussen stappen. Na het toevoegen van een kolom controleer je of de structuur bestaat; na de backfill controleer je volledigheid en inhoud; vóór het verwijderen van een oude kolom controleer je dat actieve code die niet meer gebruikt. Automatische controles kunnen aantallen of constraints valideren, terwijl een operator de belasting en uitzonderingen beoordeelt. Zo wordt een migratie geen ondoorzichtige reeks commando’s die pas aan het einde een succes- of foutmelding geeft.
Bewaar logs en migratiestatus op een manier die ook na een mislukte deploy beschikbaar blijft. Een proces kan crashen nadat de database een wijziging heeft toegepast maar vóór de uitvoerstatus is bijgewerkt. Door databaseobjecten, migratieregistratie en deploymentlogs naast elkaar te beoordelen, ontstaat een betrouwbaarder beeld van de actuele toestand dan uit één statusveld alleen.
Veelgestelde vragen
Hoe voorkom ik schema drift tussen ontwikkel-, test- en productieomgevingen?
Voorkom schema drift door wijzigingen uitsluitend via versiebeheerde migraties uit te voeren en de werkelijke databaseschema’s regelmatig met de verwachte schema’s te vergelijken. Handmatige aanpassingen in een omgeving kunnen tijdelijk nodig zijn, maar leg ze direct vast als migratie zodat ze reproduceerbaar worden. Neem een controle op schema-afwijkingen op in je releaseproces en onderzoek verschillen voordat je nieuwe migraties uitvoert. Zo voorkom je dat een deploy slaagt in test, maar faalt in productie door een onzichtbare afwijking.
Hoe test ik een migratie met realistische data zonder productiegegevens te kopiëren?
Test met synthetische of geanonimiseerde gegevens die de omvang en verdeling van productiegegevens zo goed mogelijk benaderen. Kleine testdatabases laten problemen met uitvoeringstijd, geheugengebruik of bijzondere waarden vaak niet zien. Zorg daarom ook voor realistische aantallen rijen, lange tekstwaarden, ontbrekende velden en uitzonderlijke datapatronen. Controleer vooraf dat anonimisering identificerende gegevens daadwerkelijk verwijdert. Zo kun je de migratie onder geloofwaardige omstandigheden beoordelen zonder gevoelige productiegegevens onnodig toegankelijk te maken.
Wat moet ik doen als een migratie halverwege is mislukt?
Onderzoek eerst de werkelijke databasestatus voordat je de migratie opnieuw start of als voltooid markeert. Controleer welke statements zijn uitgevoerd, welke objecten bestaan en of de migratietabel overeenkomt met de logs. Voer daarna een herstelstap uit die past bij de aangetroffen toestand: de migratie veilig hervatten, gedeeltelijke wijzigingen opruimen of een corrigerende migratie maken. Blind opnieuw uitvoeren kan dubbele of conflicterende wijzigingen veroorzaken. Leg de herstelbeslissing en de uiteindelijke status vast.
Hoe controleer ik of een back-up bruikbaar is voor herstel na een mislukte migratie?
Controleer de bruikbaarheid van een back-up door herstel regelmatig daadwerkelijk te oefenen in een aparte omgeving. Verifieer niet alleen dat het back-upbestand bestaat, maar ook dat de database opent, de verwachte gegevens aanwezig zijn en applicaties ermee kunnen verbinden. Meet hoe lang herstel duurt en vergelijk dat met de maximale onderbreking die acceptabel is. Bij herstel naar een specifiek tijdstip moet je ook nagaan of transactielogs beschikbaar zijn. Een niet-geteste back-up biedt geen zekerheid dat herstel op tijd lukt.
Hoe voorkom ik conflicten tussen databasemigraties uit verschillende ontwikkelbranches?
Voorkom conflicten door migraties een unieke, centraal beheerde volgorde te geven en branches vóór samenvoegen op elkaars nieuwste migraties te baseren. Twee ontwikkelaars kunnen onafhankelijk hetzelfde volgnummer kiezen of wijzigingen maken die alleen in hun eigen branch logisch op elkaar aansluiten. Controleer daarom bij code review zowel de volgorde als de uiteindelijke reeks migraties vanaf een schone database. Als branches botsen, maak dan een nieuwe migratievolgorde die voor alle omgevingen reproduceerbaar is; herschrijf geen migraties die al zijn uitgevoerd.