Skip to content

Database Indexing: B-Tree, Hash, GIN, & GiST

CoreConceptAugust 3, 20269 min read

Database queries executing against multi-million row tables suffer severe latency spikes if the database storage engine must scan every page file on disk sequentially (Full Table Scan / Seq Scan). Slow database queries freeze web worker threads, exhaust database connection pools, and degrade application responsiveness.

Database Indexing creates auxiliary sorted data structures on disk that allow storage engines to find specific row tuples in logarithmic time ($O(\log N)$). However, indexes are not free: every index consumes RAM/disk storage and adds write overhead during INSERT, UPDATE, and DELETE operations. This guide details PostgreSQL index types (B-Tree, Hash, GIN, GiST), covering indexes, partial indexes, and index bloat maintenance.

Relational database indexing architecture featuring B-Tree, Hash, GIN, GiST, and partial covering indexes
Relational database indexing architecture featuring B-Tree, Hash, GIN, GiST, and partial covering indexes

Mental Model: Full Table Scans vs Index Lookup Trees

Without an index, executing SELECT * FROM users WHERE email = 'alice@example.com' forces the database query planner to execute a Sequential Scan (Seq Scan), checking every row from disk page 0 to page $N$.

Index Lookups create a pre-sorted self-balancing tree structure mapping column keys to disk physical row location pointers (TIDs - Tuple IDs).

The query engine traverses the index tree in sub-millisecond time ($O(\log N)$), fetches the exact TID, and retrieves the row page directly (Index Scan). For query performance optimization, review optimizing postgresql query performance explain analyze and postgres partitioning vs sharding.

Database query execution path comparing sequential disk scan vs B-Tree covering index lookup
Database query execution path comparing sequential disk scan vs B-Tree covering index lookup

Quick reference

  • Sequential Scans (Seq Scan) read every page file sequentially, causing heavy disk IOPS overhead.
  • Index Scans traverse B-Tree node branches in logarithmic time (O(log N)) to locate target TIDs.
  • Composite indexes (col_a, col_b) follow strict left-to-right column ordering rules.
  • Indexes add write amplification overhead during row INSERT, UPDATE, and DELETE operations.
  • Query planners dynamically choose between Seq Scan, Index Scan, and Bitmap Index Scan.

Remember this

Understand query planner execution choices to select optimal index types for high-frequency search patterns.

Index Types: B-Tree, Hash, GIN (JSONB), & GiST (PostGIS)

PostgreSQL provides specialized index data structures tailored for distinct data types and search operators:

1. B-Tree (Default): Balanced tree handling equality (=) and range queries (<, <=, >, >=, BETWEEN, ORDER BY). 2. Hash Index: Optimized exclusively for exact equality (=) lookups with smaller index sizes than B-Trees. 3. GIN (Generalized Inverted Index): Designed for composite values containing multiple elements, such as JSONB documents, arrays, or full-text search tsvector columns (WHERE data @> '{"status": "active"}'). 4. GiST (Generalized Search Tree): Handles geometric data, PostGIS spatial queries (ST_DWithin), and overlapping range types (daterange).

Quick reference

  • B-Tree indexes support equality and range queries across standard scalar data types.
  • Hash indexes provide fast sub-millisecond exact equality lookups for large string keys.
  • GIN (Generalized Inverted Index) indexes multi-element JSONB documents and array columns.
  • GiST (Generalized Search Tree) powers spatial PostGIS queries and temporal daterange overlaps.
  • BRIN (Block Range Index) indexes massive append-only time-series tables with minimal disk space.

Remember this

Match query operators to specialized index structures: B-Tree for scalars, GIN for JSONB, and GiST for spatial data.

Covering Indexes (INCLUDE), Partial Indexes, & Expression Indexes

Advanced indexing patterns reduce disk I/O and index footprint sizes:

- Covering Indexes (INCLUDE): Includes non-search payload columns inside the B-Tree leaf nodes (CREATE INDEX idx_user_email ON users(email) INCLUDE (first_name)). This enables Index-Only Scans, fetching results directly from the index without reading table heap pages. - Partial Indexes: Indexes only a filtered subset of rows (CREATE INDEX idx_active_orders ON orders(user_id) WHERE status = 'PENDING'). Reduces index storage size by 90%+. - Expression Indexes: Indexes computed function results (CREATE INDEX idx_lower_email ON users(LOWER(email))) to support case-insensitive lookups.

Database query execution path comparing sequential disk scan vs B-Tree covering index lookup
Database query execution path comparing sequential disk scan vs B-Tree covering index lookup

Quick reference

  • Index-Only Scans fetch query columns directly from B-Tree leaf nodes, bypassing heap IOPS.
  • Covering indexes (INCLUDE) append payload columns without altering B-Tree search key ordering.
  • Partial indexes (WHERE clause) reduce index memory footprint by indexing only relevant hot rows.
  • Expression indexes accelerate queries filtering on transformed functions (e.g. LOWER(email)).
  • Dramatically improves read throughput while minimizing write penalty for inactive rows.

Remember this

Use Partial Indexes and Covering Index-Only Scans to maximize query speed while minimizing RAM usage.

Index Maintenance: Bloat, REINDEX, & Identifying Unused Indexes

Over time, frequent row updates and deletions cause Index Bloat (fragmented, half-empty B-Tree pages that waste memory and disk IOPS).

### 1. Identifying Unused Indexes Query pg_stat_user_indexes to find indexes with zero scans (idx_scan = 0) that waste write performance:

1SELECT relname, indexrelname, idx_scan, pg_size_pretty(pg_relation_size(indexrelid))2FROM pg_stat_user_indexes3WHERE idx_scan = 0 AND idxname NOT LIKE '%_pkey';

### 2. Zero-Downtime Reindexing Rebuild bloated indexes concurrently without locking concurrent table reads or writes: REINDEX INDEX CONCURRENTLY idx_orders_user_id;

Quick reference

  • Index Bloat wastes RAM buffer cache space and degrades B-Tree traversal performance.
  • pg_stat_user_indexes tracks index scan counts to pinpoint unused indexes for deletion.
  • REINDEX CONCURRENTLY rebuilds bloated index pages without locking table reads or writes.
  • Autovacuum manages heap page dead tuples but requires periodic reindexing on high-churn tables.
  • Dropping unused indexes accelerates INSERT/UPDATE throughput across database tables.

Remember this

Audit pg_stat_user_indexes periodically to drop unused indexes and execute CONCURRENTLY reindexing.

Key takeaway

To test database indexing strategies, run EXPLAIN (ANALYZE, BUFFERS) SELECT ... against your database. Verify that query execution plans transition from Seq Scan to Index Scan or Index Only Scan.

Share:

Related Articles

At the heart of every database system lies a Storage Engine that determines how data is written to disk, indexed, and re

Read

As relational databases grow beyond millions to billions of rows, single-table query performance degrades due to massive

Read

Relational databases like PostgreSQL excel at Online Transaction Processing (OLTP)—handling frequent single-row reads, u

Read

Keep learning

Follow a structured path or browse all courses to go deeper.