Indexing Beyond B-Trees

PostgreSQL

Chapter 7 · Indexing Beyond B-Trees

postgres1-4 and postgres1-6 both reached for a GIN index without fully explaining what one actually is. This chapter closes that gap, and covers the rest of Postgres's own richer index ecosystem — genuinely broader than MySQL's mostly B-tree-plus-hash approach.

B-Tree — The Default, Shared With MySQL

Both engines default to a B-tree index for ordinary equality and range queries — this is shared ground, not new material. Everything below is what Postgres adds beyond it.

GIN — Generalized Inverted Index

GIN stores a mapping from each individual component of a value — a JSONB key, an array element, a tsvector lexeme — to the list of rows containing it. This is a structurally different approach from a B-tree, which is built around ordering a single value per row; GIN is built for "does this composite value contain X" queries instead, which is exactly why it was used, unexplained until now, for postgres1-4's own JSONB containment queries and postgres1-6's own full-text search matching.

The trade-off: GIN indexes are genuinely more expensive to update than B-tree indexes — writes cost more, since a single row update can touch many entries in the index (one per component). GIN is a real cost, not a free upgrade.

GiST — Generalized Search Tree

GiST supports a broader, extensible class of queries than GIN — nearest-neighbor search, geometric and spatial queries (previewing postgres1-10's own PostGIS coverage), and, notably, it's the exact index structure powering postgres1-3's own EXCLUDE USING gist range-overlap constraint.

In general, GIN tends to be faster for lookups once built but slower to build and update; GiST is more flexible across a wider variety of query types but can be slower for simple lookups than a specialized GIN index — a genuine, real trade-off in both directions, not a case of one being strictly worse.

BRIN — Block Range Index

BRIN (Block Range Index) is designed specifically for very large tables where a column's values have a natural physical correlation with insertion order — a timestamp column on an append-only log or events table is the classic case, since rows are naturally inserted in roughly chronological order. Rather than indexing every individual row the way B-tree, GIN, and GiST all do, BRIN stores only summary information — typically the min/max values — for each physical block range of the table. The result is a dramatically smaller index, much cheaper to maintain, that still enables efficient range queries when the correlation assumption genuinely holds.

Hash Indexes

Hash indexes exist in both engines, but Postgres's own history here is worth being honest about: Postgres hash indexes weren't crash-safe or WAL-logged before Postgres 10, a real, documented limitation. They're safe to use now, but remain a genuinely narrow tool — useful only for pure equality lookups (=), never for range queries — and in practice, B-tree is usually still preferred, since it handles equality well and supports range queries too. Hash indexes remain a narrow, rarely-the-best-choice option even today.

Choosing the Right Index

Index typeBest forNot good for
B-TreeEquality and range queries on ordinary columnsContainment queries, huge tables with cheap alternatives available
GINJSONB containment, full-text search, array containmentWrite-heavy tables (expensive to update)
GiSTRange exclusion constraints, geometric/spatial data, nearest-neighborSimple equality lookups better served by B-tree
BRINHuge, naturally-ordered tables (time-series/logs)Data with no physical correlation to insertion order
HashPure equality lookups onlyAlmost everything else — B-tree usually wins anyway
BRIN's own core assumption can silently break
If the physical correlation a BRIN index depends on doesn't actually hold — rows bulk-loaded out of chronological order, or heavily updated/moved after insertion — a BRIN index becomes far less effective, and can even make the query planner's own choices worse than having no index at all, since the planner may still choose to use a summary index that no longer reflects reality. BRIN is only as good as the physical ordering it assumes.
Now you know what those actually were
postgres1-3's EXCLUDE USING gist, postgres1-4's GIN index on JSONB, and postgres1-6's GIN index on a tsvector column were all used before being formally explained — this chapter is where all three finally get their real mechanism spelled out.

Hands-On Exercises

Exercise 1

Explain what makes GIN indexes structurally different from B-tree indexes, and why that difference makes GIN well suited to JSONB containment and full-text search specifically.

📄 View solution
Exercise 2

Explain what a BRIN index is and why it's dramatically smaller than a B-tree index on the same large table — then explain this chapter's own warn-box gotcha about when BRIN stops being effective.

📄 View solution
Exercise 3

Explain the GiST index's connection to postgres1-3's own EXCLUDE USING gist range-overlap constraint — why does that specific integrity guarantee need GiST rather than a plain B-tree?

📄 View solution

Chapter 7 Quick Reference

  • B-Tree — shared default, ordering/range queries
  • GIN — component-to-row mapping, powers JSONB containment (postgres1-4) and full-text search (postgres1-6); expensive to update
  • GiST — extensible, flexible query types; powers postgres1-3's own EXCLUDE USING gist and previews postgres1-10's PostGIS
  • BRIN — tiny summary index for huge, naturally-ordered tables; breaks down if physical correlation is lost
  • Hash — equality-only, safe since Postgres 10, but narrow — B-tree usually wins anyway
  • Next chapter: PL/pgSQL — Postgres's Procedural Language