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.
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);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 <-> pointKNN search.
-- Prevent overlapping bookings with a GiST exclusion constraint
ALTER TABLE bookings ADD CONSTRAINT no_overlap
EXCLUDE USING gist (room_id WITH =, during WITH &&);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.
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.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
-- 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';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
fastupdateandgin_pending_list_limitto balance search freshness against insert throughput. - BRIN granularity:
pages_per_rangecontrols summary resolution; smaller ranges prune better but enlarge the index. - jsonb operator class:
jsonb_path_opsis smaller and faster for containment than the defaultjsonb_opswhen 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.
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
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