Unique Constraints
ADD CONSTRAINT ... UNIQUE takes ACCESS EXCLUSIVE and does a full table scan. Use a concurrent index instead.
The Problem
sql
-- ACCESS EXCLUSIVE lock, full table scan
ALTER TABLE users ADD CONSTRAINT uq_users_email UNIQUE (email); Safe Rewrite
Run these as two separate migrations. CREATE INDEX CONCURRENTLY cannot run inside a transaction, so it must ship in its own migration with the ORM transaction wrapper disabled.
Step 1: Build the index concurrently (separate migration, no transaction)
sql
SET lock_timeout = '2s';
-- Non-blocking, allows reads + writes during the build.
-- This statement must run outside a transaction block.
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS uq_users_email_idx ON users(email); Step 2: Attach the constraint (separate migration)
sql
SET lock_timeout = '2s';
-- Brief ACCESS EXCLUSIVE lock, no table scan because the index already exists.
ALTER TABLE users ADD CONSTRAINT uq_users_email UNIQUE USING INDEX uq_users_email_idx; Important: If your ORM auto-wraps every migration in
BEGIN/COMMIT, disable the wrapper for Step 1. pgfence flags CREATE INDEX CONCURRENTLY inside a transaction as an error.