Mahamudul Hasan Rubel
HomeBlogCoursesAboutProjectsSkillsExperiencePhotosContact
Mahamudul Hasan Rubel

Senior Software Engineer crafting high-performance web applications and SaaS platforms.

Navigation

  • Home
  • Blog
  • Courses
  • About
  • Projects
  • Skills
  • Experience
  • Photos
  • Contact

Get in Touch

Available for senior/lead roles and consulting.

bd.mhrubel@gmail.comHire Me

Subscribe to the newsletter

Get new articles and course lessons delivered to your inbox. No spam, unsubscribe anytime.

© 2026 Mahamudul Hasan Rubel. All rights reserved.

Built with using Next.js 16 & Tailwind v4

Back to Blog
DatabasesJune 26, 20264 min read

PostgreSQL Indexing: B-Tree vs GIN for Better Query Performance

PostgreSQL indexing choices define your query speed. Learn when to use B-Tree vs GIN indexes to master database query optimization and schema design.

PostgreSQLDatabase OptimizationSQL PerformanceIndexingDatabase DesignSQLMySQL

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.

Understanding B-Tree: The Balanced Default

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:

  • Primary keys and foreign keys.
  • Columns with high cardinality (many unique values).
  • Sorting operations (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.

The GIN Index: When B-Tree Hits a Wall

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.

B-Tree vs GIN Comparison

FeatureB-TreeGIN
Best ForScalar values (int, text, date)Arrays, JSONB, Full-text search
Query TypeEquality, Range, SortContainment, Intersection
Update SpeedFastSlow (index bloat)
Storage SizeCompactLarge

If you're dealing with complex data structures, consider using Database schema optimization: Indexed Generated Columns for JSONB to keep your index overhead manageable.

The Hidden Cost: Write Performance

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.

When to Use Which?

The rule of thumb is simple:

  1. Use B-Tree for standard columns, unique constraints, and anything that requires fast sorting.
  2. Use GIN only when you need to query inside a container (like 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.

FAQ

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.

Back to Blog

Similar Posts

DatabasesJune 23, 20264 min read

Database constraints: Mastering atomic upserts and unique indexes

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 more
DatabasesJune 22, 20264 min read

Database schema optimization: Indexed Generated Columns for JSONB

Database schema design with JSONB indexing is critical for performance. Learn how PostgreSQL generated columns can speed up your queries by orders of magnitude.

Read more
A diver leisurely explores a vibrant coral reef, showcasing stunning underwater photography.
DatabasesJune 21, 20264 min read

Partial indexes for high-cardinality filtering: A deep dive

Partial indexes are the secret weapon for database performance. Learn how to use them to optimize high-cardinality filtering and cut your query latency.

Read more