Migrations & Seeding
Schema evolution done right: versioned, ordered, reviewable migrations with rollback paths, applied exactly once everywhere. Plus idempotent seeding so every environment — from CI to production — converges on the same structure and data.
What is this?
Migrations are versioned, ordered schema changes applied in sequence, so every environment converges on the same structure. Each migration has a unique ID and records itself in a tracking table, guaranteeing a change applies exactly once. Seeding loads initial or development data. Together they make schema evolution a first-class, reviewable and reversible part of CI/CD.
Why do you need it?
- Without versioned migrations, schemas drift: production differs from staging and nobody knows why.
- Migrations give every change a code review, a rollback path and an audit trail.
- They apply identically to empty CI databases and long-lived production — no manual ALTER TABLE.
- Seeding standardizes realistic dev/QA data so bugs surface before deploy.
- Downtime-free patterns (expand-contract, batched backfill) change schema while traffic keeps flowing.
Mental model
A recipe listing every step and ingredient in order
A migration sequence
Skip a step and the cake fails; run a step twice and you spoil it. Versioned migrations number the steps so every kitchen applies them exactly once, in the same order.
How it works
- Each change is authored as an ordered, uniquely named up/down file pair, often protected by a checksum.
- A tracking table (schema_migrations) records the version of the last applied change.
- On deploy, the tool compares applied versions with available files and runs the missing ones in order.
- Each migration typically runs inside a transaction so a failure rolls back cleanly.
- The tracking row is inserted only after success, so a failed run retries cleanly.
- Seeding then populates reference data or a test dataset idempotently.
Applying a schema migration through CI/CD.
Key commands
goose create add_teams sqlGenerate a new, timestamped up/down SQL migration pair (goose).
goose upApply all pending migrations in order.
OK 004_add_teams.sql (19.3ms)
goose downRoll back the most recent migration using its down file.
goose statusShow each migration's version and applied state.
Applied At Migration\n2026-05-01 004_add_teams.sql\nPending 003_create_orders.sql
alembic revision -m "add teams"Create a new revision with SQLAlchemy/Alembic.
alembic upgrade headApply all pending Alembic revisions up to the latest.
Examples
A versioned up/down migration pair
-- 004_add_teams.up.sqlCREATE TABLE teams ( id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE, created_at TIMESTAMPTZ NOT NULL DEFAULT now()); ALTER TABLE users ADD COLUMN team_id BIGINT REFERENCES teams(id); CREATE INDEX idx_users_team ON users (team_id); -- 004_add_teams.down.sqlDROP INDEX IF EXISTS idx_users_team;ALTER TABLE users DROP COLUMN IF EXISTS team_id;DROP TABLE IF EXISTS teams;The up file adds the teams table, a foreign key column and an index; the down file reverses each step in the reverse order. The name 004 encodes the version and the tool records it in schema_migrations so it runs once.
In production
- CI runs migrations against a clean scratch database to validate every change before deploy.
- Expand-contract deployments add columns with NULL defaults first, backfill, then drop old columns.
- Release trains batch migrations with the application version so schema and code deploy together.
- Seed scripts rebuild demo environments and load reference tables (currencies, regions, roles) idempotently.
Best practices
- Treat applied migrations as immutable history; write a new migration instead of editing an old one.
- Add columns as nullable or with defaults first so old application code keeps working.
- Backfill large tables in batches with a WHERE range, not one giant UPDATE.
- Split risky migrations (ADD NOT NULL + backfill) across deploy windows and verify with row counts.
- Run one migration process at a time and rely on its locking to prevent double-apply.
Common mistakes
- Editing an already-applied migration to 'fix' production, breaking checksums and replayability.
- Running a long ALTER during peak traffic, locking the table and stalling requests.
- Forgetting a down migration, leaving the rollback path incomplete.
- Shipping a migration that rebuilds a huge table without a zero-downtime strategy.
Common issues
goose: Lock held by another migration processTwo migration runs started concurrently.
Serialize deploys with a single runner; the lock clears when the other process finishes.
checksum mismatch for migration 004An applied migration file was edited after it ran.
Never edit applied files; create a corrective migration instead.
ERROR: column "team_id" of relation "users" already existsThe migration ran before but was not recorded, or was edited and re-run.
Verify goose status, repair the tracking table to match reality, then re-run.
Debugging tips
- Run goose status / alembic history on every environment to compare applied vs pending versions.
- Test migrations against a copy of production data — restore a dump to staging first.
- Watch the migration lock table: a stuck lock means another run is holding it.
- Wrap backfills in transactions and count rows per batch to estimate total time.
Interview checkpoint
Hands-on lab
- Create a versioned migration up/down pair
- Apply and verify migrations on a clean database
- Roll back with the down migration
- Seed reference data idempotently
Versioned Schema Evolution
40 min · advanced
Quick quiz
Where does a migration tool record which versions have been applied?
Editing a migration that already ran in production is safe as long as you fix the tracking table.
A backfill UPDATE of 50 million rows is planned. What is the safest approach?
Cheat sheet
createGenerate an up/down pairstatusApplied vs pending versionsupApply pending migrationsdownRoll back one migrationredoRoll back then re-applyAdd column nullableNo immediate rewriteBackfill in batchesBound lock and I/OAdd NOT NULLOnly after backfillDrop old columnAfter app rolloutSeed idempotentlyINSERT ... ON CONFLICT