Zero-downtime DB Migration and Schema Changes
Adding or renaming a column in a table while the live website keeps running, or moving the whole DB to a new system, without downtime and without data loss.
58.1 Why is it used?
- If you run ALTER TABLE directly on a big table (50 crore rows), the table can lock and the site can be down for minutes or hours.
- For 24x7 businesses (Amazon, Paytm, Swiggy) even a 'Sunday 2 AM maintenance window' is not acceptable.
- In a rolling deployment the old and new code run together. The schema has to be compatible with both.
- Moving from on-prem Oracle to cloud Aurora/PostgreSQL, or from MySQL to a sharded cluster: traffic must keep flowing while data is migrated.
- When schema changes are version-controlled, every environment (dev, staging, prod) gets the same schema and rollback is possible.
58.2 How it works
- 1.R1 Expand: ADD a nullable column with gh-ost, no lock (phone = 98xxxx, mobile_no = NULL)
- 2.R2 Dual write: app writes to both + backfill 10K rows/batch, then verify (reads still from phone)
- 3.R3 Read new: reads from mobile_number, feature flag 1% -> 100%
- 4.R4 Stop old: stop writing to phone, monitor for a few days
- 5.R5 Contract: drop the phone column with Flyway V5__drop_phone.sql
After verifying R2: COUNT(*) WHERE mobile_number IS NULL = 0 and sample checksums match. Every step can be rolled back. Mistake: rename + code change in one release, and old pods crash during the rolling deploy
- Expand-Contract (parallel change): first add the new column/table (expand), the code writes to both, backfill the data, move reads to the new one, then remove the old one (contract).
- The safe way to rename: never a direct RENAME. Add new_col -> dual write -> backfill -> read new -> stop old write -> drop old_col; each step is a separate deploy.
- Backward compatible changes: add a nullable column or one with a default. Add the NOT NULL constraint later, once the data is filled.
- Online schema change tools: create a shadow table, copy data in chunks, sync live changes, and finish with an atomic rename (cut-over).
- gh-ost reads live changes from the binlog (no triggers), while pt-online-schema-change uses triggers. Both can throttle.
- Migration tools (Flyway/Liquibase): versioned scripts like V1__create.sql, V2__add_col.sql; a history table in the DB tells which ones were applied.
- Full DB migration: snapshot copy + continuous sync via CDC (binlog/WAL) -> verify data (row counts, checksums) -> compare shadow reads -> traffic switch.
- Dual writes: the app writes to both DBs. It looks simple but a partial failure causes inconsistency; CDC based sync or an outbox is safer.
- 1.The live orders table keeps running, gh-ost creates the _orders_gho ghost table + new column
- 2.Rows are copied in chunks
- 3.Live changes are synced into the ghost table from the binlog
- 4.Cut-over: atomic RENAME, a pause of a few ms
- 5.Pause / throttle control keeps the prod load low
pt-online-schema-change syncs with triggers, so write load increases. In Postgres you use CREATE INDEX CONCURRENTLY and pgroll
- 1.Snapshot copy + AWS DMS full load, then continuous CDC sync
- 2.Verify: row counts, checksums
- 3.Shadow reads: compare the results from both DBs
- 4.Traffic switch: a cut-over of a few seconds
Rollback plan: keep the old DB ready and in sync
58.3 Tools you can use
| Tool | Type | How it is used |
|---|---|---|
| gh-ost (GitHub) | MySQL online schema change | Triggerless; reads the binlog to sync the ghost table, with pause/throttle/cut-over control. |
| pt-online-schema-change (Percona) | MySQL online schema change | Copies live changes with a new table + triggers, copies data in chunks, and ends with RENAME TABLE. |
| Flyway | Migration versioning tool | SQL files named V1__, V2__; flyway migrate runs at app startup or in CI/CD. |
| Liquibase | Migration versioning tool | XML/YAML/SQL changelogs, rollback support, diff and multiple DB types. |
| AWS DMS / Azure DMS / GCP Database Migration Service | Managed DB migration | Full load + ongoing CDC replication from source to target DB, with a minimal-downtime cut-over. |
| Debezium | CDC (open-source) | Reads the MySQL binlog / Postgres WAL and sends changes to Kafka; keeps a new DB/store in sync. |
| pgroll / PostgreSQL native | Postgres online change | CREATE INDEX CONCURRENTLY, ADD COLUMN with default (fast in PG 11+); pgroll automates expand-contract. |
58.4 Comparison: gh-ost vs pt-online-schema-change vs Flyway/Liquibase vs AWS DMS
| Point | gh-ost | pt-online-schema-change | Flyway / Liquibase | AWS DMS |
|---|---|---|---|---|
| Job | Online ALTER of a big MySQL table | Online ALTER of a big MySQL table | Versioning and applying schema changes | Migrate data from one DB to another |
| Mechanism | Binlog streaming, triggerless | Triggers on the original table | Ordered scripts + history table | Full load + CDC |
| Prod load impact | Low, can pause/throttle | Triggers increase write load | Depends on the script (can lock) | Read load on the source |
| Downtime | Near zero (short cut-over) | Near zero (short rename) | Not zero-downtime by itself, follow expand-contract | A few seconds/minutes at cut-over |
| DB support | MySQL only | MySQL only | Many DBs | Many DB pairs, including heterogeneous ones |
| Best for | Adding a column/index on a huge table | Where there is no binlog access or you want a simple setup | Every schema change in CI/CD | On-prem -> cloud or an engine change |
58.5 When to choose which
- Flyway/Liquibase: in every project. Schema changes live in git with the code and are applied through the pipeline.
- gh-ost: an ALTER on a MySQL table with crores of rows, when you want to keep prod load low and want control.
- pt-online-schema-change: MySQL, simple to use, triggers are acceptable, no binlog setup.
- AWS DMS / Debezium: moving a whole database from one place to another, with live sync through CDC.
- In every case the pattern is the same: expand-contract + backward compatible code + verify + gradual switch + rollback plan.
58.6 Real-world example
Scenario: renaming the 'phone' column to 'mobile_number' in the orders table (50 crore rows).
- Release 1 (expand): add a nullable mobile_number column with gh-ost. No table lock, the copy happens in the background.
- Release 2: the app writes to both columns (phone and mobile_number), reads still come from phone.
- A backfill job fills mobile_number = phone in the old rows in chunks (10,000 rows/batch, with sleeps).
- Verify: count WHERE mobile_number IS NULL = 0, and sample checksums match.
- Release 3: reads come from mobile_number; traffic goes 1% -> 100% through a feature flag, and if there is a problem the flag is turned off.
- Release 4: stop writing to phone; monitor for a few days.
- Release 5 (contract): drop the phone column with a Flyway script. Every step was reversible, and downtime was zero.
- GitHub said in a public blog that they built gh-ost for exactly this reason: trigger-based tools caused load issues on busy MySQL tables.
58.7 Interview tips
- Column rename + code change in a single release: old pods crash during a rolling deploy.
- Ignoring locks: in Postgres, CREATE INDEX without CONCURRENTLY, or an ALTER stuck behind a long transaction building a lock queue, which blocks the whole table.
- A migration without a rollback/verification plan; not comparing checksums after the data copy.
- Interview tip: explain the expand-contract steps clearly and explain the trade-off of 'dual write vs CDC'.