Audit PostgreSQL Unused Indexes
Use pg_stat_user_indexes to find indexes that are never scanned, then drop them safely after confirming they are not unique constraints.
Task
Identify write-amplifying unused indexes from statistics and remove them with a reversible plan.
When to use this
Insert throughput degrades over time as indexes accumulate, and the team suspects many are never used by queries.
Prerequisites
- PostgreSQL 13+ with statistics collection enabled
- A recent statistics reset or long-running instance
- Permission to drop indexes
Steps
1. List indexes with zero scans and their size.
SELECT schemaname, relname, indexrelname, idx_scan, pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;Expected output: Never-scanned indexes are listed with sizes, exposing the biggest write-amplification targets.
2. Confirm an index is not a unique constraint before dropping.
SELECT indexrelid::regclass, indisunique, indisprimary
FROM pg_index WHERE indexrelid = 'public.idx_orders_legacy'::regclass;Expected output: indisunique=false and indisprimary=false means it is safe to consider removing.
Troubleshooting
Problem
Statistics recently reset
Solution
Wait for a full workload cycle before trusting idx_scan; short windows mislead.
Problem
An index is used only at month end
Solution
Check reporting cycles or compare a longer window before dropping.
Limitations
- Statistics can miss rarely used indexes.
- Dropping indexes may slow some reports; review EXPLAIN plans.
- Does not cover indexes maintained by constraints.
What this recipe does
Identify write-amplifying unused indexes from statistics and remove them with a reversible plan.
Insert throughput degrades over time as indexes accumulate, and the team suspects many are never used by queries.
Prerequisites
- PostgreSQL 13+ with statistics collection enabled
- A recent statistics reset or long-running instance
- Permission to drop indexes
Procedure
- List indexes with zero scans and their size.
SELECT schemaname, relname, indexrelname, idx_scan, pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
Expected result: Never-scanned indexes are listed with sizes, exposing the biggest write-amplification targets.
- Confirm an index is not a unique constraint before dropping.
SELECT indexrelid::regclass, indisunique, indisprimary
FROM pg_index WHERE indexrelid = 'public.idx_orders_legacy'::regclass;
Expected result: indisunique=false and indisprimary=false means it is safe to consider removing.
How to confirm it worked
Run the commands above and confirm every expected result in order. If a step does not produce the stated output, use the troubleshooting notes before continuing. A passing run shows dropping the flagged indexes reduces write latency while query plans for known hot queries stay unchanged.
Troubleshooting
Statistics recently reset — Wait for a full workload cycle before trusting idx_scan; short windows mislead.
An index is used only at month end — Check reporting cycles or compare a longer window before dropping.
Limitations
- Statistics can miss rarely used indexes.
- Dropping indexes may slow some reports; review EXPLAIN plans.
- Does not cover indexes maintained by constraints.