-
Identify tables with high sequential scan activity (candidates for missing indexes):
- PostgreSQL:
SELECT relname, seq_scan, seq_tup_read, idx_scan, n_live_tup FROM pg_stat_user_tables WHERE seq_scan > 100 AND n_live_tup > 10000 ORDER BY seq_tup_read DESC LIMIT 20
- A table with high
seq_scan count and high seq_tup_read relative to n_live_tup is scanning most of the table repeatedly
-
Find the queries causing sequential scans by correlating with pg_stat_statements:
SELECT query, calls, mean_exec_time, rows FROM pg_stat_statements WHERE query ILIKE '%table_name%' ORDER BY mean_exec_time DESC LIMIT 10
- Run
EXPLAIN (ANALYZE, BUFFERS) on the top queries to confirm sequential scan usage
-
Analyze query WHERE clauses and JOIN conditions to determine which columns need indexes. Extract the filtering columns and their selectivity:
SELECT column_name, n_distinct, correlation FROM pg_stats WHERE tablename = 'target_table'
- High
n_distinct (close to row count) indicates good index selectivity
correlation close to 1.0 or -1.0 suggests the column benefits from a B-tree index
-
Recommend composite indexes for multi-column queries. Follow the equality-first, range-second ordering:
- Place columns used with
= operators first in the index
- Place columns used with
>, <, BETWEEN, or LIKE 'prefix%' last
- Example:
WHERE status = 'active' AND created_at > '2024-01-01' -> CREATE INDEX ON orders (status, created_at)
-
Identify unused indexes wasting write performance:
- PostgreSQL:
SELECT indexrelname, idx_scan, pg_size_pretty(pg_relation_size(indexrelid)) AS index_size FROM pg_stat_user_indexes WHERE idx_scan = 0 AND indexrelname NOT LIKE '%pkey' ORDER BY pg_relation_size(indexrelid) DESC
- Indexes with zero scans over a representative period are candidates for removal (verify they are not used by foreign key constraints or unique enforcement)
-
Detect redundant indexes where one index is a prefix of another:
- A single-column index on
(customer_id) is redundant if a composite index on (customer_id, created_at) exists, because the composite index serves both single-column and multi-column queries
- Generate DROP INDEX recommendations for the redundant subset indexes
-
Evaluate partial indexes for filtered queries. If a query always filters WHERE status = 'active':
CREATE INDEX idx_orders_active ON orders (created_at) WHERE status = 'active'
- Partial indexes are smaller and faster than full indexes when the filter eliminates most rows
-
Consider covering indexes (INCLUDE clause in PostgreSQL 11+) for index-only scans:
CREATE INDEX idx_orders_covering ON orders (customer_id, created_at) INCLUDE (total_amount, status)
- The INCLUDE columns are stored in the index leaf pages, enabling index-only scans without heap access
-
Estimate the impact of each recommendation:
- Index size:
SELECT pg_size_pretty(pg_relation_size('index_name')) for existing similar indexes
- Write overhead: each additional index adds approximately 5-15% write latency per INSERT/UPDATE
- Read improvement: compare EXPLAIN plans with and without the proposed index
-
Generate a prioritized recommendations report with CREATE INDEX and DROP INDEX statements, estimated storage impact, expected query improvement, and write overhead trade-off analysis.