Skip to content
Navigation

Type at least two characters. Search covers page titles, headings, tags and database names.

↑ ↓ to navigateEnter to openEsc to close0 pages
Performanceintermediate

Index Selection

Finding indexes you need, removing the ones you do not, and judging both from usage data rather than intuition.

2 min readIntermediateUpdated Edit this page

Indexes are a trade: faster matching reads, slower writes, more memory. Reviewing them periodically keeps the trade favourable.

Missing Indexes

The signal is always the same: rows read greatly exceeding rows returned.

-- PostgreSQL: tables where sequential scans dominate and the table is not small.
SELECT relname, seq_scan, seq_tup_read, idx_scan,
       seq_tup_read / nullif(seq_scan, 0) AS avg_rows_per_seq_scan,
       n_live_tup
FROM pg_stat_user_tables
WHERE seq_scan > 0 AND n_live_tup > 100000
ORDER BY seq_tup_read DESC LIMIT 20;
-- MySQL: statements doing full scans, ranked by their cost.
SELECT * FROM sys.statements_with_full_table_scans LIMIT 20;
// MongoDB: queries whose plan was a collection scan.
db.system.profile.find({ planSummary: "COLLSCAN" }).sort({ ts: -1 }).limit(10);

Design the index from the query's predicates: equality columns first, then range, then sort. See Index Design.

Unused Indexes

Every index costs write throughput, memory and maintenance. Unused ones cost all of that for nothing.

-- PostgreSQL: never scanned, excluding those backing constraints.
SELECT s.relname AS table_name, s.indexrelname AS index_name,
       pg_size_pretty(pg_relation_size(s.indexrelid)) AS size, s.idx_scan
FROM pg_stat_user_indexes s
JOIN pg_index i ON i.indexrelid = s.indexrelid
WHERE s.idx_scan = 0 AND NOT i.indisunique AND NOT i.indisprimary
ORDER BY pg_relation_size(s.indexrelid) DESC;
-- MySQL
SELECT * FROM sys.schema_unused_indexes;
SELECT * FROM sys.schema_redundant_indexes;
// MongoDB
db.orders.aggregate([{ $indexStats: {} }]);

Redundant indexes

An index on (a) is redundant when (a, b) exists, because the composite serves every query the single-column one does. sys.schema_redundant_indexes finds these in MySQL; in PostgreSQL, compare index definitions for shared prefixes.

Reviewing on a schedule

Index sets drift: someone adds one for a query that was later rewritten, and nothing removes it. Review quarterly:

  1. List indexes by size, with their scan counts.
  2. Confirm statistics cover a full business cycle.
  3. Mark candidates invisible or disabled where possible.
  4. Wait a cycle, then drop what nothing missed.
  5. Re-check the missing-index signals afterwards, since removing an index can shift plans.