Skip to main content
View rawEdit

Index metrics

Index usage and health metrics from pg_stat_user_indexes and related views.

Data sources

ViewDescription
pg_stat_user_indexesIndex usage statistics
pg_statio_user_indexesIndex I/O statistics
pg_indexesIndex definitions
pg_classIndex sizes

Core metrics

Usage metrics

MetricTypeDescription
pg_stat_user_indexes_idx_scan_totalCounterIndex scans initiated
pg_stat_user_indexes_idx_tup_read_totalCounterIndex entries read
pg_stat_user_indexes_idx_tup_fetch_totalCounterTable rows fetched via index

Size metrics

MetricTypeDescription
pg_index_size_bytesGaugeIndex size in bytes

I/O metrics

MetricTypeDescription
pg_statio_user_indexes_idx_blks_read_totalCounterIndex blocks read from disk
pg_statio_user_indexes_idx_blks_hit_totalCounterIndex blocks hit in buffer cache

Labels

LabelDescriptionExample
indexrelnameIndex nameusers_email_idx
relnameParent table nameusers
schemanameSchema namepublic
datnameDatabase namemyapp
cluster_nameCluster identifierproduction
node_nameNode identifierprimary

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:

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 scans
  • idx_tup_read — Index entries returned
  • idx_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_read with similar idx_tup_fetch indicates efficient filtering

Covering indexes (INCLUDE)

PostgreSQL 11+ — enable index-only scans:

  • High idx_tup_read with 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:

  1. Query planner choosing sequential scan (table too small)
  2. Statistics outdated — run ANALYZE
  3. Query pattern doesn't match index

Index size growing faster than table

Indicates index bloat:

  1. Check for heavy UPDATE/DELETE activity
  2. Consider REINDEX CONCURRENTLY
  3. Review fillfactor settings

Troubleshooting

Index shows zero scans but queries use it

  1. Statistics may have been reset:

    select stats_reset from pg_stat_database where datname = current_database();
  2. 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)