IndexesPro · lab evidenceSource-grounded3 real lab transcripts

CREATE INDEX CONCURRENTLY: Building Indexes Online

An ordinary CREATE INDEX takes a SHARE lock on the table for its entire duration. That lock permits reads but blocks all writes — every INSERT, UPDATE, and DELETE waits until the index finishes building, which on a large table can be many minutes. CREATE INDEX CONCURRENTLY (CIC) exists to build the index without blocking writes.

Grounded in the source

Where this comes from in postgres/postgres

Each citation was checked against the REL_17_STABLE branch: real file path, symbol present, exact line number. Follow any link to read the code.

src/backend/commands/indexcmds.cVerified · REL_17_STABLE

Primary symbol: DefineIndex · line 540

The one thing to understand first

An ordinary CREATE INDEX takes a SHARE lock on the table for its entire duration. That lock permits reads but blocks all writes — every INSERT, UPDATE, and DELETE waits until the index finishes building, which on a large table can be many minutes. CREATE INDEX CONCURRENTLY (CIC) exists to build the index without blocking writes.

CONCURRENTLY trades wall-clock speed for availability: it scans the table twice and waits for old transactions to finish, but never blocks your writes — and if it fails it leaves an INVALID index you must clean up. Those two facts (the wait, and the INVALID-index aftermath) are where every CIC surprise comes from.

How CONCURRENTLY works

CIC trades speed for availability by doing two passes over the table with only a weak SHARE UPDATE EXCLUSIVE lock (which allows concurrent DML), defined in src/backend/commands/indexcmds.c:

  • Build pass. Record a starting snapshot, then scan the table and build the index from all rows visible to that snapshot. The index is marked not-yet-ready.
  • Wait. Wait for all transactions that could be using the old schema (that started before the index was registered) to finish.
  • Validation pass. Scan the table again to add any rows that changed during the build, then mark the index valid and usable.

Because it scans twice and waits for old transactions, CIC is slower in wall-clock time than a plain build — but it never blocks your application's writes.

The wait that surprises people

Step 2 means CIC can hang behind a single long-running transaction — even one unrelated to the table. If a reporting query or a forgotten idle in transaction session started before your CIC, CIC waits for it to end before it can finish. Always check for long transactions first:

Lab-verified
SQL
SELECT pid, now() - xact_start AS age, state, query
FROM   pg_stat_activity
WHERE  state <> 'idle'
ORDER  BY age DESC;
Real captured output for this query is in the Pro lab notebook below.

Handling failure: INVALID indexes

If CIC fails (a deadlock, a uniqueness violation discovered in the validation pass, cancellation), it leaves behind an INVALID index — present in the catalog but not used by queries and not maintained for new rows correctly. You must clean it up before retrying:

Lab-verified · corrected
SQL
-- Find invalid indexes
SELECT c.relname AS index, i.indisvalid
FROM   pg_index i
JOIN   pg_class c ON c.oid = i.indexrelid
WHERE  NOT i.indisvalid;

-- Drop it (also concurrently, to avoid blocking)
DROP INDEX CONCURRENTLY idx_orders_status;
-- Then re-run CREATE INDEX CONCURRENTLY.
Real captured output for this query is in the Pro lab notebook below.

Rules and constraints

  • CIC cannot run inside a transaction block — it manages its own snapshots. Migration tools must run it outside a transaction.
  • Only one CIC per table at a time interacts safely; serialise them.
  • To add a unique constraint online, build a unique index with CIC, then attach it: ALTER TABLE ... ADD CONSTRAINT ... UNIQUE USING INDEX ....

REINDEX CONCURRENTLY

Bloated indexes can be rebuilt online too. REINDEX INDEX CONCURRENTLY builds a fresh copy alongside the old one and swaps them, again under a weak lock. This is the standard remedy for index bloat without the blocking of a plain REINDEX:

Lab-verified
SQL
REINDEX INDEX CONCURRENTLY idx_orders_customer;
REINDEX TABLE CONCURRENTLY orders;   -- all indexes on the table
Real captured output for this query is in the Pro lab notebook below.

Layer 3 — Watch it happen on your own database

Lab-verified
SQL
-- 1. Before starting: any old transaction will stall CIC's wait phase
SELECT pid, now() - xact_start AS age, state, query
FROM   pg_stat_activity WHERE state <> 'idle' ORDER BY age DESC;

-- 2. Build it (must NOT be inside a transaction block)
CREATE INDEX CONCURRENTLY idx_orders_status ON orders (status);

-- 3. Confirm it actually succeeded before relying on it
SELECT c.relname AS index, i.indisvalid
FROM   pg_index i JOIN pg_class c ON c.oid = i.indexrelid
WHERE  NOT i.indisvalid;          -- any row here = a leftover INVALID index
Real captured output for this query is in the Pro lab notebook below.

The discipline is bookended by two checks. Before: a long-running or idle in transaction session — even one unrelated to this table — will make CIC's wait phase hang, so clear the runway first. After: query pg_index.indisvalid; if CIC failed it leaves an INVALID index that queries silently ignore. Never trust a CIC until you've confirmed indisvalid = true.

Layer 4 — The levers this hands you

The operational recipe, and the rules that constrain it:

  • Confirm no long transactions are open, then run CIC outside a transaction block (it manages its own snapshots — migration tools must not wrap it in BEGIN).
  • On failure, drop the INVALID index concurrently (DROP INDEX CONCURRENTLY) and retry — never leave it behind.
  • Serialise CIC per table — only one concurrent build at a time interacts safely.
  • Add unique constraints online by building a unique index with CIC, then ALTER TABLE ... ADD CONSTRAINT ... UNIQUE USING INDEX ....
  • Rebuild bloated indexes online with REINDEX INDEX CONCURRENTLY / REINDEX TABLE CONCURRENTLY — same weak-lock, build-and-swap mechanism.

Layer 5 — What an Oracle DBA should expect vs what they get

Oracle DBAs know online index builds well; the differences are in the failure model:

  • **CONCURRENTLY ≈ CREATE INDEX ... ONLINE.** Both build without blocking DML; both are slower than an offline build because they track concurrent changes.
  • The INVALID-index aftermath is sharper. A failed Oracle online build also leaves an unusable index, but Postgres's INVALID index is silently ignored by the planner — easy to miss without the indisvalid check.
  • Cannot run in a transaction block. Unlike most Postgres DDL (which is transactional), CIC cannot be wrapped in BEGIN/COMMIT — a real gotcha for migration frameworks that wrap everything in a transaction.
  • **No ALTER INDEX ... REBUILD ONLINE** — the equivalent is REINDEX ... CONCURRENTLY, and there is no global index to rebuild because Postgres indexes are always local.

Key takeaway

CREATE INDEX CONCURRENTLY builds under a weak SHARE UPDATE EXCLUSIVE lock via two table scans plus a wait for old transactions, so writes never block — at the cost of slower wall-clock time. Clear long transactions first, run it outside a transaction block, and always verify pg_index.indisvalid afterward; if it failed, drop the INVALID index concurrently and retry. REINDEX ... CONCURRENTLY applies the same mechanism to bloat.

Interview lens

How this shows up in interviews

What an interviewer is actually testing, the same mechanism seen from three roles, and the gap between a shallow answer and one that shows real depth.

What the interviewer is testing

Whether you know why an ordinary CREATE INDEX blocks writes and how CONCURRENTLY trades that for a slower, two-scan, non-blocking (but riskier) build.

Questions you should be able to answer

  • Why does a normal CREATE INDEX block writes, and how does CONCURRENTLY avoid it?
  • What are the downsides and failure modes of CREATE INDEX CONCURRENTLY?
  • What do you do when it leaves an INVALID index behind?

Database engineer

You use CIC on live tables, monitor for INVALID indexes, drop and rebuild failed ones, and remember CIC can't run inside a transaction block.

Software engineer

You expect CIC to take longer and to wait out old transactions, so you don't wrap it in a BEGIN/COMMIT migration.

Feature builder

You ship a feature's new index via CIC as a step separate from the app deploy.

Shallow answer

'CONCURRENTLY builds the index without locking.' Overstates it — CIC still takes a light lock and can fail to an INVALID index.

Answer that shows depth

A plain CREATE INDEX holds a SHARE lock for its entire duration — reads pass, writes block — an outage on a big table. CREATE INDEX CONCURRENTLY takes only a light lock and does two heap passes plus a wait: build from a snapshot, then a second pass to catch rows changed meanwhile, waiting for transactions that could still see the old state. The costs: it's slower, can't run inside a transaction block, needs two scans, and if it fails it leaves an INVALID index that still incurs write overhead and must be dropped and rebuilt.

Pro lab notebook

The captured run behind every query

Every SQL query above is real and the source citations are verified. Pro unlocks the captured psql output — each query re-run against a real PostgreSQL 17.10 lab, with its verbatim result.

Pro lab notebookTested, not just explained

3 real lab transcripts for this lesson

The explanation and the SQL above are free — so are the verified source citations. What Pro unlocks is the proof: every query in CREATE INDEX CONCURRENTLY: Building Indexes Online re-run against a real PostgreSQL 17.10 instance, with its verbatim psql output captured line for line.

What Pro unlocks here

  • Every query in this lesson re-run against a real PostgreSQL 17.10 lab
  • The verbatim psql output — page headers, row versions, plan nodes, catalog rows
  • The corrected form of any query the source lesson got wrong, re-captured
  • A copy-ready notebook you can replay yourself to confirm every claim
Unlock with Pro — $24.99/moor $199/yr — save ~$100

How this was verified

This explanation is anchored to PostgreSQL's source tree — the files linked above, on the REL_17_STABLE branch — and cross-checked against the official documentation for versions 15 through 18. Every SQL query shown was executed and its output captured in a throwaway PostgreSQL 17.10 lab; where a query needed correcting to run, that correction is noted inline.

Official documentation: CREATE INDEX (CONCURRENTLY)