How to Add a NOT NULL Column With No Downtime in PostgreSQL

A single ALTER TABLE ADD COLUMN NOT NULL DEFAULT ... rewrites every row in one transaction - seconds of table lock on a large table, writes blocked the whole time. The fix is a three-step pattern: add nullable first, backfill in batches, then enforce NOT NULL. Here's how to implement it.

Backend Engineerpythonpostgresqlalembic

Why the naive migration causes downtime

On PostgreSQL 10 and earlier, adding a column with a DEFAULT rewrites every row to store the new value, holding an AccessExclusiveLock for the entire duration. A 100,000-row table may lock for several seconds; a 100M-row table can lock for minutes - every write blocks until it finishes.

PostgreSQL 11+ made ADD COLUMN ... DEFAULT ... metadata-only for non-volatile defaults, so the default is recorded in the catalog rather than written to each row. But if you immediately add NOT NULL, Postgres must still scan every row to validate the constraint, which brings the lock back.

The safe pattern splits the work into three short, independently committable steps.

Step A - add the column as NULLABLE with a default

ALTER TABLE orders ADD COLUMN customer_segment TEXT DEFAULT 'standard';

On Postgres 11+ this is a pure catalog update - no row rewrite, no long lock. New rows get the default automatically. Existing rows are still NULL.

Step B - backfill existing rows in batches

Loop until no NULLs remain, committing each batch independently so no single transaction holds a long lock:

import psycopg2

conn = psycopg2.connect("postgres://postgres@localhost:5432/app")
conn.autocommit = True   # each batch is its own transaction
BATCH_SIZE = 5000

while True:
    with conn.cursor() as cur:
        cur.execute(
            """
            WITH batch AS (
              SELECT id FROM orders
              WHERE customer_segment IS NULL
              LIMIT %s
              FOR UPDATE SKIP LOCKED
            )
            UPDATE orders SET customer_segment = 'standard'
            FROM batch WHERE orders.id = batch.id
            """,
            (BATCH_SIZE,),
        )
        if cur.rowcount == 0:
            break
    print(f"backfilled {BATCH_SIZE} rows...")

Postgres does not support LIMIT directly in UPDATE, so the CTE pattern is the standard approach. FOR UPDATE SKIP LOCKED avoids blocking concurrent writers.

Step C - enforce NOT NULL and drop the default

Once every row has a value, the constraint check is a fast metadata scan, not a full rewrite:

ALTER TABLE orders ALTER COLUMN customer_segment SET NOT NULL;
ALTER TABLE orders ALTER COLUMN customer_segment DROP DEFAULT;

This does require a brief AccessExclusiveLock, but it is a constraint validation plus metadata change - not a row rewrite - so it completes in milliseconds.

Idempotency

Make each step safe to re-run by checking state before acting:

Idempotency means the script can be interrupted and restarted at any point without corrupting the table.

Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.

Try it in a workspace →

What you'll practice

FAQ

Why does ALTER TABLE ADD COLUMN NOT NULL lock my table?

Adding a NOT NULL constraint forces PostgreSQL to scan every existing row to confirm none are NULL. On large tables that full-table scan holds an AccessExclusiveLock for its entire duration, blocking all writes.

How do I add a NOT NULL column in PostgreSQL without locking the table?

Use the three-step pattern: first ADD COLUMN as nullable with a DEFAULT (metadata-only on Postgres 11+), then backfill existing NULL rows in small batches each committed separately, then SET NOT NULL once no NULLs remain.

How do I UPDATE in batches in PostgreSQL?

Use a CTE to SELECT a limited set of rows and JOIN the UPDATE to it - for example: WITH batch AS (SELECT id FROM orders WHERE col IS NULL LIMIT 5000) UPDATE orders SET col = val FROM batch WHERE orders.id = batch.id. Postgres does not support LIMIT directly in UPDATE.

Keep learning

Return the Correct HTTP Status CodeBackend projectRepair Broken API PaginationBackend projectAdd CORS Middleware to an Express APIBackend projectBackend roadmapStep by step to hiredBackend interview questionsSTAR answersAll Backend projectsProjects hub

Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.

Open the workspace →