A B-tree index answers questions about a column's value: equal to this, greater than that, between these. It works because values have a total order.
An array does not. Neither does a JSON document or a body of text. The question you want to ask is not "is this column equal to X" but "does this column contain X", and that needs a different structure.
What changed in 2026
- Semi-structured data became routine. JSON columns and arrays in relational tables made containment queries a common requirement.
- Write cost got better understood. The asymmetry between GIN read and write performance became a standard design consideration.
- Pending-list behaviour got documented. The delayed-merge model, and its effect on recently-written rows, became known rather than surprising.
- Alternatives clarified. Extracting fields to generated columns emerged as the better answer where the query pattern is known.
What it indexes
A GIN index is an inverted index: for each component found in the column, it stores the list of rows containing it.
For an array column, the components are elements. For a JSON column, keys and values. For text search, individual words.
That structure answers containment efficiently — find every row whose tags array contains urgent, or whose document has a particular key, or whose text contains a word. It cannot answer ordering questions, because components have no meaningful order across rows.
| Query type |
GIN |
B-tree |
| Column equals value |
Poor fit |
Ideal |
| Column greater than value |
No |
Yes |
| Array contains element |
Yes |
No |
| JSON has key |
Yes |
No |
| Text contains word |
Yes |
No |
| Sorted output |
No |
Yes |
Writes are expensive
The cost that shapes how you use it.
Inserting a row into a B-tree adds one entry. Inserting a row into a GIN index adds one entry per component — an array of twenty tags produces twenty index entries, a JSON document with thirty keys produces many more.
That makes GIN writes substantially more expensive than B-tree writes, and the cost scales with how many components each row has. On a write-heavy table with large documents, the index maintenance can exceed the cost of the insert itself.
The mitigation built into the design is a pending list: new entries are buffered rather than merged into the index structure immediately, and merged later in bulk. That makes writes much cheaper.
It has a consequence worth knowing. A query must check both the main index and the pending list, and scanning a large pending list is slow. So immediately after a burst of writes, queries can be noticeably slower until the merge happens. If you see a GIN-indexed query intermittently slow after bulk inserts, this is why — and forcing a merge or tuning the pending list size is the lever.
When something else is better
A generated column plus a B-tree, when you query one specific field inside a document. Extracting document->>'status' to a real column and indexing it normally is smaller, faster to write, and gives the planner better statistics than a GIN index over the whole document — see generated columns.
A separate table, when an array is really a relationship. Tags stored as an array with a GIN index work; tags stored as a join table work better if you need to query, count, or constrain them.
A trigram index, for substring and fuzzy matching rather than whole-word containment — see trigram indexes.
Reach for GIN when the query genuinely needs containment across many possible components and you cannot predict which ones will be queried.
Common mistakes
- GIN where a B-tree fits. Larger, slower to write, no ordering support.
- Indexing a whole document when one field is queried. Extract it instead.
- Ignoring write cost on hot tables. Can dominate insert time.
- Not understanding pending-list latency. Intermittent slow queries after bulk writes.
- Expecting range or sort support. It has none.
- Indexing arrays that should be a join table. Model problem, not an index problem.
- Not measuring index size. GIN indexes can be very large.
FAQ
How much slower are writes?
Substantially, and it depends entirely on component count per row. Measure on your data rather than assuming; the difference between a five-element array and a fifty-key document is large.
Can I use it for full-text search?
That is one of its main uses — indexing the words in a text column. Whether a database's built-in search is sufficient or you need a dedicated search system depends on your requirements for ranking and language handling.
Does it help with LIKE queries?
Not directly. Prefix and substring matching is what trigram indexes address — see trigram indexes.
How do I know if mine is being used?
Read the plan. An index existing does not mean it is chosen, and a GIN index the planner declines is pure write cost — see query planners.
Where to go next
For extracting specific fields instead, read generated columns and JSON columns. For substring matching, trigram indexes.