⚡THE SHORT ANSWER
By adopting the multi-phase Expand and Contract (Parallel Run) pattern: adding new nullable columns (Expand), dual-writing to both columns in application code, asynchronously backfilling legacy rows in small batches, switching reads to the new column, and safely dropping the old column in a final release (Contract).
Engineering Handbook & Failure Dynamics
6-Dimensional Architecture Breakdown⚙️1. Underlying Mechanism
Execution🎯2. Appropriate Use Context
Scope⚠️3. Production Failure Modes
P0 Risk📡4. Diagnostic Signals & Telemetry
Telemetry🛡️5. Prevention & Safeguards
Safeguards⚖️6. Architectural Trade-offs
Trade-offCase Study (TinyCTO In-Field Example)
TinyCTO Incident 038: A junior engineer ran ALTER TABLE transactions ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id); on a 400M-row table. Postgres placed an Exclusive Lock on both tables to validate every row, halting checkout for 28 minutes. Re-executing with NOT VALID followed by VALIDATE CONSTRAINT validated the constraint asynchronously with zero downtime.
Interactive Concept Drills
3 CardsWhat are the 5 discrete steps of the Expand and Contract database migration pattern?
Why is `CREATE INDEX CONCURRENTLY` required in PostgreSQL for production tables?
What should you always set at the beginning of a migration transaction to prevent catastrophic lock queues?
Zero-Downtime Database Migrations (Expand & Contract) — Technical FAQ
How does gh-ost (GitHub Online Schema Transformations) perform zero-downtime migrations in MySQL?
gh-ost creates a ghost table with the new schema, reads the binary log (binlog) to stream ongoing writes asynchronously, backfills historical data in chunks, and swaps the tables using an atomic rename.
How do you add a `NOT NULL` constraint to a huge table without locking it?
Add a `CHECK (column IS NOT NULL) NOT VALID;` constraint (instant, no lock), then execute `ALTER TABLE ... VALIDATE CONSTRAINT;` which verifies rows in the background without exclusive locks.
Why should you never execute large data backfills inside the main migration transaction?
Long-running transactions hold open row locks, prevent database autovacuuming, bloat write-ahead logs (WAL), and dramatically increase failover recovery time if interrupted.
🤖 AEO & Key Facts Summary
Key Architectural Facts
- ▸
Every DDL operation (even fast ones) requires an AccessExclusiveLock in PostgreSQL, which waits for all currently executing queries on that table to finish.
- ▸
Never rename a database column directly in a single release; always use the Expand and Contract pattern across at least two deployments.
Common Misconceptions
- ✗
Believing that ORM migration tools (like Prisma migrate or Django migrations) automatically prevent locks; standard ORM migrations generate dangerous blocking DDL by default unless customized.
Decision & Governance Guidance
Enforce the Expand and Contract pattern for any schema change on production tables exceeding 100,000 rows. Always configure lock timeouts and concurrent index creation.
Authoritative Sources & Standards
- [OFFICIAL-DOC]Parallel Change (Expand and Contract)— Martin Fowler
- [OFFICIAL-DOC]gh-ost: GitHub's triggerless online schema migration tool for MySQL— GitHub Engineering
