PostgreSQL indexing choices define your query speed. Learn when to use B-Tree vs GIN indexes to master database query optimization and schema design.
During a recent refactor, I watched a dashboard query drop from a painful 4-second wait time to roughly 120ms just by swapping an index type. It’s a classic trap: we often assume a "default" index works for everything, but PostgreSQL indexing is highly specialized. If you pick the wrong tool for the job, you’re just wasting write cycles and storage.
Choosing between B-Tree and GIN is the most common crossroads in PostgreSQL database query optimization. Get this right, and your system scales; get it wrong, and your write-heavy tables will grind to a halt.
The B-Tree is the workhorse of relational databases. When you execute a CREATE INDEX statement without specifying a type, Postgres gives you a B-Tree. It stores data in a sorted, balanced tree structure, making it incredibly efficient for equality (=) and range queries (<, >, BETWEEN).
Think of it like a phone book. If you’re looking for a name, you don't read every page; you jump to the right section and narrow it down. B-Trees perform exceptionally well for:
ORDER BY).When I’m working on Database schema design: Choosing Primary Keys for Performance, I almost always stick to B-Tree indexes. They are lightweight, stable, and handle updates gracefully.
GIN, or Generalized Inverted Index, works differently. It’s designed for "composite" values—data where one row contains multiple keys, like a jsonb column or an array. Instead of mapping a row to a single value, it maps each individual element inside the column back to the rows that contain it.
If you’re building a search feature or storing logs in jsonb blobs, B-Tree won't help you find a key nested deep in a document. That’s where GIN shines.
| Feature | B-Tree | GIN |
|---|---|---|
| Best For | Scalar values (int, text, date) | Arrays, JSONB, Full-text search |
| Query Type | Equality, Range, Sort | Containment, Intersection |
| Update Speed | Fast | Slow (index bloat) |
| Storage Size | Compact | Large |
If you're dealing with complex data structures, consider using Database schema optimization: Indexed Generated Columns for JSONB to keep your index overhead manageable.
Here is where I see most developers run into trouble. Because GIN indexes map multiple values for a single row, every time you update that row, Postgres has to update every single entry in the GIN index associated with that data.
I once worked on a system where we added a GIN index to a high-traffic tags array column. Our read performance on search improved, but our UPDATE latency spiked by about 300ms because the index maintenance became a bottleneck.
Before committing to GIN, always verify your write volume. If your table sees thousands of updates per second, a GIN index might eventually cause enough bloat to degrade performance across the entire database. In those cases, you might be better off with Postgres Partial Indexes: Optimizing High-Traffic Query Performance to keep the index size small and focused.
The rule of thumb is simple:
jsonb or arrays) or for full-text search (tsvector).If you’re unsure, start with a B-Tree or a partial index. Only reach for GIN when you have a concrete query pattern that requires it, like WHERE my_jsonb_col @> '{"status": "active"}'. Always use EXPLAIN ANALYZE to check if your index is actually being used. If the planner is doing a sequential scan despite your index, your query might be formatted in a way that prevents index usage. For more complex scenarios, Database Indexing Strategies: Optimizing Laravel Query Performance provides great context on how application-level queries interact with these indexes.
Q: Can I use a B-Tree index on a JSONB column? A: Yes, but it only works for equality operators on the whole JSON object. It won't help you query specific keys inside the JSON. For key-specific lookups, you need a GIN index or an expression index.
Q: Why is my GIN index so much larger than my B-Tree index? A: Because GIN is an inverted index. It stores a list of all rows that contain a specific value. If your JSONB or array columns are wide, the index effectively duplicates a lot of metadata.
Q: Does GIN support ORDER BY?
A: No. GIN is not a sorted structure. If you need to sort results from a query involving GIN, Postgres will have to perform a separate sort operation in memory or on disk after it retrieves the rows.
I still find myself occasionally over-indexing, hoping to "fix" a bad query. It never works. You're better off running a simple EXPLAIN and actually seeing what the database is doing before you throw an index at it. Start small, measure the impact, and don't be afraid to drop indexes that aren't pulling their weight.
Database constraints are your first line of defense against bad data. Learn how to implement atomic upserts and unique indexes to ensure data integrity today.
Read moreDatabase schema design with JSONB indexing is critical for performance. Learn how PostgreSQL generated columns can speed up your queries by orders of magnitude.