Lesson 7 of 9 · 20m
Zero-Downtime Migrations
Column adds are free; rewrites are not. The expand-contract pattern, concurrent index builds, and the migration habits that don't take the table lock at 3pm.
The migration that takes your database down isn't the one that adds a table — it's ALTER TABLE big_table ADD COLUMN x NOT NULL DEFAULT 'y': a full table rewrite under an access-exclusive lock. Every zero-downtime migration strategy is a variation on "never hold a lock while doing heavy work."
What's cheap and what's expensive
- Cheap (metadata only):
ADD COLUMNnullable or with a constant default (Postgres 11+ stores it in metadata),DROP COLUMN,RENAME,CREATE INDEX CONCURRENTLY. - Expensive (rewrite or scan):
ALTER COLUMNtype changes,ADD COLUMNwith a volatile default (now()),SET NOT NULLwithout a pre-validated check,VALIDATE CONSTRAINTon huge tables.
Expand-contract for risky changes
Never change a column in place when readers depend on the old shape:
- Expand: add the new column (
new_email), deploy code that writes to both and reads the old. - Backfill: copy old→new in batches (1k rows/transaction — don't hold a transaction open for an hour).
- Flip: deploy code that reads the new column.
- Contract: drop the old column after the old code is fully gone.
Painful? Yes. Cheaper than an outage? Always.
The lock discipline
CREATE INDEX CONCURRENTLY— builds without blocking writes; can't run inside a transaction; monitorpg_stat_progress_create_index.ALTER TABLE ... ADD CONSTRAINT ... NOT VALID+VALIDATE CONSTRAINT— the two-step that skips the full scan under lock.lock_timeout— set it in migration scripts (SET lock_timeout = '2s'): a migration that waits 10 minutes for a lock queues every write behind it.- Batch backfills with
pg_sleepbetween chunks andLIMIT-based keyset iteration.
Field notes
- Test migrations on a production-sized copy — a 30-second staging migration can be a 30-minute prod one.
pg_stat_activityduring migrations: watch foridle in transactionsessions holding locks.- Migrations should be one-way and idempotent where possible — a rolled-back deploy should leave a working database.