Skip to content
Low Level Design Mastery Logo
LowLevelDesign Mastery

Database Indexing Strategies

Making queries lightning fast

Without indexes, databases must scan every row to find data. With indexes, databases can jump directly to the data they need.

Diagram

An index is a separate data structure that maps search keys to data locations.

Diagram

Key Concept: Index stores keys (what you search by) mapped to locations (where the data is).


B-tree (Balanced Tree) is the most common index structure in SQL databases. It’s a self-balancing tree that keeps data sorted.

Diagram

Search Process:

  1. Start at root
  2. Compare key with node values
  3. Follow appropriate branch
  4. Repeat until leaf node
  5. Time: O(log n) - logarithmic time!

Diagram

Pros:

  • Fast reads: O(log n) lookups
  • Range queries: Efficient “WHERE x BETWEEN a AND b”
  • Sorted data: Data stays sorted
  • Mature: Well-understood, optimized

Cons:

  • Slower writes: Must rebalance tree
  • Random writes: Not optimized for sequential writes
  • Write amplification: One write can cause multiple disk writes

Used in: PostgreSQL, MySQL, SQL Server, Oracle


LSM (Log-Structured Merge) Tree optimizes writes by batching them in memory and merging to disk.

Diagram

Key Concept: Writes go to fast memory first, then flush to disk in batches. Background process merges files.


Diagram

Pros:

  • Fast writes: Sequential writes, batched operations
  • High throughput: Can handle millions of writes/second
  • Write-optimized: Designed for write-heavy workloads
  • Compression: Merging compacts data

Cons:

  • Slower reads: May need to check multiple files
  • Read amplification: One read may touch multiple files
  • Compaction overhead: Background merging uses resources

Used in: Cassandra, RocksDB, LevelDB, HBase


Inverted index maps words to documents containing them. Used in search engines and full-text search.

Diagram

Key Concept: Instead of “document → words”, store “word → documents” for fast keyword lookups.


Documents:

  • Doc1: “The cat sat on the mat”
  • Doc2: “The dog chased the cat”
  • Doc3: “A bird flew away”

Inverted Index:

the → [Doc1, Doc2]
cat → [Doc1, Doc2]
dog → [Doc2]
bird → [Doc3]
sat → [Doc1]
mat → [Doc1]
chased → [Doc2]
flew → [Doc3]
away → [Doc3]

Query: “cat”

  • Lookup “cat” in index → [Doc1, Doc2]
  • Result: Doc1 and Doc2 contain “cat”
  • Fast! No need to scan all documents

Diagram

Pros:

  • Fast search: O(1) word lookup
  • Boolean queries: AND, OR, NOT operations
  • Phrase queries: Find exact phrases
  • Ranking: Can rank results by relevance

Cons:

  • Storage overhead: Index can be large
  • Update cost: Must update index when documents change
  • Complexity: More complex than simple indexes

Used in: Elasticsearch, Lucene, Solr, full-text search


Diagram
AspectB-TreeLSM Tree
Read Performance✅ Fast (O(log n))⚠️ Slower (may check multiple files)
Write Performance⚠️ Slower (must rebalance)✅ Very fast (batched)
Range Queries✅ Excellent⚠️ Good
Storage✅ Efficient⚠️ More overhead (multiple files)
Use CaseRead-heavy, SQL databasesWrite-heavy, NoSQL databases

Diagram

Rule: Index columns used in WHERE, JOIN, and ORDER BY clauses.


2. Composite Indexes for Multi-Column Queries

Section titled “2. Composite Indexes for Multi-Column Queries”
Diagram

Rule: For queries with multiple WHERE conditions, create composite indexes.


Diagram

Rule: Monitor index usage. Remove unused indexes. Balance read speed vs write performance.


How indexing strategies affect your code design:

Index-Aware Queries
class UserRepository:
def __init__(self, db_connection):
self.db = db_connection
# Ensure indexes exist
self._create_indexes()
def _create_indexes(self):
"""Create indexes for common queries"""
# Index on email (frequent lookup)
self.db.execute("""
CREATE INDEX IF NOT EXISTS idx_users_email
ON users(email)
""")
# Composite index for user queries
self.db.execute("""
CREATE INDEX IF NOT EXISTS idx_users_status_created
ON users(status, created_at)
""")
def find_by_email(self, email: str):
"""Uses email index - fast!"""
cursor = self.db.execute(
"SELECT * FROM users WHERE email = %s",
(email,)
)
return cursor.fetchone()
def find_active_recent(self, days: int):
"""Uses composite index - fast!"""
cursor = self.db.execute("""
SELECT * FROM users
WHERE status = 'active'
AND created_at > NOW() - INTERVAL %s DAY
ORDER BY created_at DESC
""", (days,))
return cursor.fetchall()

Deep Dive: Production Indexing Considerations

Section titled “Deep Dive: Production Indexing Considerations”

B-Tree: Advanced Performance Characteristics

Section titled “B-Tree: Advanced Performance Characteristics”

Write Amplification: One logical write causes multiple physical writes.

Example:

  • Logical write: Update one row
  • Physical writes:
    • Write to data page
    • Write to index page (B-tree node)
    • Write to WAL (Write-Ahead Log)
    • Total: 3-5x amplification

Production Impact:

  • SSD wear: More writes = shorter SSD lifespan
  • Throughput: Limited by write amplification
  • Latency: Higher latency due to multiple writes

Mitigation:

  • Batch writes: Group multiple updates
  • Delayed index updates: Update index asynchronously
  • SSD optimization: Use SSDs with high write endurance

Node Size: Affects tree height and performance.

Small Nodes (4KB):

  • Shallow tree: More nodes, but fits in cache
  • Cache-friendly: More nodes in memory
  • More I/O: More nodes to read

Large Nodes (64KB):

  • Deep tree: Fewer nodes, less I/O
  • Cache-unfriendly: Fewer nodes fit in cache
  • Waste: May read unused data

Production Defaults:

  • PostgreSQL: 8KB pages (good balance)
  • MySQL InnoDB: 16KB pages (optimized for modern SSDs)
  • SQL Server: 8KB pages

Best Practice: Use database defaults unless profiling shows issues


Size-Tiered Compaction (STCS):

How it works:

  • Small files merged into larger files
  • Files organized by size tiers

Pros:

  • ✅ Simple to understand
  • ✅ Good for write-heavy workloads
  • ✅ Low write amplification

Cons:

  • ❌ High read amplification (many files to check)
  • ❌ Space amplification (temporary duplicates)

Use case: Write-heavy, read-light workloads


Leveled Compaction (LCS):

How it works:

  • Files organized into levels
  • Each level is 10x larger than previous
  • Files within level don’t overlap

Pros:

  • ✅ Low read amplification (fewer files to check)
  • ✅ Predictable space usage
  • ✅ Better for read-heavy workloads

Cons:

  • ❌ Higher write amplification (more rewriting)
  • ❌ More complex

Use case: Read-heavy, balanced workloads

Production Example: Cassandra

  • Default: Size-tiered compaction
  • Alternative: Leveled compaction (tunable)
  • Choice: Based on read/write ratio

Problem: Compaction can’t keep up with writes.

Symptoms:

  • Write latency spikes: Writes stall waiting for compaction
  • Disk I/O saturation: Compaction consumes all I/O
  • Throughput degradation: System slows down

Solutions:

Solution 1: Throttle Writes

class ThrottledLSMWriter:
def write(self, key, value):
# Check compaction lag
if self.compaction_lag > threshold:
# Throttle writes
time.sleep(0.1) # Slow down writes
self.memtable.write(key, value)

Solution 2: Increase Compaction Threads

  • More threads = faster compaction
  • Trade-off: More CPU/memory usage

Solution 3: Tune Compaction Parameters

  • Adjust compaction thresholds
  • Balance between write speed and read performance

Challenge: Inverted indexes can be larger than original data.

Example:

  • Original documents: 10GB
  • Inverted index: 15GB (150% of original!)

Why so large:

  • Stores word → document mappings
  • Posting lists (lists of document IDs)
  • Term frequencies, positions

Optimization Techniques:

1. Stop Words Removal

  • Remove common words (“the”, “a”, “an”)
  • Reduction: 20-30% index size

2. Stemming

  • Reduce words to root (“running” → “run”)
  • Reduction: 10-20% index size

3. Compression

  • Compress posting lists (delta encoding)
  • Reduction: 50-70% index size

Production Example: Elasticsearch

  • Uses compression by default
  • Index size: Typically 30-50% of original data
  • Trade-off: Slightly slower queries (decompression overhead)

Challenge: Complex queries can be slow.

Optimization Techniques:

1. Boolean Query Optimization

Bad:

# Query all documents, then filter
results = index.search("term1 AND term2 AND term3")
# Checks all documents with term1

Good:

# Start with rarest term
results = index.search("rare_term AND common_term")
# Checks fewer documents first

Rule: Process terms from rarest to most common


2. Caching Frequent Queries

Pattern: Cache query results.

class CachedSearchEngine:
def __init__(self, index, cache):
self.index = index
self.cache = cache
def search(self, query):
# Check cache first
cache_key = f"query:{hash(query)}"
cached = self.cache.get(cache_key)
if cached:
return cached
# Execute query
results = self.index.search(query)
# Cache results
self.cache.set(cache_key, results, ttl=300)
return results

Benefit: 10-100x faster for repeated queries


Index Maintenance: Production Considerations

Section titled “Index Maintenance: Production Considerations”

When to Rebuild:

  • Fragmentation: Index becomes fragmented (slow queries)
  • Statistics outdated: Query planner uses wrong estimates
  • Schema changes: Adding/removing indexes

Strategies:

1. Online Rebuild (Zero Downtime)

  • PostgreSQL: REINDEX CONCURRENTLY
  • MySQL: Online DDL (5.6+)
  • Benefit: No downtime
  • Cost: Slower, uses more resources

2. Offline Rebuild (Faster)

  • PostgreSQL: REINDEX
  • MySQL: ALTER TABLE ... DROP INDEX, ADD INDEX
  • Benefit: Faster
  • Cost: Table locked during rebuild

Production Pattern:

  • Use online rebuild for production (zero downtime)
  • Use offline rebuild during maintenance windows

What to Monitor:

1. Index Usage

-- PostgreSQL: Find unused indexes
SELECT schemaname, tablename, indexname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;

2. Index Bloat

-- PostgreSQL: Find bloated indexes
SELECT schemaname, tablename, indexname,
pg_size_pretty(pg_relation_size(indexrelid)) as size
FROM pg_stat_user_indexes
WHERE pg_relation_size(indexrelid) > 1000000000 -- >1GB
ORDER BY pg_relation_size(indexrelid) DESC;

3. Index Fragmentation

  • Monitor index scan performance
  • Alert on degradation
  • Rebuild when fragmentation >20%

Production Automation:

class IndexMaintenance:
def monitor_indexes(self):
unused = self.find_unused_indexes()
bloated = self.find_bloated_indexes()
# Alert on unused indexes
if unused:
self.alert(f"Unused indexes: {unused}")
# Schedule rebuild for bloated indexes
if bloated:
self.schedule_rebuild(bloated)

Performance Benchmarks: Real-World Index Performance

Section titled “Performance Benchmarks: Real-World Index Performance”
MetricB-TreeLSM Tree
Read Latency (point)1-5ms5-20ms
Read Latency (range)5-10ms10-50ms
Write Latency5-20ms1-5ms
Write Throughput1K-10K ops/sec10K-100K ops/sec
Read Throughput10K-50K ops/sec5K-20K ops/sec
Space Amplification1.1x1.5-2x
Write Amplification3-5x2-10x (during compaction)

Key Insights:

  • B-Tree: Better for read-heavy workloads
  • LSM Tree: Better for write-heavy workloads
  • Hybrid: Some databases use both (e.g., RocksDB with B-Tree for reads)

Benchmark: 1M row table, query by indexed column

ScenarioWithout IndexWith IndexImprovement
Point lookup500ms2ms250x faster
Range query800ms5ms160x faster
Join on indexed column2000ms50ms40x faster
Order by indexed column1500ms10ms150x faster

Key Insight: Indexes provide 40-250x performance improvement!


Pattern: Index only subset of rows.

Example:

-- Index only active orders
CREATE INDEX idx_active_orders
ON orders(order_date)
WHERE status = 'active';

Benefits:

  • ✅ Smaller index (only active orders)
  • ✅ Faster queries (fewer rows to scan)
  • ✅ Less maintenance overhead

Use case: Queries that filter by common condition


Pattern: Index contains all columns needed for query.

Example:

-- Query: SELECT name, email FROM users WHERE country = 'USA'
-- Covering index includes all columns
CREATE INDEX idx_users_country_name_email
ON users(country, name, email);

Benefits:

  • Index-only scan: Don’t need to read table
  • Faster queries: 2-5x faster than regular index
  • Less I/O: No table access needed

Trade-off: Larger index (more columns)


Pattern: Index on computed expression.

Example:

-- Index on lowercased email (case-insensitive search)
CREATE INDEX idx_users_email_lower
ON users(LOWER(email));
-- Query uses index
SELECT * FROM users WHERE LOWER(email) = '[email protected]';

Benefits:

  • ✅ Enables index usage for computed queries
  • ✅ Faster than full table scan

Use case: Queries with functions/expressions



Congratulations! You’ve completed the Database & Storage Systems section. You now understand:

  • Relational databases (ACID, normalization, indexing)
  • Isolation levels (read committed, repeatable read, serializable)
  • Scaling strategies (read replicas, connection pooling)
  • Sharding & partitioning (horizontal vs vertical)
  • NoSQL databases (Document, Key-Value, Column-family, Graph)
  • Database selection (decision framework)
  • Indexing strategies (B-trees, LSM trees, inverted indexes)

Next up: Explore Caching & Performance Optimization — Learn caching strategies to make your systems even faster!