Index types
coreintermediateB-tree is the default and correct choice for most cases — equality and range queries (<, <=, =, >=, >, BETWEEN, IN) on sortable data. GIN indexes arrays/JSONB/full-text (containment queries: does this array/document contain X). GiST handles geometric/nearest-neighbor queries. BRIN is a tiny, cheap index for huge tables where a column's values roughly follow physical row order (e.g. an auto-incrementing id or a timestamp).
Think of it as
An index is a trade: faster reads for a specific access pattern, at the cost of extra storage and slower writes (every INSERT/UPDATE has to update every index on that table too). B-tree earns its status as the default because most real queries are equality/range lookups on ordinary columns — everything else (GIN, GiST, BRIN) exists because SOME data shape breaks B-tree's assumptions. A JSONB column asking "does this document contain key X" isn't a range query at all — GIN is built for exactly that shape. BRIN takes the opposite bet from B-tree: instead of indexing every row precisely, it stores just a min/max per block, betting that a huge table's values are already roughly sorted by insertion order — tiny index, slightly less precise, but the only realistic option once a table gets large enough that a full B-tree index itself becomes expensive to maintain.
What we're doing: Index a JSONB column for fast "contains key" lookups, and confirm a plain B-tree index would not help this query shape.
- 1
- USING GIN is required here — a default B-tree index on a JSONB column can only accelerate exact whole-column equality, not "does this document contain this key/value," which is what @> (containment) actually needs.
Why this works: A B-tree index on attributes would only speed up queries comparing the ENTIRE JSONB value for equality — the containment query (@>, "does this document have color=red among possibly many other keys") needs an index that understands the document's internal structure, which is exactly what GIN provides for JSONB/array columns.
Adding a B-tree index on a JSONB column and expecting containment queries to speed up
Wrong
Better
What you see: EXPLAIN on the containment query still shows a Seq Scan even after adding an index — the index exists but the query planner never uses it for this operator.
Why: A B-tree index only supports the operators B-tree knows how to order (<, <=, =, >=, >) — @> (containment) is not one of them, so the planner correctly ignores a B-tree index for this query and falls back to scanning every row. GIN is built specifically to index composite values by their contents, which is what containment operators need.
- B-tree (default) — equality/range on ordinary columns
- GIN — arrays/JSONB/full-text containment
- GiST — geometric / nearest-neighbor
- BRIN — huge, insertion-ordered tables
Choosing an index type
Together
Remember: B-tree is the default and handles equality/range on ordinary columns; GIN is for arrays/JSONB/full-text containment queries; GiST is for geometric/nearest-neighbor; BRIN is a tiny index for huge, insertion-ordered tables. Every index has a real write cost — add them in response to an observed query pattern, not speculatively.
See also: query plans · vacuum and analyze · primary foreign unique not null check

