Safe Schema Changes: Columns and Constraints
Learn to add columns, backfill data and enforce constraints in production PostgreSQL without taking the site down — from lock mechanics to NOT VALID patterns.
Every ALTER TABLE grabs a lock that blocks even SELECT. On a table with a million rows and real traffic, a migration that waits too long can turn into an outage — and a waiting DDL queues up everything behind it. This course shows why that happens and what to do about it. We work on a real stand: four tables, 200 k customers, 1 M orders, 800 k payments. We watch a DDL block in pg_locks, arm the migration with lock_timeout so it fails fast instead of hanging, and walk every column-change pattern the planner cares about — nullable add, constant default, volatile default, and batched backfill. Then we close with the two safest sequences every production engineer needs: adding NOT NULL through NOT VALID + VALIDATE, and wiring up a foreign key the same way. Every output in this course was captured on a live container.
What you'll build
- Explain why ALTER TABLE takes ACCESS EXCLUSIVE and what queues behind it
- Reproduce a blocked DDL and read it in pg_locks and pg_stat_activity
- Set lock_timeout and statement_timeout as a migration header pattern
- Add a nullable column and a column with a constant default safely
- Recognise a volatile default and explain the table-rewrite cost
- Backfill 200 k rows in batches without blocking concurrent traffic
- Add NOT NULL through CHECK NOT VALID + VALIDATE CONSTRAINT + SET NOT NULL
- Add a foreign key through NOT VALID + VALIDATE and understand the lock profile
Contents
- A database big enough to feel the locks
- Start it
- Connect with psql
- The customers table — without loyalty_tier
- The orders table — one million rows coming
- The payments table — foreign key added separately
- The subscriptions table
- Create the tables
- Seed the customers, repeatably
- Seed a million orders
- Seed the payments
- Seed the subscriptions
- Load the data
- Refresh statistics and confirm the stand
- pg_locks — the X-ray for concurrent access
- Simulated production traffic
- Hold a lock to block the DDL
- The DDL blocks and the queue forms
- Read the lock wait in pg_locks
- The lock releases and the DDL completes
- Clean up the test column
- pg_stat_activity — find the blocked statement
- Spot the blocked migration from pg_stat_activity
- The migration header pattern
- Add the nullable column
- A script to apply a numbered migration
- lock_timeout in action — fail fast
- Apply the real migration — no contention, instant
- Three ways to add a column — three different risks
- Constant default — catalog-only on PG 11+
- Volatile default — every row is rewritten
- Drop the test columns
- All loyalty_tier values are NULL
- A progress query to run while backfill runs
- Backfill skeleton — find the PK bounds
- The batch loop — short transactions, small pauses
- Run the backfill
- Progress snapshots during backfill
- Why a single UPDATE is an anti-pattern
- Four steps to add NOT NULL safely
- Step 1 — add the constraint without validating
- Apply step 1
- Inspect the constraint state
- Step 2 — validate existing rows with a milder lock
- Apply step 2 — traffic keeps flowing
- The lock profile of VALIDATE CONSTRAINT
- Steps 3 and 4 — promote and clean up
- Apply the final NOT NULL steps
- Safe FK: the same NOT VALID pattern
- Migration 0004 — add the FK without validating
- Apply the NOT VALID foreign key
- Confirm the constraint is not yet validated
- Step 2 — validate the foreign key
- Apply the validation
- Confirm the FK is fully validated
- The complete picture in the catalog