← All scenarios

Scenario · Migrations & Releases

CREATE INDEX without CONCURRENTLY

A sandboxed PostgreSQL incident — investigate with your own tools, submit a fix, and get deterministic Detect / Fix / Trap scoring.

L3 · 10–15 min · runs locally in Docker

Launch

Start this scenario

Boot it in a real PostgreSQL sandbox and investigate with psql, EXPLAIN and pg_stat_statements.

ride postgres start stage-09/02-create-index-without-concurrently

Part of these paths

Show the postmortem & investigation hints spoilers
CREATE INDEX without CONCURRENTLY
Type: incident simulation · Topic: Migrations & Releases · Level: L3 · Duration: 10–15 min
Launch: ride postgres start stage-09/02-create-index-without-concurrently

POSTMORTEM (root cause · how it was found · the fix · lesson)
Root cause: a release migration built an index on the hot `orders` table with a
plain `CREATE INDEX` (no CONCURRENTLY). A non-concurrent build holds a SHARE lock
on the table for its entire duration, and SHARE conflicts with the ROW EXCLUSIVE
lock that writes need — so every INSERT/UPDATE blocked until the build finished
(here, the migration kept the transaction open, so it blocked indefinitely).

How it was found: pg_stat_activity showed a `migration_runner` session whose query
was `CREATE INDEX ...`, holding a lock that pg_blocking_pids() tied to the stalled
`app_writer`.

The fix: terminate the blocking build, then rebuild without blocking writes:
  CREATE INDEX CONCURRENTLY idx_orders_user_id_safe ON orders (user_id);
CONCURRENTLY builds the index without taking a write-blocking lock (and can't run
inside a transaction block).

Lesson: on a live table, always build indexes with CONCURRENTLY. A plain
CREATE INDEX in a migration is a release-time write outage. Pair it with a
`lock_timeout` so a migration fails fast instead of stalling production.

INVESTIGATION HINTS (the staged path to diagnose and fix)
1. Writes to orders started blocking during a release. pg_stat_activity shows a migration session whose query is a CREATE INDEX (no CONCURRENTLY), running inside a transaction and holding a lock.
2. A plain CREATE INDEX takes a SHARE lock for the whole build, which conflicts with the ROW EXCLUSIVE that writes need — on a live table that's a write outage. The safe form for a hot table is CREATE INDEX CONCURRENTLY.
3. End the migration's blocking build (application_name LIKE 'migration_runner%'), then rebuild the index the safe way: CREATE INDEX CONCURRENTLY ... (it can't run inside a transaction block).