Introduktion
“Vi er nødt til at tage siden ned til vedligeholdelse” er en sætning, der burde være uddød i 2024. Alligevel ser jeg stadig teams, der planlægger nedetid til databasemigreringer, som sagtens kunne udføres sikkert online.
Denne guide dækker de mønstre, jeg bruger til at migrere databaser uden nedetid, selv for tabeller med millioner af rækker.
Den gyldne regel
Foretag aldrig en ændring, der er inkompatibel med den applikationskode, der kører lige nu.
Det betyder, at migreringer sker i flere faser:
- Deploy kode, der fungerer med både det gamle og det nye skema
- Kør migreringen
- Deploy kode, der kun fungerer med det nye skema
- Ryd op (valgfrit)
Sikre operationer
Disse operationer er generelt sikre at køre uden nedetid:
Tilføjelse af en nullable kolonne
ALTER TABLE users ADD COLUMN middle_name VARCHAR(100);
Dette er sikkert, fordi:
- Eksisterende kode ikke kender kolonnen (ignorerer den)
- Ny kode kan håndtere NULL-værdier
Tilføjelse af et indeks samtidigt
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
Nøgleordet CONCURRENTLY er afgørende - uden det låses tabellen under indeksoprettelsen.
Tilføjelse af en ny tabel
CREATE TABLE user_preferences (
user_id INTEGER REFERENCES users(id),
preferences JSONB
);
Nye tabeller påvirker ikke eksisterende kode.
Farlige operationer
Disse kræver omhyggelig håndtering:
Omdøbning af en kolonne
Forkert måde (forårsager nedetid):
ALTER TABLE users RENAME COLUMN name TO full_name;
Rigtig måde (ingen nedetid):
- Tilføj ny kolonne:
ALTER TABLE users ADD COLUMN full_name VARCHAR(200);
- Deploy kode, der skriver til begge kolonner:
await db.query(
'UPDATE users SET name = $1, full_name = $1 WHERE id = $2',
[name, userId]
);
- Efterudfyld eksisterende data:
UPDATE users SET full_name = name WHERE full_name IS NULL;
-
Deploy kode, der læser fra den nye kolonne, men skriver til begge
-
Deploy kode, der kun bruger den nye kolonne
-
Slet den gamle kolonne:
ALTER TABLE users DROP COLUMN name;
Tilføjelse af en NOT NULL-betingelse
Forkert måde:
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
Dette gennemsøger hele tabellen og låser den.
Rigtig måde:
- Tilføj en check-betingelse (låser ikke):
ALTER TABLE users ADD CONSTRAINT users_email_not_null
CHECK (email IS NOT NULL) NOT VALID;
- Validér betingelsen (gennemsøger, men låser ikke):
ALTER TABLE users VALIDATE CONSTRAINT users_email_not_null;
- Konvertér til NOT NULL (øjeblikkeligt, bruger eksisterende betingelse):
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
ALTER TABLE users DROP CONSTRAINT users_email_not_null;
Ændring af kolonnetype
Forkert måde:
ALTER TABLE orders ALTER COLUMN amount TYPE DECIMAL(10,2);
Rigtig måde: Samme mønster som ved omdøbning - tilføj ny kolonne, migrér data, skift over.
Migreringer af store tabeller
For tabeller med millioner af rækker kræver selv “sikre” operationer omhu.
Batchvis efterudfyldning
Opdatér aldrig millioner af rækker i én transaktion:
async function backfillInBatches(batchSize = 1000) {
let processed = 0;
while (true) {
const result = await db.query(`
UPDATE users
SET full_name = name
WHERE id IN (
SELECT id FROM users
WHERE full_name IS NULL
LIMIT $1
)
RETURNING id
`, [batchSize]);
processed += result.rowCount;
console.log(`Processed ${processed} rows`);
if (result.rowCount < batchSize) break;
// Kort forsinkelse for at reducere belastningen
await sleep(100);
}
}
Overvågning under migreringer
Hold øje med disse målinger:
- Database-CPU og -I/O
- Replikationsforsinkelse
- Forespørgselslatens
- Ventetider på lås
Test af migreringer
Lokal test
- Dump produktionsskemaet (ikke dataene)
- Anvend migreringen lokalt
- Kør applikationstests
Staging-test
- Gendan en nylig produktionsbackup i staging
- Kør migreringen
- Verificér, at applikationen fungerer
- Tjek migreringens varighed
Tør prøvekørsel i produktion
Til kritiske migreringer:
BEGIN;
-- Kør migrering
-- Tjek resultater
ROLLBACK;
Rollback-strategi
Hav altid en rollback-plan:
- Additive ændringer: Kræver normalt ikke rollback (nye kolonner ignoreres)
- Datamigreringer: Behold den gamle kolonne, indtil du er sikker
- Destruktive ændringer: Hav en genoprettelsesplan
Et konkret eksempel: Tilføjelse af en fremmednøgle
Vi skulle tilføje en fremmednøgle fra orders til customers på en tabel med 50 mio. rækker.
Fremgangsmåde:
- Tilføj betingelse uden validering:
ALTER TABLE orders
ADD CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customers(id)
NOT VALID;
- Validér i batches i en periode med lav trafik:
ALTER TABLE orders VALIDATE CONSTRAINT fk_orders_customer;
Resultat: Ingen nedetid, gennemført på 45 minutter under normal trafik.
Værktøjer og automatisering
Migrerings-lintere
Brug værktøjer, der fanger farlige mønstre:
- strong_migrations (Ruby)
- squawk (PostgreSQL)
Integration i deployment
Gør migreringer til en del af din deployment-pipeline:
- Kør migreringer, før ny kode deployes
- Verificér, at migreringerne lykkedes
- Deploy ny applikationskode
Konklusion
Migreringer uden nedetid kræver mere planlægning og flere deployments, men det er indsatsen værd. Dine brugere oplever ingen afbrydelser, og du kan deploye med tillid til enhver tid.
De centrale principper:
- Bryd aldrig kompatibiliteten med den kørende kode
- Foretag ændringer i små, reversible trin
- Test grundigt før produktion
- Overvåg under og efter migreringer
Begynd at behandle databaseændringer med samme omhu som applikationskode, og “planlagt vedligeholdelse” bliver fortid.