PostgreSQL Indexes, From ZeroPhase 3 of 31 min read

Composite and Partial Indexes

Column order in multi-column indexes, indexing only the rows you query, and when to stop adding indexes.


Single-column indexes get you far. Real queries usually filter on more than one thing.

Composite indexes

create index orders_customer_created_idx
  on orders (customer_id, created_at);

A composite B-tree is sorted by the first column, then by the second column within each value of the first, like a phone book sorted by last name, then first name.

That ordering decides which queries it serves well:

-- Great: uses the leading column
where customer_id = 42
 
-- Great: leading column, then a range on the second
where customer_id = 42 and created_at > now() - interval '30 days'
 
-- Also great: matches the sort order, no separate sort step
where customer_id = 42 order by created_at desc

A query that filters only on created_at cannot use the tree's ordering the same way, because matching rows are scattered across every customer_id. Newer Postgres versions can sometimes skip through the leading column, but it is far less efficient than a properly ordered index.

Rule of thumb: put columns compared with = first, and the column you range over or sort by last.

Partial indexes

If your queries only ever care about a small slice of a table, index just that slice:

create index jobs_pending_idx
  on jobs (run_at)
  where status = 'pending';

Most jobs are done, so this index is tiny compared to indexing every row. It stays fast to scan and cheap to maintain. The catch: the planner only uses it when the query's where clause clearly implies the index condition, for example where status = 'pending' and run_at < now().

Partial indexes also enforce rules. A unique partial index like

create unique index one_active_subscription
  on subscriptions (user_id)
  where active;

allows many inactive subscriptions per user but at most one active one.

Knowing when to stop

Before adding another index, check:

  • Is there an existing composite index whose leading columns already cover this query?
  • How write-heavy is the table? Every index slows every insert.
  • Does pg_stat_user_indexes show indexes with idx_scan = 0? Those cost you writes and never help reads. Consider dropping them.

The best index set is the smallest one that makes your important queries fast.

Check your understanding

Quiz

4 questions. Score 70% or more to complete this phase.

Question 1

Question 1

Index on (customer_id, created_at). Which query fits it best?

Question 2

Question 2

In a composite index, where should a column you filter with a range (>, <) usually go?

Question 3

Question 3

What is the main benefit of `create index ... where status = 'pending'`?

Question 4

Question 4

pg_stat_user_indexes shows an index with idx_scan = 0 for months. What does that suggest?

Results won't be saved unless you log in.