DB Migration Guide
Migration Tools
| Tool | Language | Approach | Best For |
|---|---|---|---|
| Flyway | Java/CLI | Versioned SQL files (V1__name.sql) | Java apps, SQL-first teams |
| Liquibase | Java/CLI | XML/YAML/SQL changelogs | Multi-DB, complex change sets |
| golang-migrate | Go/CLI | Numbered .up.sql / .down.sql | Go apps, simple SQL migrations |
| Alembic | Python | Python scripts with upgrade/downgrade | SQLAlchemy/Python projects |
| Prisma Migrate | TypeScript | Schema-diff based, auto-generated | Node.js/TypeScript apps |
| Atlas | Go/CLI | Declarative (desired state) or versioned | Modern Go apps, CI/CD |
Versioned Migration File Structure
db/migrations/
โโโ 000001_create_users.up.sql
โโโ 000001_create_users.down.sql
โโโ 000002_add_email_index.up.sql
โโโ 000002_add_email_index.down.sql
โโโ 000003_add_orders_table.up.sql
โโโ 000003_add_orders_table.down.sql
# golang-migrate example
migrate -path db/migrations -database "postgres://..." up # run all pending
migrate -path db/migrations -database "postgres://..." up 1 # run 1 migration
migrate -path db/migrations -database "postgres://..." down # rollback all
migrate -path db/migrations -database "postgres://..." version # current version
# Flyway example
V001__create_users.sql -- versioned
R__refresh_materialized_view.sql -- repeatable
U001__undo_create_users.sql -- undo (paid feature)
Zero-Downtime: Expand-Contract Pattern
-- Scenario: Rename column "username" to "display_name"
-- NEVER do this (breaks live app immediately):
-- ALTER TABLE users RENAME COLUMN username TO display_name;
-- STEP 1 (Expand): Add new column, keep old
ALTER TABLE users ADD COLUMN display_name VARCHAR(100);
-- STEP 2: Backfill new column
UPDATE users SET display_name = username WHERE display_name IS NULL;
-- STEP 3: Add NOT NULL constraint gradually
ALTER TABLE users ALTER COLUMN display_name SET DEFAULT '';
-- wait for app to write both columns...
-- STEP 4: Deploy new app code using display_name
-- (dual-write: write both columns during transition)
-- STEP 5 (Contract): Remove old column after full deploy
ALTER TABLE users DROP COLUMN username;
-- Zero-downtime index creation (PostgreSQL)
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
-- CONCURRENTLY builds without locking writes (takes longer but safe)
Migration Checklist
| Step | Action |
|---|---|
| Before | Take a full database backup |
| Before | Test migration on staging with production data copy |
| Before | Estimate lock duration for DDL changes |
| Before | Prepare rollback script and test it |
| During | Monitor active connections and replication lag |
| During | Use statement_timeout to avoid indefinite locks |
| After | Verify row counts and data integrity |
| After | Monitor slow queries for index usage |
| Cleanup | Remove deprecated columns only after old app code is gone |