Adding an index is the highest-ROI database optimization โ€” a well-chosen index can turn a 30-second sequential scan into a sub-millisecond index lookup. But indexing is full of trade-offs: each index slows writes, consumes storage, and requires the query planner to choose it. This guide covers index types, when to use each, and how to verify they are actually working.

Index Types: Complete Reference

Index TypeHow It WorksBest ForLimitationsDatabase Support
B-TreeBalanced tree, sorted order=, <, >, BETWEEN, LIKE 'prefix%', ORDER BY, GROUP BYSlow on LIKE '%suffix' (no leading wildcard)All databases (default index type)
HashHash table, O(1) lookup= (equality only)No range queries, no ORDER BY, no partial key matchesPostgreSQL, MySQL (Memory engine)
GIN (Generalized Inverted)Inverted index: key โ†’ list of row IDsArrays, JSONB, full-text search (tsvector)Slower writes; larger than B-Tree; GIN scans return all matchesPostgreSQL
GiST (Generalized Search Tree)Balanced tree, extensibleGeometric/spatial data (PostGIS), full-text searchMore complex than GIN; slower reads, faster writes than GINPostgreSQL
BRIN (Block Range INdex)Summary per block range (min/max)Very large tables (>100M rows), naturally sorted data (timestamps)Loose โ€” returns false positives that must be filteredPostgreSQL
SP-GiST (Space-Partitioned GiST)Partitioned search treeNon-overlapping data, phone numbers, IP addressesSpecialized; narrow use casePostgreSQL
Full-Text IndexTokenized inverted indexMATCH ... AGAINST, CONTAINS, @@ tsqueryLanguage-specific tokenization; keyword search โ‰  semantic searchPostgreSQL (GIN), MySQL (FULLTEXT), MSSQL (Full-Text)
Bitmap (Columnar)Bitmap per distinct valueLow-cardinality columns (status, category, boolean)High-cardinality columns create massive bitmapsPostgreSQL (via extensions), Oracle, Data Warehouses

Advanced Index Patterns

PatternWhat It IsWhen to UseExample
Composite (Multi-Column)Index on (col1, col2, col3)Queries filter on col1, then col2, then col3INDEX ON orders (user_id, status, created_at)
Covering Index (INCLUDE)Index contains all columns the query needsEnable Index-Only Scans; avoid heap lookupsINDEX ON users (email) INCLUDE (name, avatar)
Partial IndexIndex only rows matching WHEREIndex a subset of data, save spaceINDEX ON orders (created_at) WHERE status = 'active'
Expression / Functional IndexIndex on expression resultQueries filter on computed valuesINDEX ON users (LOWER(email)), INDEX ON orders (date_trunc('day', created_at))
Descending IndexIndex sorted DESC (not default ASC)ORDER BY col DESC is the primary access patternINDEX ON events (created_at DESC)

How to Verify Your Index Is Working

-- PostgreSQL: Check index usage
SELECT
    schemaname || '.' || relname AS table,
    indexrelname AS index,
    idx_scan AS index_scans,
    idx_tup_read AS rows_returned,
    idx_tup_fetch AS rows_fetched,
    pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0  -- Unused indexes! Safe to drop
ORDER BY pg_relation_size(indexrelid) DESC;

-- Find missing indexes (seq scans on tables > 1MB)
SELECT
    schemaname || '.' || relname AS table,
    seq_scan, seq_tup_read,
    pg_size_pretty(pg_relation_size(relid)) AS table_size
FROM pg_stat_user_tables
WHERE seq_scan > 0
  AND pg_relation_size(relid) > 1024 * 1024  -- > 1MB
ORDER BY seq_tup_read DESC;

The 5-Minute Indexing Checklist

  1. Find the slow query: pg_stat_statements or slow query log โ†’ extract the WHERE/JOIN/ORDER BY
  2. Check existing indexes: \d tablename โ€” is there already an index that covers this? Is it being used?
  3. Match index type to query: = โ†’ B-Tree or Hash; range/LIKE prefix โ†’ B-Tree; JSONB/array โ†’ GIN; full-text โ†’ GIN + tsvector
  4. Create with purpose: Partial index for subsets, covering index (INCLUDE) for Index-Only Scans, composite for multi-column filters
  5. Verify with EXPLAIN: Did the plan change from Seq Scan โ†’ Index Scan / Index Only Scan? Run ANALYZE first.

Bottom line: B-Tree indexes solve 90% of indexing needs โ€” they handle =, range, sorting, and prefix matching. GIN is essential for JSONB and full-text search workloads. The most common indexing mistakes: (1) indexing columns that are never queried, (2) missing composite indexes for multi-column WHERE clauses, and (3) not using covering indexes (INCLUDE) to enable Index-Only Scans. Run the unused index query above quarterly โ€” dropping unused indexes speeds up every INSERT/UPDATE. See also: PostgreSQL Query Optimization and Database Design Fundamentals.