What Are Database Migrations?
Versioning a database schema the same way you version code, instead of editing production by hand.
2 min read
A database schema isn't static — new features need new tables and columns, and existing ones need to change shape. Migrations are the standard way to make those changes safely, repeatably, and in a way every environment (a teammate's laptop, staging, production) can apply in the same order.
The problem migrations solve
Without them, schema changes tend to happen by someone connecting directly to the production database and running an ALTER TABLE by hand. That approach has no history, no way to reproduce the same change on staging or a new developer's machine, and no way to undo it cleanly if it turns out to be wrong.
What a migration looks like
A migration is a small, version-controlled file describing one schema change, usually with both a forward step and a way to reverse it:
-- 0007_add_orders_status.sql (up)
ALTER TABLE orders ADD COLUMN status TEXT NOT NULL DEFAULT 'pending';
-- 0007_add_orders_status.sql (down)
ALTER TABLE orders DROP COLUMN status;A migration tool tracks which migrations have already been applied to a given database (usually in a small internal table like schema_migrations), and applies only the ones that haven't run yet, in order:
$ migrate up
Applying 0006_create_orders_table... done
Applying 0007_add_orders_status... done
Why they're checked into version control
Migrations live in the codebase alongside the application code that depends on the schema they produce. Checking out an older commit means checking out the migrations that match it — the schema history and the code history stay in lockstep, and any teammate (or CI pipeline, or new production deploy) can run migrate up and end up with an identical schema, deterministically.
Migrations that change existing data need care
Adding a new nullable column is low-risk. Migrations that change or move existing data are where real danger lives:
- Renaming a column in one step, on a live system, can break the currently-running version of the application that still expects the old name — mid-deploy, both versions may be running at once.
- A large
UPDATEon a huge table can lock rows or the whole table long enough to cause visible downtime. - Dropping a column should typically happen in a later, separate migration after the application code has stopped reading/writing it — never in the same deploy that removes the code's last reference, so a rollback of the code doesn't hit a missing column.
The safe pattern for a risky change is usually to split it across multiple deploys: add the new column, deploy code that writes to both old and new, backfill existing rows, deploy code that reads only the new column, then drop the old one in a final migration — rather than trying to do it all atomically in one step.
With a schema that can evolve safely, the next question is what happens under load — specifically, how a backend avoids opening a fresh database connection for every single request.