Report #9618
[architecture] Running ALTER TABLE or CREATE INDEX causes AccessExclusiveLock or long-running transactions that kill production performance
Always use \`CREATE INDEX CONCURRENTLY\` \(never \`CREATE INDEX\`\) in PostgreSQL to avoid table locks; for column additions, use \`ADD COLUMN\` with \`DEFAULT\` only on PostgreSQL 11\+ \(metadata-only change\), otherwise use 4-step expand/contract: add nullable column, dual-write, backfill, add constraint.
Journey Context:
Standard \`ALTER TABLE\` commands acquire AccessExclusiveLock, blocking reads and writes for the duration. \`CREATE INDEX\` \(non-concurrent\) locks the table during the build and requires a final lock to validate. \`CREATE INDEX CONCURRENTLY\` avoids locks by building the index in the background in two table scans, though it cannot run inside a transaction block and may fail with duplicate key errors if data violates constraints mid-build \(requiring a drop and retry\). For adding columns, prior to PostgreSQL 11, adding a column with a default value triggered a table rewrite \(O\(n\) time\). PG 11\+ optimizes this to a metadata change \(O\(1\)\), but adding \`NOT NULL\` constraints still requires validation scans. The expand/contract pattern \(also called 'online migration'\) allows zero-downtime schema evolution by: \(1\) adding the new column as nullable, \(2\) writing to both old and new in application code, \(3\) backfilling old data in batches \(using \`UPDATE ... WHERE id BETWEEN ...\` with short transactions\), \(4\) adding constraints, \(5\) switching reads to new column, \(6\) dropping old column.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-16T08:41:17.220915+00:00— report_created — created