DDL Locking: Why ALTER TABLE Can Freeze Your App
Most forms of ALTER TABLE take an ACCESS EXCLUSIVE lock — the strongest table lock, conflicting with every other lock mode including the ACCESS SHARE that a plain SELECT takes. While held, no session can even read the table. On a busy table this turns a "quick schema change" into a full outage.
The one thing to understand first
Most forms of ALTER TABLE take an ACCESS EXCLUSIVE lock — the strongest table lock, conflicting with every other lock mode including the ACCESS SHARE that a plain SELECT takes. While held, no session can even read the table. On a busy table this turns a "quick schema change" into a full outage.
The damage isn't the lock you take — it's the queue that forms behind a DDL statement that is itself waiting. A single slow query in front of your ALTER can freeze every new read of the table, so the whole art of safe DDL is never letting a strong-lock request block.
The hidden danger: the lock queue
The worst part is not the lock itself but the queue. PostgreSQL grants lock requests roughly in order. When your ALTER requests ACCESS EXCLUSIVE, it must wait for existing readers to finish — and meanwhile every new query queues behind your waiting ALTER, even simple SELECTs that would not normally conflict with each other. One long-running query in front of your ALTER can therefore stall the entire application:
-- Session A: a slow report holds ACCESS SHARE for 30s
-- Session B: ALTER TABLE ... (waits for A, holding the queue)
-- Session C..Z: every new SELECT now waits behind BWhat is cheap vs expensive
Not all DDL is equal. Operations that only change catalog metadata are fast; those that rewrite the table or validate data are slow:
- Cheap (metadata only):
ADD COLUMNwith a non-volatile default (modern PG stores it as metadata),DROP COLUMN(marks it dead), renaming, adding aNOT VALIDconstraint. - Expensive (table rewrite): changing a column type that requires conversion,
SET DEFAULTwith a volatile expression on old versions, adding a column with a volatile default. - Expensive (full scan): adding a
CHECKorFOREIGN KEYnormally validates every existing row under a strong lock.
Layer 3 — Watch it happen on your own database
-- Before any DDL: are there long transactions that would block
-- (and then be blocked behind by) your ALTER?
SELECT pid, now() - xact_start AS age, state, query
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY age DESC;
-- Reproduce the queue poisoning in two psql sessions:
-- A: BEGIN; SELECT * FROM orders; -- holds ACCESS SHARE
-- B: ALTER TABLE orders ADD COLUMN notes text; -- waits for A
-- C: SELECT count(*) FROM orders; -- now stuck behind B!Run the inspection query first — any old transaction is a landmine. Then reproduce the effect: open the slow reader (A), fire the ALTER (B) and watch it wait, then issue an ordinary SELECT (C) and watch it block too. That third session proves the headline danger: a waiting strong-lock request blocks the entire queue behind it, including reads that don't conflict with each other.
Layer 4 — The levers this hands you
The universal safety valve is to bound how long DDL will wait, so a blocked ALTER fails fast instead of building a queue:
SET lock_timeout = '2s';
ALTER TABLE orders ADD COLUMN notes text;
-- If the lock can't be acquired in 2s, the statement errors out
-- and the application keeps running. Retry later.- **
lock_timeout** — the single most important DDL safety setting; pair it with a shortstatement_timeoutfor any rewrite step. Try, fail fast, retry in a quieter window. - Prefer cheap (metadata-only) DDL:
ADD COLUMNwith a non-volatile default,DROP COLUMN, renames, and adding constraints asNOT VALID— all avoid a table rewrite/scan. - Defer the expensive half. Split type changes and constraint validation into a fast metadata step plus a separate lock-light validation step (next articles in this pathway).
- Clear the runway. Run DDL when long transactions are absent; check
pg_stat_activityand wait out or terminate blockers first.
Layer 5 — What an Oracle DBA should expect vs what they get
DDL locking surprises Oracle DBAs in both directions:
- No automatic online DDL. Oracle offers
ALTER TABLE ... ONLINE,DBMS_REDEFINITION, and edition-based redefinition; PostgreSQL gives youlock_timeout+ expand/contract patterns instead, and you assemble online DDL by hand. - The queue-poisoning behaviour is sharper. A waiting ACCESS EXCLUSIVE request blocking all subsequent reads is more aggressive than Oracle's typical DDL behaviour — this catches Oracle migrants off guard constantly.
- **
ADD COLUMNwith default is cheap on both modern engines** — Postgres (11+) stores a constant default as catalog metadata, similar to Oracle 11g+'s fast add-column. - **
NOT VALID≈ENABLE NOVALIDATE.** Adding a constraint without scanning existing rows, then validating separately, mirrors Oracle's novalidate-then-validate flow.
Key takeaway
Most ALTER TABLE takes ACCESS EXCLUSIVE, which conflicts with reads; worse, a DDL statement waiting for that lock blocks every query that queues behind it. Inspect pg_stat_activity for long transactions first, always set a low lock_timeout so a blocked ALTER fails fast, prefer metadata-only DDL, and split expensive changes into cheap-plus-validate steps. This is the foundation for every zero-downtime migration technique that follows.
Interview lens
How this shows up in interviews
What an interviewer is actually testing, the same mechanism seen from three roles, and the gap between a shallow answer and one that shows real depth.
What the interviewer is testing
Whether you know which ALTER TABLE forms take ACCESS EXCLUSIVE and how a short lock stuck behind a long queue turns into an outage.
Questions you should be able to answer
- ▸Why can a 'quick' ALTER TABLE freeze reads on a busy table?
- ▸Which ALTER forms are metadata-only vs table-rewriting in modern PostgreSQL?
- ▸How does lock_timeout plus retry make DDL safe?
Database engineer
You wrap DDL in a short lock_timeout with retry, and you know ADD COLUMN with a constant default is metadata-only since PG11 while a type change rewrites.
Software engineer
You realize an ALTER waiting behind one long transaction then blocks every SELECT queued behind it, not just writers.
Feature builder
You split schema changes into individually safe steps and schedule the risky ones.
Shallow answer
'ALTER TABLE locks the table while it runs.' True but misses the lock-queue amplification and which forms are cheap.
Answer that shows depth
Most ALTER TABLE forms take ACCESS EXCLUSIVE, which conflicts with the ACCESS SHARE a plain SELECT needs, so while held nobody can even read. Worse, if the ALTER must wait behind a long-running transaction it sits at the head of the lock queue and every subsequent SELECT queues behind it — turning a millisecond change into an outage. Modern PostgreSQL makes many forms cheap (ADD COLUMN with a constant default; adding a constraint NOT VALID then VALIDATE) as metadata-only, while type changes rewrite the table. The safe pattern is a short lock_timeout with retry so blocked DDL fails fast instead of freezing the app.
Pro lab notebook
The captured run behind every query
Every SQL query above is real and the source citations are verified. Pro unlocks the captured psql output — each query re-run against a real PostgreSQL 17.10 lab, with its verbatim result.
1 real lab transcript for this lesson
The explanation and the SQL above are free — so are the verified source citations. What Pro unlocks is the proof: every query in DDL Locking: Why ALTER TABLE Can Freeze Your App re-run against a real PostgreSQL 17.10 instance, with its verbatim psql output captured line for line.
What Pro unlocks here
- Every query in this lesson re-run against a real PostgreSQL 17.10 lab
- The verbatim psql output — page headers, row versions, plan nodes, catalog rows
- The corrected form of any query the source lesson got wrong, re-captured
- A copy-ready notebook you can replay yourself to confirm every claim
How this was verified
This explanation is anchored to PostgreSQL's source tree — the files linked above, on the REL_17_STABLE branch — and cross-checked against the official documentation for versions 15 through 18. Every SQL query shown was executed and its output captured in a throwaway PostgreSQL 17.10 lab; where a query needed correcting to run, that correction is noted inline.
Official documentation: ALTER TABLE (locking)