All rules Rule BV011 · LK
PostgreSQL
warning
LK · Locks & blocking
free in the CLI

Does SET NOT NULL scan the table under an exclusive lock?

SET NOT NULL scanning the table under ACCESS EXCLUSIVE

Warning: this works, but blocks traffic or rewrites data at scale.

What happens

SET NOT NULL must verify every existing row, and it does so while holding ACCESS EXCLUSIVE, the table is fully unavailable for the scan. The safe path is a CHECK (col IS NOT NULL) NOT VALID constraint, VALIDATE CONSTRAINT (which does not block), then SET NOT NULL, which sees the validated constraint and skips the scan.

Why it is dangerous on a populated table

It completes, but the time it holds its lock or rewrites rows scales with the table, and on a busy one that is long enough for traffic to queue.

Measured

Both forms ran against the same table (20M rows, 1.7 GB) on Postgres 18.6, with one session inserting a row and another reading one row every 50 ms. Worst wait is the slowest of those probe calls while the statement ran. This run is from Sep 16, 2026; the whole benchmark has every scenario and how to run it.

StatementTimeWorst write waitWorst read waitLock
FiresSET NOT NULL (full scan under ACCESS EXCLUSIVE)1.0 s954 ms954 msAccessExclusiveLock
SafeCHECK ... IS NOT NULL NOT VALID, VALIDATE, then SET NOT NULL691 ms1 ms1 msShareUpdateExclusiveLock

Fires on

ALTER TABLE orders ALTER COLUMN region SET NOT NULL;

The safe pattern

Add CHECK (col IS NOT NULL) NOT VALID (instant), VALIDATE it in a later migration (non-blocking scan), then SET NOT NULL, on Postgres 12+ it sees the validated constraint and skips its own scan, making the ACCESS EXCLUSIVE window a metadata flip. Drop the helper constraint afterwards.

SET lock_timeout = '5s';
ALTER TABLE orders ADD CONSTRAINT orders_region_nn
  CHECK (region IS NOT NULL) NOT VALID;
-- later migrations:
--    ALTER TABLE orders VALIDATE CONSTRAINT orders_region_nn;  -- non-blocking
--    ALTER TABLE orders ALTER COLUMN region SET NOT NULL;      -- scan skipped (PG12+)
--    ALTER TABLE orders DROP CONSTRAINT orders_region_nn;

Fixtures

The rule ships with these files and the test suite runs them on every change: the first set must fire, the second must stay silent.

Fires (1)

set not null
ALTER TABLE orders ALTER COLUMN region SET NOT NULL;

Stays silent (1)

on new table
CREATE TABLE audit_log (id bigint, entry text);
ALTER TABLE audit_log ALTER COLUMN entry SET NOT NULL;

How to check locally

Catch this before it ships

This rule runs locally in the free CLI, or with the full corpus through the hosted service. No install, nothing leaves your machine:

npx bolvrk check migration.sql