SELECT pg_size_pretty(pg_indexes_size('t'));
An index speeds reads but costs storage and write throughput — every relevant write must also update every applicable index.
SELECT pg_size_pretty(pg_relation_size('accounts')), pg_size_pretty(pg_indexes_size('accounts'));
EXPLAIN SELECT ...;
Seq Scan (most rows match), Index Scan (few rows match), Bitmap Index+Heap Scan (moderate rows match) — chosen by the planner based on selectivity.
EXPLAIN SELECT * FROM accounts WHERE balance BETWEEN 500 AND 600;
SELECT attname, n_distinct FROM pg_stats WHERE tablename = ?;
Selectivity (fraction of rows matched) is usually driven by cardinality (distinct value count) — low-cardinality columns rarely benefit from a plain index on equality.
EXPLAIN SELECT * FROM accounts WHERE is_active = true; -- check the rows estimate
SELECT indexrelname, idx_scan FROM pg_stat_user_indexes WHERE idx_scan = 0;
Every index is a standing write/storage cost — audit idx_scan to find indexes paying that cost with zero observed read benefit.
SELECT indexrelname, idx_scan FROM pg_stat_user_indexes WHERE relname = 'accounts' ORDER BY idx_scan;
CREATE INDEX idx ON t (col_a, col_b); -- sorted by col_a, then col_b within it
A composite index is one structure sorted primarily by its first column — column order must match how real queries narrow down.
CREATE INDEX idx_orders_customer_date ON orders (customer_id, created_at);
index (a, b, c) → usable prefixes: a | (a,b) | (a,b,c)
A composite index only narrows a scan starting from its leftmost column — a constraint on a later column alone cannot use it.
EXPLAIN SELECT * FROM orders WHERE status = 'pending'; -- won't use an index on (customer_id, status)
CREATE INDEX idx ON t (key_col) INCLUDE (payload_col);
An index-only scan skips the heap when every needed column is in the index and the visibility map confirms the page is all-visible.
EXPLAIN SELECT x, y FROM tab WHERE x = ?; -- look for "Index Only Scan"