ADD COLUMN + NOT NULL
Adding a NOT NULL column with a default is one of the most common production incidents in Postgres. Here is the correct expand/contract pattern.
The Problem
sql
-- ACCESS EXCLUSIVE lock for the full duration of a table rewrite
ALTER TABLE users ADD COLUMN email_verified BOOLEAN NOT NULL DEFAULT false; Safe Rewrite (3-Step)
Step 1: Add nullable column (instant)
sql
SET lock_timeout = '2s';
ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verified BOOLEAN; Step 2: Backfill out-of-band (not in a migration)
Run this as a background job, not inside a migration. Use the CTE pattern to avoid UPDATE … LIMIT, which PostgreSQL does not support:
sql
-- Repeat until 0 rows updated
WITH batch AS (
SELECT id FROM users
WHERE email_verified IS NULL
ORDER BY id
LIMIT 1000
FOR UPDATE SKIP LOCKED
)
UPDATE users u
SET email_verified = false
FROM batch
WHERE u.id = batch.id; Step 3: Add NOT NULL constraint (non-blocking scan)
sql
SET lock_timeout = '2s';
-- NOT VALID skips the full table scan (brief ACCESS EXCLUSIVE for metadata only)
ALTER TABLE users
ADD CONSTRAINT chk_email_verified_nn
CHECK (email_verified IS NOT NULL) NOT VALID;
-- VALIDATE takes SHARE UPDATE EXCLUSIVE, does not block reads or writes
ALTER TABLE users VALIDATE CONSTRAINT chk_email_verified_nn;
-- Now safe to set NOT NULL (Postgres skips the scan when constraint exists)
ALTER TABLE users ALTER COLUMN email_verified SET NOT NULL;
-- Drop the helper constraint
ALTER TABLE users DROP CONSTRAINT chk_email_verified_nn;