Skip to content
Production

Migrations that lock, and the ones that do not

Most of what makes a migration dangerous is which lock it takes and for how long. A few of them take a lock that stops every read on the table, and they look identical in the file.

12 min · runs against the events dataset

Adding a nullable column with no default is instant: it changes the catalog and touches no rows. Adding one with a volatile default rewrites the whole table. In a migration file the two lines are almost identical, and one of them takes an ACCESS EXCLUSIVE lock on your largest table at whatever hour the deploy runs.

  • ADD COLUMN nullable, no default — instant, catalog only.
  • ADD COLUMN with a constant default — instant in Postgres 11 and later, a full rewrite before it.
  • CREATE INDEX — blocks writes for its whole duration. CREATE INDEX CONCURRENTLY does not, and takes roughly twice as long.
  • ALTER COLUMN … TYPE — a full rewrite, and an ACCESS EXCLUSIVE lock for all of it.
  • ADD CONSTRAINT … CHECK — scans the whole table. NOT VALID then VALIDATE CONSTRAINT does it in two cheap steps.

Building an index over a table this size is not instant even here, on a few thousand rows in a browser tab. On a hundred million rows on a production disk it is minutes to hours — and for all of them, without CONCURRENTLY, nothing can write.

Every one of these is a row an ALTER … TYPE would have to rewrite, under a lock that stops reads.

rows_to_rewritebigint
8000

1 row · 8,000 examined · 91 pages read

The practical defence is a short lock_timeout on migrations and a retry. Failing fast and trying again in ten seconds is almost always better than waiting, because waiting is what turns one slow query into an outage.