{"post":{"slug":"gin-indexes-explained-2026","title":"GIN Indexes: Indexing Values Inside a Column","excerpt":"A B-tree indexes a column's value. A GIN index indexes the things inside it — array elements, document keys, words in a text field — which is a different problem.","category":"code","date":"January 7, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">A</span> 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.\n\nAn 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.\n\n## What changed in 2026\n\n- **Semi-structured data became routine.** JSON columns and arrays in relational tables made containment queries a common requirement.\n- **Write cost got better understood.** The asymmetry between GIN read and write performance became a standard design consideration.\n- **Pending-list behaviour got documented.** The delayed-merge model, and its effect on recently-written rows, became known rather than surprising.\n- **Alternatives clarified.** Extracting fields to generated columns emerged as the better answer where the query pattern is known.\n\n## What it indexes\n\nA GIN index is an inverted index: for each *component* found in the column, it stores the list of rows containing it.\n\nFor an array column, the components are elements. For a JSON column, keys and values. For text search, individual words.\n\nThat 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.\n\n| Query type | GIN | B-tree |\n| --- | --- | --- |\n| Column equals value | Poor fit | Ideal |\n| Column greater than value | No | Yes |\n| Array contains element | Yes | No |\n| JSON has key | Yes | No |\n| Text contains word | Yes | No |\n| Sorted output | No | Yes |\n\n## Writes are expensive\n\nThe cost that shapes how you use it.\n\nInserting 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.\n\nThat 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.\n\nThe 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.\n\nIt 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.\n\n## When something else is better\n\n**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](/blog/generated-columns-explained-2026).\n\n**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.\n\n**A trigram index**, for substring and fuzzy matching rather than whole-word containment — see [trigram indexes](/blog/trigram-indexes-explained-2026).\n\nReach for GIN when the query genuinely needs containment across many possible components and you cannot predict which ones will be queried.\n\n## Common mistakes\n\n- **GIN where a B-tree fits.** Larger, slower to write, no ordering support.\n- **Indexing a whole document when one field is queried.** Extract it instead.\n- **Ignoring write cost on hot tables.** Can dominate insert time.\n- **Not understanding pending-list latency.** Intermittent slow queries after bulk writes.\n- **Expecting range or sort support.** It has none.\n- **Indexing arrays that should be a join table.** Model problem, not an index problem.\n- **Not measuring index size.** GIN indexes can be very large.\n\n## FAQ\n\n**How much slower are writes?**\nSubstantially, 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.\n\n**Can I use it for full-text search?**\nThat 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.\n\n**Does it help with `LIKE` queries?**\nNot directly. Prefix and substring matching is what trigram indexes address — see [trigram indexes](/blog/trigram-indexes-explained-2026).\n\n**How do I know if mine is being used?**\nRead 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](/blog/query-planner-explained-2026).\n\n## Where to go next\n\nFor extracting specific fields instead, read [generated columns](/blog/generated-columns-explained-2026) and [JSON columns](/blog/json-columns-explained-2026). For substring matching, [trigram indexes](/blog/trigram-indexes-explained-2026).","hue":316,"metaTitle":"GIN Indexes: Indexing Values Inside a Column","metaDescription":"How GIN indexes support containment queries on arrays, JSON, and full-text search, why writes are expensive, and when a B-tree is the better choice.","keywords":["gin index","inverted index postgres","jsonb indexing","array containment query","full text search index","pending list gin","index write cost","database indexing 2026"],"tldr":["**It indexes components, not whole values** — array elements, JSON keys, individual words.","**Writes are considerably more expensive than B-tree writes** — one row can produce many index entries.","**A pending list buffers writes** — which means recently-inserted rows may be searched slowly until it is merged.","**It answers containment and existence, not ordering** — no range queries, no sorted output.","**Skip:** a GIN index on a column you query by whole value. A B-tree is smaller and faster there."],"readTime":"4 min read","wordCount":869},"related":[{"slug":"database-indexing-explained-2026","title":"Database indexing explained in 2026: what every developer must know","excerpt":"Bad indexes cause more production incidents than almost any other root cause. Here is the 2026 guide to choosing, building, and maintaining the right ones.","category":"code","date":"March 10, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":318},{"slug":"partial-indexes-explained-2026","title":"Partial Indexes: Indexing Only the Rows That Matter","excerpt":"Most queries against a large table only ever touch a small slice of it. A partial index covers just that slice, and is a fraction of the size and cost.","category":"code","date":"August 12, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":316},{"slug":"trigram-indexes-explained-2026","title":"Trigram Indexes: Making LIKE Queries Fast","excerpt":"A leading-wildcard search cannot use a normal index, so it scans the table. Trigram indexing makes substring and fuzzy matching indexable by breaking text into three-character pieces.","category":"code","date":"January 26, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":310}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"exactly-once-delivery-2026","title":"Exactly-Once Delivery: The Guarantee You Cannot Have","excerpt":"Message systems advertise exactly-once semantics. What is actually achievable is at-least-once delivery plus idempotent processing, which produces the same outcome by a different route.","category":"code","date":"January 7, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":312},"older":{"slug":"llm-unit-economics-2026","title":"LLM Unit Economics: What One User Actually Costs","excerpt":"Token pricing tells you nothing until you know how many calls one user action triggers. The gap between the two is where AI products discover they have negative gross margin.","category":"ai","date":"January 7, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":205}}