Index metrics
Index usage and health metrics from pg_stat_user_indexes and related views.
Data sources
| View | Description |
|---|---|
pg_stat_user_indexes | Index usage statistics |
pg_statio_user_indexes | Index I/O statistics |
pg_indexes | Index definitions |
pg_class | Index sizes |
Core metrics
Usage metrics
| Metric | Type | Description |
|---|---|---|
pg_stat_user_indexes_idx_scan_total | Counter | Index scans initiated |
pg_stat_user_indexes_idx_tup_read_total | Counter | Index entries read |
pg_stat_user_indexes_idx_tup_fetch_total | Counter | Table rows fetched via index |
Size metrics
| Metric | Type | Description |
|---|---|---|
pg_index_size_bytes | Gauge | Index size in bytes |
I/O metrics
| Metric | Type | Description |
|---|---|---|
pg_statio_user_indexes_idx_blks_read_total | Counter | Index blocks read from disk |
pg_statio_user_indexes_idx_blks_hit_total | Counter | Index blocks hit in buffer cache |
Labels
| Label | Description | Example |
|---|---|---|
indexrelname | Index name | users_email_idx |
relname | Parent table name | users |
schemaname | Schema name | public |
datname | Database name | myapp |
cluster_name | Cluster identifier | production |
node_name | Node identifier | primary |
Common queries
Unused indexes
pg_stat_user_indexes_idx_scan_total == 0
Index scan rate
rate(pg_stat_user_indexes_idx_scan_total[5m])
Index buffer hit ratio
rate(pg_statio_user_indexes_idx_blks_hit_total[5m])
/
(
rate(pg_statio_user_indexes_idx_blks_hit_total[5m])
+
rate(pg_statio_user_indexes_idx_blks_read_total[5m])
)
Largest indexes
topk(10, pg_index_size_bytes)
Index read efficiency
Ratio of tuples fetched vs tuples read from index:
rate(pg_stat_user_indexes_idx_tup_fetch_total[5m])
/
rate(pg_stat_user_indexes_idx_tup_read_total[5m])
Most active indexes
topk(10, rate(pg_stat_user_indexes_idx_scan_total[5m]))
Dashboard usage
These metrics are used in:
- 10. Index health — Index overview
- 11. Single index — Index deep-dive
Index analysis queries
Find unused indexes
select
schemaname,
relname as table_name,
indexrelname as index_name,
pg_size_pretty(pg_relation_size(indexrelid)) as index_size,
idx_scan as scan_count
from pg_stat_user_indexes
where idx_scan = 0
and indexrelname not like '%_pkey'
order by pg_relation_size(indexrelid) desc;
Find duplicate indexes
select
indrelid::regclass as table_name,
array_agg(indexrelid::regclass) as duplicate_indexes
from pg_index
group by indrelid, indkey
having count(*) > 1;
Index usage per table
select
relname as table_name,
indexrelname as index_name,
idx_scan,
idx_tup_read,
idx_tup_fetch,
pg_size_pretty(pg_relation_size(indexrelid)) as size
from pg_stat_user_indexes
where relname = 'your_table'
order by idx_scan desc;
Index bloat estimation
Using pgstattuple extension:
create extension if not exists pgstattuple;
select
indexrelname,
pg_size_pretty(pg_relation_size(indexrelid)) as size,
round(100 - (avg_leaf_density)) as bloat_pct
from pg_stat_user_indexes
cross join lateral pgstatindex(indexrelid)
where pg_relation_size(indexrelid) > 10000000
order by bloat_pct desc
limit 20;
Index types and metrics
B-tree indexes
Standard index type — metrics cover scan frequency and tuple access:
idx_scan— Number of index scansidx_tup_read— Index entries returnedidx_tup_fetch— Heap rows fetched
Partial indexes
Indexes with WHERE clause — may show lower scan counts but high efficiency:
- Check query patterns match the index predicate
- Lower
idx_tup_readwith similaridx_tup_fetchindicates efficient filtering
Covering indexes (INCLUDE)
PostgreSQL 11+ — enable index-only scans:
- High
idx_tup_readwith low heap fetches indicates effective index-only scans - Monitor visibility map freshness for optimal performance
Interpreting metrics
Index-only scan efficiency
When idx_tup_read >> idx_tup_fetch:
- Index is returning entries but heap visibility checks are failing
- Run VACUUM to update visibility map
- Consider covering indexes
Low scan count on seemingly useful index
Possible causes:
- Query planner choosing sequential scan (table too small)
- Statistics outdated — run
ANALYZE - Query pattern doesn't match index
Index size growing faster than table
Indicates index bloat:
- Check for heavy UPDATE/DELETE activity
- Consider
REINDEX CONCURRENTLY - Review fillfactor settings
Troubleshooting
Index shows zero scans but queries use it
-
Statistics may have been reset:
select stats_reset from pg_stat_database where datname = current_database(); -
Check dashboard time range covers query activity period
Metrics don't update after adding index
New indexes start with zero counters. Allow time for query activity to accumulate statistics.
Index size doesn't match expected
Index size includes:
- All index pages (leaf and internal)
- Free space (bloat)
- Visibility information (for covering indexes)
Related metrics
- Table metrics — Parent table statistics
- pg_stat_statements — Query patterns using indexes