Zero-Downtime Schema Changes

Expand-contract pattern, online schema changes, column renames, index creation, and backward compatibility.

Overview

Overview

Schema migrations that lock tables cause downtime. On a high-traffic database even a millisecond-level lock on a large table builds a connection queue that looks like an outage, so zero-downtime migrations need a disciplined approach.

Diagnosis flow: is this migration safe?

The expand-contract pattern

  • Expand — Add the new structure (new column, table or index) alongside the old — Old and new code both keep working
  • Contract — After all code is deployed and no longer uses the old structure, remove it — Separates the deploy timeline from the migration timeline

Operations and their lock behavior

  • NOT NULL column with a DEFAULT, PostgreSQL before 11 — Full table rewrite under a table lock — Upgrade, or add nullable and backfill
  • NOT NULL column with a constant default, PostgreSQL 11+ — No rewrite — Safe as is
  • NOT NULL column with a computed default — Needs a backfill — Add as NULLABLE, backfill in batches, then add a CHECK ... NOT VALID constraint and VALIDATE it; the scan does not block writes
  • Index creation, PostgreSQL — CREATE INDEX blocks writes — CREATE INDEX CONCURRENTLY: slower, but reads and writes continue
  • Index or supported alters, MySQL — Full table copy — ALTER TABLE ... ALGORITHM=INPLACE
  • Column rename — Table lock — Expand-contract only

> [!IMPORTANT] Always use CONCURRENTLY for index creation in production.

Online schema change tools

  • pt-online-schema-change (Percona) — MySQL — Triggers
  • gh-ost (GitHub) — MySQL — Binary log streaming instead of triggers
  • pgroll — PostgreSQL — Creates the new structure and cuts over

> [!WARNING] Column rename is one of the most dangerous migrations: without a table lock the only way is add new column, copy data, switch the application, drop the old column, across multiple deployments.

Key concepts

Expand-contract
Add new structure alongside old (expand), deploy code using new structure, remove old structure (contract). Decouples the migration from the deploy. Requires multiple deploy phases.
CREATE INDEX CONCURRENTLY
PostgreSQL command to create an index without locking the table. Allows concurrent reads/writes. Takes longer. Can fail if a concurrent write conflicts — drop the partial index and retry.
Backfill in batches
UPDATE in small batches (1000-10000 rows) with sleep between batches to avoid locking the table. UPDATE large_table SET new_col = compute(old_col) WHERE id BETWEEN 1 AND 1000.
gh-ost
GitHub online schema change for MySQL. Streams binary log (no triggers), creates shadow table, applies changes, does atomic cut-over. Zero writes to the original table during migration.
pgroll
PostgreSQL online schema change tool. Uses multi-version schema approach — both old and new schema are active simultaneously, allowing multiple application versions during rollout.

When to use it

  • Planning a schema change for a table with millions of rows in production
  • Explaining why renaming a column requires multiple deploys
  • Using the expand-contract pattern to add a NOT NULL column safely
  • Choosing between pt-osc and gh-ost for a MySQL migration

Interview questions

  • How do you safely rename a column in a production PostgreSQL table with 500 million rows without downtime?

Sign up free to read the full topic

More Troubleshooting topics

Start free on Cariara · Coding problems · System design studio