IndexesPro · lab evidence1 real lab transcript

Beyond B-Tree: GIN, GiST, BRIN, and Hash Indexes

B-tree assumes a total ordering of scalar keys. Many workloads do not fit that model — full-text search, JSON containment, geometric overlap, or simply tables too large to index conventionally. PostgreSQL's extensible access-method framework provides specialised index types for each.

The one thing to understand first

B-tree assumes a total ordering of scalar keys. Many workloads do not fit that model — full-text search, JSON containment, geometric overlap, or simply tables too large to index conventionally. PostgreSQL's extensible access-method framework provides specialised index types for each.

The index type must match the operator in your WHERE clause, not the column's data type. "Containment" wants GIN, "overlap" wants GiST, "this huge table is already sorted on disk" wants BRIN — pick the structure that speaks your query's operator and the rest is detail.

GIN — Generalized Inverted Index

src/backend/access/gin/. A GIN index maps each component of a value to the list of rows containing it — an inverted index. It powers:

  • Full-text search over tsvector (each lexeme → rows).
  • jsonb containment (@>, key existence).
  • Array membership (&&, @>).

GIN is fast to search but slower to update; it buffers updates via the "pending list" (controlled by fastupdate and gin_pending_list_limit) to amortise insert cost.

Lab-verified
SQL
CREATE INDEX idx_doc_fts ON documents USING gin (to_tsvector('english', body));
CREATE INDEX idx_meta ON events USING gin (metadata jsonb_path_ops);
Real captured output for this query is in the Pro lab notebook below.

GiST — Generalized Search Tree

src/backend/access/gist/. GiST is a balanced tree where each node holds a predicate that is true for everything below it. It is a framework: operator classes define how to split and search. Used for:

  • Geometry / PostGIS — bounding-box overlap for spatial queries.
  • Range types — overlap and containment (&&, @>) and exclusion constraints.
  • Nearest-neighbour — ordered ORDER BY geom <-> point KNN search.
Lab-verified · corrected
SQL
-- Prevent overlapping bookings with a GiST exclusion constraint
ALTER TABLE bookings ADD CONSTRAINT no_overlap
  EXCLUDE USING gist (room_id WITH =, during WITH &&);
Real captured output for this query is in the Pro lab notebook below.

BRIN — Block Range Index

src/backend/access/brin/. BRIN stores only a summary (e.g. min/max) per range of physical blocks. It is tiny — often kilobytes for a multi-terabyte table — and wins when data is naturally ordered on disk, such as an append-only time-series table where created_at increases with physical position.

Lab-verified
SQL
CREATE INDEX idx_events_brin ON events USING brin (created_at)
  WITH (pages_per_range = 128);
-- A range query prunes block ranges whose min/max can't match.
Real captured output for this query is in the Pro lab notebook below.

BRIN trades precision for size: it tells you which block ranges might contain matches; the executor rechecks. On correlated data this is enormously efficient; on randomly ordered data it is useless.

Hash indexes

src/backend/access/hash/. Hash indexes support only equality (=) but can be smaller and faster than B-tree for that single case on large keys. Since PostgreSQL 10 they are crash-safe and WAL-logged. In practice B-tree is usually preferred because it also serves ranges and ordering, but hash can win for pure equality on long text keys.

Layer 3 — Watch it happen on your own database

Lab-verified
SQL
-- Dramatic size difference: BRIN vs B-tree on an ordered column
CREATE INDEX o_btree ON events (created_at);
CREATE INDEX o_brin  ON events USING brin (created_at);
SELECT relname, pg_size_pretty(pg_relation_size(oid))
FROM   pg_class WHERE relname IN ('o_btree','o_brin');

-- Confirm the access method actually gets used
EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*) FROM events WHERE created_at >= now() - interval '1 day';
Real captured output for this query is in the Pro lab notebook below.

On a large, append-ordered table the BRIN index is often a few hundred kilobytes against many megabytes (or gigabytes) for the B-tree — proof of the precision-for-size trade. The EXPLAIN tells you whether the planner chose a Bitmap Heap Scan driven by your specialised index, or fell back to a sequential scan because the operator did not match the index type.

Layer 4 — The levers this hands you

  • GIN write cost: tune fastupdate and gin_pending_list_limit to balance search freshness against insert throughput.
  • BRIN granularity: pages_per_range controls summary resolution; smaller ranges prune better but enlarge the index.
  • jsonb operator class: jsonb_path_ops is smaller and faster for containment than the default jsonb_ops when you do not need key-existence queries.
  • Exclusion constraints ride on GiST — the cleanest way to forbid overlapping ranges (bookings, schedules).

Layer 5 — What an Oracle DBA should expect vs what they get

This is where PostgreSQL's extensibility pulls clearly ahead of a stock Oracle DBA's toolkit:

  • GIN ≈ Oracle Text, but native. Full-text and "inverted index" needs that Oracle meets with Oracle Text / domain indexes are core, built-in GIN here — no separate text index type to license or manage.
  • GiST is a generic framework. Oracle spatial uses dedicated R-tree-style domain indexes; GiST is one extensible tree that serves geometry, ranges, KNN, and exclusion constraints through operator classes.
  • BRIN ≈ Exadata storage indexes / zone maps, but as an ordinary, persistent index you create with SQL — ideal for time-series where Oracle DBAs would reach for partition pruning alone.
  • Hash indexes are equality-only and rarely needed. Unlike Oracle's hash clusters, a Postgres hash index just serves =; B-tree usually wins because it also covers ranges and ordering.

Key takeaway

PostgreSQL's access-method framework gives you GIN (inverted, for full-text/jsonb/arrays), GiST (predicate tree, for geometry/ranges/KNN/exclusion), BRIN (tiny block-range summaries for physically-ordered data), and hash (equality-only). Choose by the operator your query uses, not the column type, and tune the type-specific knobs (fastupdate, pages_per_range, operator class). Matching index to data shape beats almost any query rewrite.

Interview lens

How this shows up in interviews

What an interviewer is actually testing, the same mechanism seen from three roles, and the gap between a shallow answer and one that shows real depth.

What the interviewer is testing

Whether you can match a non-scalar workload to the right access method instead of defaulting to B-tree.

Questions you should be able to answer

  • When would you choose GIN vs GiST vs BRIN vs Hash?
  • Why is a BRIN index tiny, and when does it actually work?
  • What kind of column suits GIN?

Database engineer

You pick GIN for multi-value containment (jsonb/arrays/FTS), GiST for geometry/ranges/nearest-neighbor, BRIN for naturally-ordered huge tables, and Hash for equality-only.

Software engineer

You index a jsonb or tsvector column with GIN, not a B-tree, accepting slower writes for fast containment reads.

Feature builder

You pick the access method from the query operator (@>, &&, <->, =) the feature needs.

Shallow answer

'Use GIN for JSON and GiST for geometry.' A memorized pairing with no operator-based reasoning.

Answer that shows depth

Postgres's extensible access-method framework fits the data model. GIN inverts composite values (array elements, jsonb keys, FTS lexemes) so many keys point at each row — great for containment (@>), slower to update. GiST is a balanced tree of bounding predicates for overlap (&&), ranges, and nearest-neighbor (<->). BRIN stores per-block-range min/max — tiny, and only wins when physical order correlates with the column (append-only time series). Hash serves equality only. The interview signal is mapping the query operator to the method, not reciting pairs.

Pro lab notebook

The captured run behind every query

Every SQL query above is real and the source citations are verified. Pro unlocks the captured psql output — each query re-run against a real PostgreSQL 17.10 lab, with its verbatim result.

Pro lab notebookTested, not just explained

1 real lab transcript for this lesson

The explanation and the SQL above are free — so are the verified source citations. What Pro unlocks is the proof: every query in Beyond B-Tree: GIN, GiST, BRIN, and Hash Indexes re-run against a real PostgreSQL 17.10 instance, with its verbatim psql output captured line for line.

What Pro unlocks here

  • Every query in this lesson re-run against a real PostgreSQL 17.10 lab
  • The verbatim psql output — page headers, row versions, plan nodes, catalog rows
  • The corrected form of any query the source lesson got wrong, re-captured
  • A copy-ready notebook you can replay yourself to confirm every claim
Unlock with Pro — $24.99/moor $199/yr — save ~$100

How this was verified

This explanation is anchored to PostgreSQL's source tree — the files linked above, on the REL_17_STABLE branch — and cross-checked against the official documentation for versions 15 through 18. Every SQL query shown was executed and its output captured in a throwaway PostgreSQL 17.10 lab; where a query needed correcting to run, that correction is noted inline.

Official documentation: §11.2 Index Types