Skip to content
EL
Contents

PostgreSQL index types

Are these the right index types?

DecisionAdvanced3 min read15 min hands-on#indexing

The idea in 30 seconds

Reviewing an index means asking three questions: which operator does the query use, how are the values distributed, and in what order do rows arrive? A B-tree is rarely wrong, but on a large append-only table a BRIN or partial index can do the same job at a sliver of the size, and a hash index on a column with few distinct values can be a disaster.

Helps to know first: B-tree indexes

On this page

The situation

The events table receives a steady stream of new rows and is never updated: five million today, growing by a million a month. Three queries are slow. An AI assistant opened a pull request.

Review taskIndex the events table

Pull request: “Add indexes for the three slow event queries.”

-- daily report: count events per day
CREATE INDEX ON events (created_at);

-- work queue: newest pending events (about 1% of rows)
CREATE INDEX ON events USING hash (status);

-- support tool: look up a user's events by email
CREATE INDEX ON events USING hash (email);

Would you approve it? Write the review you would leave.

Your call

How to decide

Start with B-tree. It serves equality, ranges, sorting and IS NULL, and it deduplicates repeated values. Choose something else only for a reason.

Prefer BRIN for very large tables whose values follow insertion order, typically timestamps on append-only data, unless the queries need single rows or the order gets scrambled by updates.

Prefer a partial index when queries always target a small, well-defined subset of rows, like a queue of pending work.

Prefer hash only for equality on unique or nearly unique values, and measure it against a B-tree first. On repeated values it loses.

Use GIN or GiST when the operator demands it: containment and arrays for GIN, overlaps and nearest-neighbour for GiST.

Check yourself

  1. Why was a hash index on status so slow to build?

    Show answer

    status has only two values, so all rows land in two hash buckets, each becoming a very long chain of overflow pages.

  2. What makes created_at on this table a good fit for BRIN?

    Show answer

    The table is append-only and rows arrive in time order, so each block range covers a narrow time span and its minimum and maximum let whole ranges be skipped.

  3. Which queries can use CREATE INDEX ON events (created_at) WHERE status = 'pending'?

    Show answer

    Queries whose WHERE clause guarantees status = 'pending', such as the work-queue query. Queries for other statuses cannot use it.

Finished reading?

Saved in this browser only.

Sources

Where each claim on this page comes from.

  1. 1.
    PostgreSQL documentation: 65.6 Hash Indexes
    Primary· §65.6 Hash Indexes

    Hash indexes are most suitable for unique or nearly unique data; uneven distributions create overflow chains and can make a hash index worse than a B-tree.

  2. 2.
    PostgreSQL documentation: 65.5 BRIN Indexes
    Primary· §65.5 BRIN Indexes

    BRIN suits very large tables whose column values correlate with physical location.

  3. 3.
    PostgreSQL documentation: 11.8 Partial Indexes
    Primary· §11.8 Partial Indexes

    Partial indexes avoid indexing common values and are used only by queries whose condition implies the predicate.

Go further

What to read next, and why it is worth the time.

  1. 1.
    PostgreSQL documentation: 11.8 Partial Indexes
    Primary· §11.8

    Three worked examples, one very close to this review's work queue, and a fourth that warns against using many partial indexes in place of partitioning.

Applies to PostgreSQL 18. Draft: not yet fact-checked end to end.