ALTER TABLE users ADD COLUMN email_verified boolean NOT NULL DEFAULT false; on a table with 40 million rows takes a lock. In Postgres 11+ adding a column with a non-volatile default is metadata-only, so that particular statement is safe — but ALTER COLUMN ... TYPE, adding a constraint, or backfilling a new index will still queue behind every open transaction and block all writes behind it. One long-running analytics query, and your “quick migration” becomes a five-minute outage.
The industry answer is expand and contract: deploy the new schema shape first, run both shapes side by side, and remove the old one only after every application instance speaks the new dialect. Three open-source tools automate that pattern for Postgres, and they automate it very differently.
TL;DR: Quick Verdict
Use pgroll if you want the most explicit, versioned two-phase workflow with rollback and a schema-version search_path for gradual rollout — it is the most complete implementation of expand/contract and the best fit for teams running multiple app versions simultaneously. Use Reshape if you prefer migrations declared in YAML and want the tool to generate the views and triggers for you, with a straightforward start/complete/abort lifecycle. Use graphile-migrate if you already live in the Node/graphile ecosystem and want idempotent SQL files with a shadow database that validates migrations in CI before they touch production. None of them removes the need to understand locks — they remove the tedium.
Side-by-Side Comparison
| Dimension | pgroll | Reshape | graphile-migrate |
|---|---|---|---|
| GitHub stars | 6,607 | 1,857 | 836 |
| Last commit | 2026-09-21 | 2026-09-26 | 2026-10-15 |
| Language | Go (single binary) | Go (binary + Docker image) | TypeScript (npm CLI) |
| Migration format | JSON migration files | YAML migration files | SQL files (current.sql, committed batches) |
| Core mechanism | Per-version schemas + views + triggers | Views + triggers, same idea | SQL + shadow DB validation, no dual schema |
| Rollback of a started migration | pgroll rollback | reshape migration abort | Roll forward only (by design) |
| Coexisting app versions | Yes — search_path per version | Yes — reshape schema-query connection string | No — assumes single deployed version |
| Validation before prod | Migration validation on start | Dry-run via abort/start loop | Shadow database replay in CI |
| License | Apache-2.0 | Apache-2.0 | MIT |
Star counts and last-commit dates come from the GitHub API at publish time.
Decision Matrix: Pick in 10 Seconds
| Your situation | Recommended tool | Why |
|---|---|---|
| Rolling deploys, several app versions live at once | pgroll | Versioned schemas let old and new code read the same tables |
| You want migrations readable by non-DBAs | Reshape | YAML operations, reshape docs output, explicit lifecycle |
| Node/Graphile stack, migrations reviewed in PRs | graphile-migrate | current.sql is idempotent SQL, shadow DB catches errors in CI |
| You need an instant abort after a bad deploy | pgroll or Reshape | Both can roll back a started migration without data loss |
| Small team, single app version, boring tables | graphile-migrate | Least machinery; no views or triggers to reason about |
| Very large tables where trigger overhead hurts writes | graphile-migrate | No dual-write triggers in the hot path |
| Compliance requires a documented migration audit trail | pgroll | Migration files are versioned artifacts with explicit completion |
pgroll: Schema Versions You Can Switch Between
pgroll’s insight is that a migration creates a new schema version rather than mutating the existing one. Under the hood it keeps the physical table, adds views per version, and uses triggers to route writes to the right columns. Your application chooses a version with search_path.
Real CLI flow from the project README:
| |
A migration file describes operations declaratively:
| |
The operational catch is connection routing. Every client must set search_path — which means your connection pooler, ORM, and background workers all need to agree on the version. Forget a worker and it writes to the old view while the new one reports stale data. Set the version in one place (a single env var injected into every consumer) or accept that debugging will be painful.
Reshape: YAML Migrations With an Abort Button
Reshape implements the same dual-schema trick but with a YAML migration file and a three-verb lifecycle: start, complete, abort. From the README:
| |
The lifecycle is documented precisely: start sets up views and triggers so both the old and new schema are usable at once; complete removes the old schema, intermediate data, and triggers; abort rolls back everything start did without losing data.
For applications in supported languages, Reshape provides helper libraries so the connection automatically gets the right schema. For everything else, generate the query yourself:
| |
| |
Development loop — the part teams get wrong:
| |
Running complete during development is a trap: it drops the intermediate structures you need to keep iterating, and the next start begins from a different baseline.
graphile-migrate: Idempotent SQL Plus a Shadow Database
graphile-migrate takes the least magical approach: you write SQL, and it validates that SQL against a shadow database before it ever reaches production. Its README describes the model directly — in development it uses two databases (main and shadow), while in production most users only run graphile-migrate migrate, which operates solely on the main database.
The workflow revolves around current.sql, which must be idempotent — safe to run repeatedly:
| |
A typical current.sql that stays idempotent and does not lock your table:
| |
Because there is no dual schema, graphile-migrate does not let two application versions disagree about column semantics. That is a feature and a constraint: you must keep migrations backward compatible by hand — add columns as nullable, deploy code, backfill, then add the NOT NULL constraint in a later migration.
Backfilling Without an Outage
Every tool above handles schema shape. None of them can save you from a naive backfill. The pattern that works on large tables:
| |
Two things make this safe: limit bounds the lock footprint, and for update skip locked avoids waiting on rows another worker already grabbed. Sleep briefly between batches, watch replica lag, and stop the loop if lag crosses your threshold.
Then, and only then, tighten the schema:
| |
Add the constraint with NOT VALID first if you want to avoid a long validation lock, then VALIDATE CONSTRAINT separately — validation takes a weaker lock that does not block writes.
Pitfalls That Take Production Down
Long transactions block DDL. Every ALTER TABLE waits for a lock behind open transactions. Set lock_timeout (e.g. SET lock_timeout = '3s') so a migration fails fast instead of queueing every write behind it.
Trigger overhead is real. pgroll and Reshape install triggers to keep dual schemas consistent. On write-heavy tables that is measurable overhead and it applies to every insert and update until complete runs. Do not leave a migration half-completed for a week.
Forgetting one consumer of the schema. A worker, cron job, or BI tool that still uses the old search_path will quietly write to the wrong view. Grep your entire deployment for connection strings before starting a pgroll migration.
Running complete before the old version is gone. Both pgroll and Reshape drop the old schema on completion. If a single instance is still running the previous release, it will start throwing “column does not exist” errors.
Non-idempotent current.sql in graphile-migrate. watch re-runs the file on every save. Without if not exists clauses you will get duplicate-object errors that look like tool bugs.
Index creation without CONCURRENTLY. CREATE INDEX takes a SHARE lock that blocks writes for the duration of the build. On a large table that is minutes. CONCURRENTLY is slower overall but does not block.
No rehearsal. Run the exact migration against a production-sized copy. Our database dev/test environment comparison covers how to get a realistic clone cheaply.
Assuming MySQL lessons transfer. The MySQL world solves this with gh-ost and pt-online-schema-change, which copy tables and swap them. Postgres has no equivalent table-swap toolchain, so the expand/contract approach is not optional — see our MySQL zero-downtime migration comparison for how the other side does it.
For plain versioned migrations where zero downtime is not a requirement, our schema management tool comparison covers Atlas, sqldef, and Skeema — simpler tools that are, for many teams, entirely sufficient.
FAQ
Does pgroll work with an existing production database?
Yes. pgroll init creates its metadata schema and pgroll baseline imports your current structure as version zero, after which new migrations are applied on top. The baseline step is what people miss when they try pgroll on a legacy database.
Which tool has the least runtime overhead? graphile-migrate, because it does not create parallel schemas or triggers. pgroll and Reshape both install triggers to keep old and new shapes consistent, which costs write throughput until the migration completes — acceptable for most workloads, but worth measuring on hot tables.
Can I use these with Django/Rails/Prisma migrations? You can, but you are mixing two philosophies. Framework migrations usually assume the schema changes atomically with the deploy. If you adopt pgroll or Reshape, let it own schema DDL and use the framework only for application code, otherwise two tools will fight over the same columns.
What happens if a deploy fails halfway through?
With pgroll, pgroll rollback removes the new version. With Reshape, reshape migration abort undoes everything start created without data loss. With graphile-migrate, you roll forward with a compensating migration — it deliberately does not support down migrations.
Is ALTER TABLE ADD COLUMN with a default still safe on Postgres 16+?
Yes for non-volatile defaults — Postgres 11 and later store the default in the catalog instead of rewriting the table. It stops being free when the default is volatile (e.g. now()), when the column is added with a constraint that requires validation, or when you change a column type.
Do I need search_path management if I only run one app version?
Not for correctness, but pgroll still requires clients to select a version schema while a migration is in progress. If managing that is unacceptable, Reshape’s schema-query approach or graphile-migrate’s single-schema model will be less friction.
💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com