awesome-everything RU
↑ Back to the climb

Databases

ADD COLUMN: instant in PG 11+ vs rewrite in older Postgres

Crux A constant default makes ADD COLUMN instant on any table size since PG 11; volatile defaults and NOT NULL without backfill still rewrite or block.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at junior altitude — the surface
◷ 10 min

A team deploys ALTER TABLE orders ADD COLUMN status TEXT NOT NULL DEFAULT 'pending' on a 50M-row table. On Postgres 10 the database freezes for 15 minutes. On Postgres 11+, the same statement finishes in milliseconds. Same SQL, completely different outcome.

How Postgres 11 made ADD COLUMN instant

Before PG 11, ALTER TABLE t ADD COLUMN c TEXT DEFAULT 'x' rewrote every row to materialise the default value. On a 100M-row table that meant minutes holding AccessExclusiveLock — every reader and writer blocked for the duration.

Since PG 11, when the default is non-volatile (a literal string, number, boolean, or stable expression), Postgres stores the default in pg_attribute system catalog:

  • pg_attribute.atthasmissing = true
  • pg_attribute.attmissingval = 'x'

Existing rows keep their original physical storage — the column is not written. When you SELECT an old row, Postgres reads attmissingval and synthesises the value at read time. New rows inserted after the migration store the column normally. The lock hold time drops from minutes to milliseconds.

OperationPG 10PG 11+
ADD COLUMN c TEXT DEFAULT ‘pending’Full table rewrite — minutes per 100M rowsInstant — default in pg_attribute metadata
ADD COLUMN c TEXT DEFAULT clock_timestamp()Full table rewriteFull table rewrite — volatile default, must materialise per row
ADD COLUMN c TEXT (no default)Instant — NULL for existing rowsInstant — NULL for existing rows
ADD COLUMN c TEXT NOT NULL (no default, rows exist)Fails — existing NULLs violate NOT NULLFails — existing NULLs violate NOT NULL

Volatile defaults still rewrite

DEFAULT clock_timestamp(), DEFAULT random(), DEFAULT gen_random_uuid() — any volatile function — cannot be stored as a single missing value because each row needs a different result. Postgres must materialise the default per-row, which means a full table rewrite under AccessExclusiveLock.

Fix: add the column without a default (instant), then backfill existing rows with an UPDATE in batches, then set the default for future inserts with ALTER TABLE t ALTER COLUMN c SET DEFAULT gen_random_uuid().

Adding NOT NULL safely on a large table

ADD COLUMN c TEXT NOT NULL DEFAULT 'x' is fine on PG 11+ for a constant default — instant, no rewrite. The danger is adding NOT NULL to an existing nullable column that may have NULLs:

-- WRONG on a large table: scans every row under AccessExclusiveLock
ALTER TABLE users ALTER COLUMN email_verified SET NOT NULL;

The safe multi-step pattern (covered in depth in lesson 04):

  1. Add the column without NOT NULL: instant.
  2. Backfill existing NULLs in small batches.
  3. Add CHECK (col IS NOT NULL) NOT VALID — enforces for new rows only, no table scan.
  4. VALIDATE CONSTRAINT — scans under a lighter lock that does not block writes.
  5. ALTER COLUMN SET NOT NULL — fast because the constraint already proves validity.
ADD COLUMN cost comparison
ADD COLUMN constant default, PG 11+
Instant (ms)
ADD COLUMN volatile default, any PG
Minutes per 100M rows
ADD COLUMN no default
Instant (NULL for existing rows)
Lock taken by ADD COLUMN
AccessExclusiveLock (brief or long)
Read-time cost for missing-default rows
Negligible (single pg_attribute lookup)
Cost disappears as rows are UPDATE-d
Passively, via normal traffic
Why this works

Why did Postgres wait until version 11 (2018) to make ADD COLUMN instant? The change required a new mechanism in the heap tuple reading code: every read of an old-format tuple has to check atthasmissing and inject the stored default. This made the read path slightly more complex and required careful testing. The payoff was enormous — it eliminated the most common cause of migration-induced downtime pre-2018.

Quiz

In PG 11+, which ADD COLUMN form is instant (no table rewrite)?

Quiz

What does PG 11 store in pg_attribute to make ADD COLUMN with a constant default instant?

Order the steps

Order the steps to safely add a NOT NULL column to a 100M-row production table:

  1. 1 ADD COLUMN with a constant DEFAULT (instant in PG 11+; NULL default if none needed)
  2. 2 Deploy app code that writes the new column on every INSERT and UPDATE
  3. 3 Backfill existing NULL rows in batches of 1k–10k with pg_sleep between batches
  4. 4 ADD CONSTRAINT email_verified_nn CHECK (col IS NOT NULL) NOT VALID (instant, no scan)
  5. 5 VALIDATE CONSTRAINT (scans under SHARE UPDATE EXCLUSIVE — does not block writes)
  6. 6 ALTER COLUMN SET NOT NULL (fast — constraint already proved validity)
  7. 7 Optionally DROP the CHECK constraint (it is redundant once NOT NULL is set)
Recall before you leave
  1. 01
    How does PG 11+ make ADD COLUMN with a constant default instant, and what is the read-time behaviour for old rows?
  2. 02
    Why do volatile defaults still cause a full table rewrite even on PG 11+?
  3. 03
    What is the multi-step pattern to add a NOT NULL constraint to a nullable column on a large table?
Recap

Since PG 11, ADD COLUMN with a non-volatile constant default stores the value in pg_attribute.attmissingval and returns it synthetically at read time — no rows are touched, the operation completes in milliseconds on any table size. Volatile defaults (functions like clock_timestamp()) still require per-row materialisation and cause a full rewrite. Adding NOT NULL to an existing nullable column without a backfill scan is never safe in a single ALTER COLUMN SET NOT NULL on a large table; the correct path splits the work into an instant column add, a batched backfill, a NOT VALID constraint, a VALIDATE CONSTRAINT under a light lock, and finally a fast SET NOT NULL.

Connected lessons
appears again in258
Continue the climb ↑The lock-queue failure mode: why instant DDL can freeze the database
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.