Skip to content
dbexplore

Question: Indexes

How do I index a JSONB column?

Answered in the first paragraph. Last updated .

Three options, and the right one depends on whether you know in advance which keys you query. An inverted index over the whole document answers containment, key-exists and path-match tests for any key. Its narrower operator class drops the key-exists operators, builds much smaller and searches better. A plain expression index on one extracted field is smallest of all and behaves like an ordinary index on an ordinary column.

Choosing between the two inverted classes

The default class stores an entry for every key and every value, which is what lets it answer whether a key is present. The narrower class stores entries only for values, with the path to each folded into the entry, so it cannot answer key-exists questions at all. In exchange the documentation describes it as usually much smaller, and more selective on data where the same keys appear in every document, which is most real data.

It has one documented gap: it produces no entries for structures that contain no values, so a search for an empty object falls back to scanning the whole index. If that shape appears in your data, know about it before the plan surprises you.

Either way the result arrives as a bitmap rather than as rows in index order, which is a bitmap heap scan and explains why these plans look different from the ones a B-tree produces.

When an expression index wins, and what maintenance costs

If queries filter on one known field, indexing just that field as an expression gives you a normal B-tree: ranges work, sorting works, and an index-only scan becomes possible, none of which the inverted forms offer. It is smaller by an order of magnitude and cheaper on every write. The trade is flexibility, since a new query shape needs a new index.

Write cost is the part that gets underestimated. Maintaining an inverted index over an entire document is expensive, and the mechanism that defers some of that work into a pending area makes the cost lumpy rather than smaller: most writes are cheap and occasionally one pays for the batch. On a write-heavy table that shows up as latency spikes with no obvious cause.

The pragmatic route is to start with expression indexes for the queries you actually run, and add a document-wide index only when the query shapes are genuinely open-ended. Unused and missing indexes covers checking afterwards which of them is being read, and if a new index is not being used at all, the two reasons are in why Postgres is not using my index.

Put every Postgres you run on autopilot.

We onboard teams in small batches. Tell us about your fleet and we will reach out when a seat opens. One email, no drip campaign.