Situation: You need to rebuild an index on a high-write OLTP Postgres cluster without impacting peak-hour latency or causing excessive write amplification.Plan (high-level):- Prefer non-blocking/concurrent operations or offload work to a replica.- Prepare by tuning maintenance params and monitoring WAL and I/O.- Build a replacement index concurrently, verify, then atomically swap names and drop the old index.- Have rollbacks and threshold-based aborts ready.Step-by-step operational runbook:1. Prep (hours/days before)- Increase maintenance_work_mem on the instance running the build (temporary).- Ensure autovacuum is healthy; check pg_stat_activity, I/O, replication lag.- Identify peak and safe windows; target low-write hours but not necessarily full maintenance window (e.g., 2–4am local).- Alerting: set metrics on p99 latency, write throughput, replication lag and WAL volume.2. Preferred approach — build on a standby replica then swap- Create the index on a replica (read-only) to avoid impacting primary: - Connect to replica and run:sql
CREATE INDEX CONCURRENTLY idx_mytable_col_new ON mytable (col);
- Wait for the replica to catch up. Once built, you must also create it on primary or do fast swap (see below). Creating on primary will re-run build; so best if you can take brief lock to swap names (atomic and cheap).- If using logical replication or PgBouncer routing, you can promote a replica after index created, or use a fast rename strategy (next).3. Direct primary approach (when replica route unavailable)- On primary, build replacement index concurrently with a temporary name:sql
CREATE INDEX CONCURRENTLY idx_mytable_col_new ON mytable (col);
- This avoids exclusive locks for writes but increases WAL and requires two scans; monitor WAL generation and disk.- Validate query plans: run EXPLAIN on critical queries to ensure the new index is used or equivalent.- Swap names quickly (fast, takes brief access exclusive lock only on the index object, not the table):sql
BEGIN;
ALTER INDEX idx_mytable_col RENAME TO idx_mytable_col_old;
ALTER INDEX idx_mytable_col_new RENAME TO idx_mytable_col;
COMMIT;
- Drop the old index concurrently:sql
DROP INDEX CONCURRENTLY idx_mytable_col_old;
Tuning & concurrent options:- Use CREATE INDEX CONCURRENTLY or REINDEX CONCURRENTLY (Postgres 12+) to avoid table locks.- Increase maintenance_work_mem to speed builds, but watch memory.- If WAL/disk I/O is the bottleneck, throttle by scheduling during lower I/O or build on replica.- If index includes expressions or requires table rewrite, concurrent option might not apply; plan for maintenance window.Rollback and failure handling:- If CREATE INDEX CONCURRENTLY fails: the failed index is cleaned up automatically; re-run after investigating cause.- If after swap the new index causes regressions, quickly rename back:sql
BEGIN;
ALTER INDEX idx_mytable_col RENAME TO idx_mytable_col_newbad;
ALTER INDEX idx_mytable_col_old RENAME TO idx_mytable_col;
COMMIT;
DROP INDEX CONCURRENTLY idx_mytable_col_newbad;
- If WAL or replication lag crosses thresholds during build, abort and drop new index, restore settings.- Always test the full procedure on staging with production-like load.Monitoring & metrics:- Track p99 write latency, replication lag, WAL volume, IOPS, index build progress in pg_stat_progress_create_index (Postgres 12+), and application error rates.- Use automated runbook scripts that check thresholds and send alerts or cancel builds.Trade-offs:- CONCURRENT builds avoid locks but double I/O/WAL cost; building on replica shifts cost off primary but adds operational complexity.- Renaming swap is minimal-impact; dropping old index concurrently avoids table locks.This approach minimizes client-facing downtime and write amplification by preferring replica/off-peak builds, using CONCURRENTLY, performing a fast atomic swap, and providing clear rollback steps and monitoring.