Database Indexing: B-Tree vs LSM, Composite, Covering
mediumAn index is a separate data structure that lets the database find rows matching a query without scanning the whole table. The trade-off is fundamental: indexes accelerate reads but cost on every write and consume storage.
Key Concepts
WHERE a=? AND b>? ORDER BY c, the right index is (a, b, c) — equality first, then range, then sort. The leftmost prefix rule: an index on (a, b, c) is also usable for queries on a or (a, b). Wrong column order = unused index. Always validate with EXPLAIN ANALYZE.WHERE deleted = false) — smaller, faster for the common case. GIN: full-text, JSONB containment, array membership. GiST: spatial, geometric, fuzzy. BRIN: tiny index for naturally ordered huge tables (logs). Bloom filters (in LSM): skip SSTables for absent keys.pg_stat_user_indexes for Postgres; sys.dm_db_index_usage_stats for SQL Server. Stale stats → bad plans; run ANALYZE after large data changes.pg_stat_statements). (b) Identify top offenders by total time. (c) EXPLAIN ANALYZE each — look for sequential scans, hash joins on large tables, sort spills. (d) Design indexes for those specific queries. (e) Re-measure. Common pitfall: functions in WHERE clauses break index usage (WHERE lower(email)=? needs a functional index).Approach
- Enable slow-query logging or query stats (Postgres pg_stat_statements).
- Identify the top 5-10 slow queries by total time.
- EXPLAIN ANALYZE each. Look for sequential scans, hash joins on large tables, sort spills.
- Design indexes specifically for those queries. Composite columns ordered: equality → range → sort.
- Consider covering indexes (INCLUDE clause in Postgres) to avoid heap fetch.
- Drop unused indexes (pg_stat_user_indexes idx_scan = 0 for weeks).
- Schedule VACUUM/ANALYZE (Postgres) or OPTIMIZE TABLE (MySQL) regularly.
- For LSM, tune compaction strategy (size-tiered vs leveled) for read/write ratio.
Index types
B-tree: equality, range, ORDER BY, LIKE 'prefix%'. Default for OLTP.
Hash: equality only, O(1). Postgres has them but rarely better than B-tree.
GIN (inverted): full-text, JSONB, arrays.
GiST: spatial, geometric, trigrams (fuzzy match).
BRIN: tiny index for naturally ordered huge tables (logs).
Bitmap: low-cardinality columns; common in OLAP not OLTP.
LSM-tree (storage engine, not table-level index): write-optimized; supports range scans within an SSTable level.
Components
- Query planner / cost estimator (uses table statistics).
- ANALYZE / table statistics refresh.
- WAL / binlog for durable updates.
- Compaction (LSM) — leveled, size-tiered, universal.
- Bloom filters (LSM) — skip SSTables for absent keys.
- Index-only scans / covering indexes.
- Online index creation (CREATE INDEX CONCURRENTLY in Postgres).
Trade-offs
More indexes: faster reads, slower writes, more disk.
B-tree: balanced; writes do some random I/O at scale.
LSM: sequential writes (great throughput), read amplification (multiple SSTables).
Composite index: powerful, but wrong column order = unused.
Covering index: avoids heap fetch but larger index.
Partial index: smaller and faster for the matching subset; unusable for queries that don't match the WHERE.
Common pitfalls
- Indexing every column 'just in case'. Each costs writes.
- Wrong column order in composite index. EXPLAIN reveals it.
- Indexing low-cardinality columns. Planner ignores them.
- Forgetting to ANALYZE after large data changes; stale statistics → bad plans.
- Functions in WHERE clauses break index usage (
WHERE lower(email) = ?— need a functional index). - IS NULL queries often not covered by standard B-tree indexes.