Two dangerous shapes
CREATE INDEX idx_orders_customer_id
ON orders(customer_id);
A normal index build takes a SHARE lock and blocks writes for the duration of the build.
BEGIN;
CREATE INDEX CONCURRENTLY idx_orders_customer_id
ON orders(customer_id);
COMMIT;
A reviewable sequence
1. Separate the migration transaction
Disable the framework's automatic transaction for this migration and confirm no surrounding deployment wrapper starts another transaction.
2. Bound lock acquisition
SET lock_timeout = '2s';
SET statement_timeout = '0';
CREATE INDEX CONCURRENTLY idx_orders_customer_id
ON orders(customer_id);
The timeout values above are examples, not universal defaults. Select them from your workload, release window, and retry policy.
3. Observe progress and validity
SELECT *
FROM pg_stat_progress_create_index;
SELECT indexrelid::regclass, indisvalid, indisready
FROM pg_index
WHERE indexrelid = 'idx_orders_customer_id'::regclass;
4. Verify the benefit
Compare the intended query plan and latency before and after. Account for index size, write amplification, vacuum work, cache pressure, and overlap with existing indexes.
Failure and retry plan
A failed concurrent build can leave an invalid index. The runbook must identify it, decide whether to retry or remove it, and avoid creating duplicate indexes under new names.
DROP INDEX CONCURRENTLY IF EXISTS idx_orders_customer_id;
Evidence gate
- Framework transaction behavior is verified from the generated migration.
- Table size, write rate, long transactions, and available I/O headroom are measured.
- Existing equivalent or overlapping indexes are reviewed.
- Progress, invalid-index cleanup, retry, and cancellation procedures are written.
- The target query is benchmarked and index write cost is accepted.