How to add a NOT NULL column safely in PostgreSQL
Quick answer: If you can give the column a constant default, just
ADD COLUMN ... NOT NULL DEFAULT <const>. It is metadata-only and instant since PostgreSQL 11. If you need a backfilled value with no fixed default, do it in steps: add the column nullable and backfill in batches, then, on PostgreSQL 18+.ADD CONSTRAINT ... NOT NULL <col> NOT VALID(instant) andVALIDATE CONSTRAINT(scans underSHARE UPDATE EXCLUSIVE, so reads and writes continue). On PostgreSQL 17 and earlier you need the longer sequence: a validatedCHECK (col IS NOT NULL), thenSET NOT NULL(which PostgreSQL 12+ does without re-scanning the table), then drop the check.
The easy case: a constant default
Section titled “The easy case: a constant default”If every existing row can share the same value, one statement does it:
ALTER TABLE orders ADD COLUMN status text NOT NULL DEFAULT 'active';Since PostgreSQL 11, this is metadata-only: the default is stored in the
catalog and applied to existing rows logically, so there is no table rewrite even
though the column is NOT NULL. It takes an ACCESS EXCLUSIVE lock only for the
instant it updates the catalog.
(On PostgreSQL 10 and earlier, this same statement rewrote the entire table under that lock, the classic migration-that-took-the-site-down.)
The careful case: NOT NULL with a backfill
Section titled “The careful case: NOT NULL with a backfill”When there is no single default, the value must be computed per row, or comes
from elsewhere, do not do it in one shot. Do it in steps: add the column, backfill
it, and then attach the constraint. On PostgreSQL 18+ attaching the constraint
is two commands and never scans under an exclusive lock; on 17 and earlier it is
a longer sequence built around a placeholder CHECK.
1. Add the column nullable (instant):
ALTER TABLE orders ADD COLUMN region text;2. Backfill in batches so no single statement runs as one long transaction
and floods the table with dead tuples. (An UPDATE takes only a ROW EXCLUSIVE
lock, it never blocks readers, so the hazard here is transaction length and
bloat, not locking):
UPDATE orders SET region = compute_region(id)WHERE region IS NULL AND id BETWEEN 1 AND 10000;-- repeat in ranges until doneBatching keeps each transaction short and lets autovacuum keep up (a single
table-wide UPDATE is a
bloat
hazard).
3. Add the NOT NULL constraint NOT VALID, on PostgreSQL 18+, not-null
constraints live in pg_constraint like any other constraint, so you can add one
unvalidated. No placeholder CHECK is needed:
{/* sql-check: pg 18+ */}
ALTER TABLE orders ADD CONSTRAINT orders_region_not_null NOT NULL region NOT VALID;This takes an ACCESS EXCLUSIVE lock only for the instant it updates the catalog. There is no scan. The column is enforced against new inserts and updates
immediately (pg_attribute.attnotnull flips to true right away); only
pre-existing rows go unchecked, and the constraint is marked
pg_constraint.convalidated = false until you validate it.
4. Validate it. This is the step that scans, and it runs under
SHARE UPDATE EXCLUSIVE, which allows reads and writes for the whole scan:
{/* sql-check: pg 18+ */}
ALTER TABLE orders VALIDATE CONSTRAINT orders_region_not_null;That is it. There is no SET NOT NULL step and nothing to drop afterwards.
On PostgreSQL 17 and earlier, not-null is only a column attribute, so you need
the older four-step sequence: add a placeholder CHECK (region IS NOT NULL) NOT VALID, VALIDATE it under SHARE UPDATE EXCLUSIVE, then SET NOT NULL, which on PostgreSQL 12+ skips the table scan because it trusts the validated
CHECK, but is not lock-free: it still takes an ACCESS EXCLUSIVE lock, so run
it behind a lock_timeout and retry rather than letting it queue behind a long
query and stall all traffic. Then drop the now-redundant check:
{/* sql-check: isolated */}
ALTER TABLE orders ADD CONSTRAINT orders_region_check CHECK (region IS NOT NULL) NOT VALID;ALTER TABLE orders VALIDATE CONSTRAINT orders_region_check;ALTER TABLE orders ALTER COLUMN region SET NOT NULL;ALTER TABLE orders DROP CONSTRAINT orders_region_check; -- now redundantOn PostgreSQL 11 and earlier, SET NOT NULL still does a full-table verification
scan under ACCESS EXCLUSIVE, so guard it with a lock_timeout and run it in a
low-traffic window.
What to avoid
Section titled “What to avoid”- A volatile default.
ADD COLUMN ... DEFAULT random()(or any volatile function) must differ per row, so it rewrites the whole table. The metadata-only path is only for constant defaults. - One giant backfill UPDATE. Not a locking problem (
UPDATEis onlyROW EXCLUSIVE). It is one very long transaction plus a huge dead-tuple burst that autovacuum cannot clean until it commits. Batch it.
Why does aI-generated migration code get this wrong?
Section titled “Why does aI-generated migration code get this wrong?”Your AI coding agent (Claude Code, Cursor) writes the obvious statement.ADD COLUMN ... NOT NULL with a per-row backfill in a single UPDATE, or a
volatile default, because it is the direct translation of the request. It cannot
see that orders has 200 million rows, or which PostgreSQL version you run
(which decides whether the fast paths even apply). Whether a migration is instant
or an outage is a function of data volume and version, not the SQL text.
How do I keep migrations safe?
Section titled “How do I keep migrations safe?”- Prefer the constant-default one-liner when you can (PG 11+).
- Otherwise on PG 18+: nullable → batch backfill →
NOT NULL ... NOT VALID→VALIDATE CONSTRAINT. - On PG 17 and earlier: nullable → batch backfill → validated CHECK →
SET NOT NULL→ drop the check. - Guard every
ACCESS EXCLUSIVEstep with a shortlock_timeout.
Is adding a NOT NULL column with a default slow?
Not since PostgreSQL 11, a constant default is metadata-only, no rewrite even withNOT NULL. On 10 and earlier it rewrote the table.How do I add a NOT NULL column without a fixed default?
Add it nullable and backfill in batches. On PostgreSQL 18+ that is followed by just two commands:ADD CONSTRAINT ... NOT NULL <col> NOT VALID, an instant catalog change that immediately enforces the constraint on new inserts and updates, thenVALIDATE CONSTRAINT, which scans the pre-existing rows underSHARE UPDATE EXCLUSIVEso reads and writes both continue. NoSET NOT NULL, and nothing to drop afterwards. On PostgreSQL 17 and earlier, add a validatedCHECK (col IS NOT NULL), thenSET NOT NULL, on 12+ the validated check lets it skip the scan, but it still takes a briefACCESS EXCLUSIVElock, so use alock_timeout, then drop the check.Why backfill in batches?
Not for locking, anUPDATEtakes onlyROW EXCLUSIVEand does not block readers. A single table-wideUPDATEis one long transaction with a dead-tuple burst that autovacuum cannot reclaim while it is open, so the table bloats. Batches keep transactions short and let vacuum keep up.Does a volatile default rewrite the table?
Yes, the metadata-only path is only for constant defaults. A volatile default likerandom()rewrites every row underACCESS EXCLUSIVE.