Database migrations in production are terrifying — one mistake can corrupt data, cause downtime, or lock a critical table for hours. Yet every application needs them. This guide covers battle-tested strategies for running database migrations with zero downtime, including the expand-contract pattern, handling large tables, and reversible migrations.

Migration Strategies Compared

StrategyDowntimeComplexityBest For
Expand-ContractZeroHighSchema changes on high-traffic tables
Online Schema Change (gh-ost, pt-online-schema-change)ZeroMediumALTER TABLE on large MySQL tables
Blue-Green DatabaseNear-zeroVery HighMajor version upgrades, risky operations
Deploy + Migrate (simultaneous)Brief (seconds)LowSmall apps with maintenance windows
Shadow Table MigrationZeroMediumReshaping or cleaning data with dual writes

The Expand-Contract Pattern (Zero-Downtime)

Best for: Adding, renaming, or removing columns without downtime. The key insight: deploy in multiple phases, and each phase must be compatible with the previous version.

Example: Renaming a Column (users.name → users.full_name)

PhaseWhat to DoApp Behavior
1. ExpandAdd new column full_name (nullable), write to BOTH columnsApp writes to both old and new column
2. BackfillCOPY name into full_name for existing rowsApp reads from new column, falls back to old; writes to both
3. Migrate readsDeploy code that reads only from new columnApp reads from full_name only, writes to both
4. ContractStop writing to old column, eventually DROP itApp reads and writes full_name only

Handling Large Tables (100M+ Rows)

Critical rule: Never run a blocking ALTER TABLE on a large production table — it acquires an ACCESS EXCLUSIVE lock for the duration, blocking all reads and writes.

DatabaseSafe SolutionTool
PostgreSQLAdd CHECK constraints as NOT VALID, validate laterBuilt-in: ADD CONSTRAINT ... NOT VALID; ALTER CONSTRAINT ... VALIDATE
PostgreSQLCreate index with CONCURRENTLYCREATE INDEX CONCURRENTLY (no table lock)
PostgreSQLAdd column with a default (PG 11+)ALTER TABLE ... ADD COLUMN ... DEFAULT (no rewrite in PG 11+)
MySQLOnline schema changegh-ost (GitHub), pt-online-schema-change (Percona)
SQLiteBatched writes in a transactionWrap in BEGIN/COMMIT, limit batch size to ~10,000 rows

Reversible Migrations

Every migration should have a planned rollback. Before running a migration, write (and test) the down migration:

ChangeUp MigrationDown Migration
Add columnALTER TABLE users ADD COLUMN bio TEXT;ALTER TABLE users DROP COLUMN bio;
Add NOT NULL columnAdd nullable → backfill → set NOT NULL (3-phase)ALTER TABLE users ALTER COLUMN bio DROP NOT NULL;
Rename columnExpand-contract (4 phases, see above)Reverse the expand-contract phases
Add indexCREATE INDEX CONCURRENTLY ...;DROP INDEX CONCURRENTLY ...;

Bottom line: The expand-contract pattern is the gold standard for zero-downtime migrations — deploy changes in small, compatible steps. For any ALTER TABLE on a large production table, use your database's non-blocking equivalent (CONCURRENTLY for PostgreSQL, gh-ost for MySQL). Never run a migration you cannot roll back. See also: Database Design Fundamentals and PostgreSQL vs MySQL vs SQLite.