Creating an index on a large table can take minutes or even hours. For GIN, trigram, or multi-million row tables, it can feel like nothing is happening at all.
PostgreSQL provides built-in visibility into index creation progress. You don’t need external tools — just the right system view.
The pg_stat_progress_create_index view
PostgreSQL exposes index build progress via:
- PostgreSQL
The key view is:
SELECT *
FROM pg_stat_progress_create_index;
This shows all currently running CREATE INDEX operations.
If nothing is building, it returns zero rows.
Example output explained
A typical query looks like this:
SELECT
pid,
datname,
relid::regclass AS table_name,
index_relid::regclass AS index_name,
phase,
lockers_total,
lockers_done,
blocks_total,
blocks_done,
tuples_total,
tuples_done
FROM pg_stat_progress_create_index;
Important columns:
phase
Tells you what stage the build is in. Examples:
initializingbuilding indexwaiting for writers before validationindex validationwaiting for old snapshots
If you're using CREATE INDEX CONCURRENTLY, you’ll see additional validation phases.
blocks_total and blocks_done
These indicate:
- Total table blocks to scan
- How many have been processed
Progress percentage:
(blocks_done::float / NULLIF(blocks_total, 0)) * 100
This is usually the most reliable indicator during the scan phase.
tuples_total and tuples_done
Shows:
- Estimated total rows
- Rows processed so far
Useful, but block progress is often more stable.
Monitoring concurrent index builds
When using:
CREATE INDEX CONCURRENTLY ...
PostgreSQL performs multiple scans and validation steps.
You’ll see phases like:
building indexwaiting for writers before validationindex validationwaiting for old snapshots
If it appears stuck in:
waiting for old snapshots
That usually means a long-running transaction is preventing completion.
You can inspect active transactions:
SELECT pid, state, query, xact_start
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY xact_start;
Calculating progress percentage
A simple progress query:
SELECT
relid::regclass AS table_name,
index_relid::regclass AS index_name,
phase,
ROUND(
100.0 * blocks_done / NULLIF(blocks_total, 0),
2
) AS progress_percent
FROM pg_stat_progress_create_index;
This gives you a clean percentage during the main build phase.
When progress appears frozen
Common reasons:
- Very small
maintenance_work_mem - Disk I/O saturation
- WAL bottlenecks
- Waiting on long-running transactions (concurrent builds)
- CPU-heavy index types (e.g. GIN with trigrams)
If blocks aren’t increasing, check:
SELECT wait_event_type, wait_event
FROM pg_stat_activity
WHERE pid = <index_pid>;
This tells you whether it’s waiting on:
- Lock
- IO
- WAL
- Buffer pin
- etc.
Estimating total duration
A rough estimate during build:
SELECT
blocks_done,
blocks_total,
now() - backend_start AS elapsed
FROM pg_stat_progress_create_index
JOIN pg_stat_activity USING (pid);
You can extrapolate remaining time from block progress.
Version requirement
pg_stat_progress_create_index is available since:
- PostgreSQL 12
If you’re on an older version, you won’t have native progress tracking.
Practical workflow
When building a large index in production:
- Run
CREATE INDEX(orCONCURRENTLY) - Monitor
pg_stat_progress_create_index - Watch for blocking transactions
- Track I/O saturation
- Increase
maintenance_work_memif needed
This gives you visibility instead of guessing.
Final thoughts
Large index builds are expensive operations. Monitoring progress:
- Reduces uncertainty
- Helps detect blockers
- Allows time estimation
- Makes concurrent builds safer in production
If you regularly build large GIN or trigram indexes, having a monitoring query ready in your toolbox saves a lot of stress.
If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts, subscribe use the RSS feed.