Reference · PostgreSQL Internals

PostgreSQL Indexing — Cheat Sheet

Compressed reference. Pairs with Lesson 1: Composite Indexes.

Glossary

TermMeaning
B-tree indexDefault Postgres index type. Stores sorted keys in a balanced tree — fast equality & range lookups, and rows come back already sorted by the indexed column(s).
Composite indexOne B-tree built over multiple columns, e.g. (org_id, status, created_at). Sorted by the first column, then the second within each value of the first, and so on.
SelectivityFraction of rows a condition matches. Low selectivity (few matching rows) = good index candidate. High selectivity (most rows match) = index often skipped.
Seq ScanReads the whole table, row by row. Chosen when no useful index exists, or the planner estimates it's cheaper than using one (e.g. low selectivity).
Index ScanWalks the B-tree to find matching rows directly, then fetches each row from the table heap.
Index-Only ScanAnswers the query from the index alone, no heap fetch needed — because every requested column is in the index.
Bitmap Heap Scan / BitmapAndPlanner's fallback when it has two separate single-column indexes to combine: builds a bitmap of matching row locations from each index, ANDs them, then fetches rows. Works, but can't provide sort order.
Sort nodeExplicit sort step in a query plan, shown in EXPLAIN when the data isn't already in the order ORDER BY needs. Cost grows with row count — expensive at scale.
EXPLAIN ANALYZERuns the query for real and shows actual time/rows per plan node, not just estimates. Always prefer this over plain EXPLAIN when diagnosing.

The composite index column-order rule

Rule of thumb

Equality columns first, range/sort column last — in that order, matching the ORDER BY direction.

For WHERE org_id = ? AND status = ? ORDER BY created_at DESC:

CREATE INDEX ON table (org_id, status, created_at DESC);

Why this order: the B-tree is sorted by org_id first, then by status within each org_id, then by created_at DESC within each (org_id, status) pair. Since both org_id and status are equality filters, all matching rows sit in one contiguous, already-created_at-sorted block. No separate sort step, no combining multiple indexes.

Quick diagnosis checklist

  1. Run EXPLAIN ANALYZE on the slow query.
  2. Look for a Sort node above an Index/Bitmap scan — means the index didn't already deliver the right order.
  3. Look for BitmapAnd — means the planner is stitching together two single-column indexes instead of using one composite index.
  4. Check column order in any existing composite index against the rule above — equality columns first, sort/range column last.

Primary sources