← Knowledge Base

How Databases Work

A comprehensive deep dive into database internals — from storage engines and B-Trees to distributed consensus and real-world system architectures.

21 Sections Storage & Indexing Query Processing Transactions Distributed Systems

1 Storage Engines Overview

Every database has a storage engine at its core — the component responsible for writing data to disk and reading it back efficiently. When you issue a SQL query, it passes through several layers before anything touches the disk.

The Journey from SQL to Disk

SQL Query → Parser → Query Optimizer → Execution Engine → Storage Engine → Buffer Pool → Disk
 ┌──────────────────────────────────────────────────────────┐
 │                    CLIENT APPLICATION                    │
 │                  SELECT * FROM users                     │
 └──────────────────────┬───────────────────────────────────┘
                        │
                        ▼
 ┌──────────────────────────────────────────────────────────┐
 │                  SQL PARSER / LEXER                      │
 │          Tokenize → Parse → Abstract Syntax Tree         │
 └──────────────────────┬───────────────────────────────────┘
                        │
                        ▼
 ┌──────────────────────────────────────────────────────────┐
 │                   QUERY PLANNER                          │
 │    Rewrite rules → Cost estimation → Plan selection      │
 │    "Should I use index scan or sequential scan?"         │
 └──────────────────────┬───────────────────────────────────┘
                        │
                        ▼
 ┌──────────────────────────────────────────────────────────┐
 │                  EXECUTION ENGINE                        │
 │      Volcano/Iterator model: open() → next() → close()  │
 └──────────────────────┬───────────────────────────────────┘
                        │
                        ▼
 ┌──────────────────────────────────────────────────────────┐
 │                  STORAGE ENGINE                          │
 │  ┌──────────┐  ┌──────────┐  ┌───────────────────────┐  │
 │  │ B-Tree   │  │ LSM Tree │  │ Buffer Pool / Cache   │  │
 │  │ Index    │  │ (alt.)   │  │ (pages in memory)     │  │
 │  └────┬─────┘  └────┬─────┘  └───────────┬───────────┘  │
 │       └──────────────┴───────────────────┬┘              │
 └──────────────────────────────────────────┼───────────────┘
                                            │
                        ┌───────────────────┴─────────────┐
                        ▼                                 ▼
                 ┌─────────────┐                  ┌─────────────┐
                 │  DATA FILES │                  │  WAL / LOG  │
                 │  (pages on  │                  │  (append-   │
                 │   disk)     │                  │   only)     │
                 └─────────────┘                  └─────────────┘

Two Major Families

B-Tree Engines Read-optimized

Used by PostgreSQL, MySQL/InnoDB, SQLite, Oracle. Data is organized in balanced trees of fixed-size pages. Reads are fast (O(log n) with high fan-out), but writes require updating pages in-place, which can cause write amplification and fragmentation.

LSM-Tree Engines Write-optimized

Used by RocksDB, LevelDB, Cassandra, HBase. Writes go to an in-memory buffer (memtable) first, then flush to sorted files on disk (SSTables). Reads may need to check multiple files, but writes are extremely fast because they are always sequential.

"The storage engine is the beating heart of any database. Everything else — the query optimizer, the transaction manager, the replication layer — depends on its ability to store and retrieve data efficiently." — Alex Petrov, Database Internals

2 Pages & Disk I/O

Databases don't read individual rows from disk — they read pages (also called blocks). A page is the smallest unit of I/O: typically 4 KB (SQLite), 8 KB (PostgreSQL), or 16 KB (MySQL/InnoDB). Understanding pages is fundamental to understanding database performance.

Page Structure

 ┌────────────────────────────────────────────────────────┐
 │                    PAGE (8 KB)                         │
 ├────────────────────────────────────────────────────────┤
 │  Page Header (24 bytes)                                │
 │  ┌──────────────────────────────────────────────────┐  │
 │  │ page_id | lsn | checksum | free_space | flags    │  │
 │  └──────────────────────────────────────────────────┘  │
 ├────────────────────────────────────────────────────────┤
 │  Item Pointers (line pointers) → offsets to tuples     │
 │  [ptr1] [ptr2] [ptr3] [ptr4] ...    → grows downward   │
 ├────────────────────────────────────────────────────────┤
 │                                                        │
 │              ◄── FREE SPACE ──►                        │
 │                                                        │
 ├────────────────────────────────────────────────────────┤
 │  Tuple data (actual rows)           ← grows upward     │
 │  ┌──────────┐ ┌──────────┐ ┌──────────┐               │
 │  │  Row 4   │ │  Row 3   │ │  Row 2   │ │  Row 1  │   │
 │  └──────────┘ └──────────┘ └──────────┘ └─────────┘   │
 ├────────────────────────────────────────────────────────┤
 │  Special space (B-tree pointers, etc.)                 │
 └────────────────────────────────────────────────────────┘

Page Sizes Across Databases

DatabaseDefault Page SizeConfigurable?Notes
PostgreSQL8 KBCompile-time onlyTOAST for large values
MySQL/InnoDB16 KBYes (4K–64K)Compression available
SQLite4 KBYes (512B–64K)Matches OS page size
Oracle8 KBYes (2K–32K)Multiple block sizes per tablespace
SQL Server8 KBNoExtents of 8 pages (64 KB)

Sequential vs Random I/O

Sequential I/O

  • Reads consecutive pages from disk
  • OS can prefetch/read-ahead
  • HDD: ~200 MB/s throughput
  • SSD: ~3,500 MB/s (NVMe)
  • Used by: table scans, WAL writes

Random I/O

  • Reads scattered pages across disk
  • HDD seek time: ~10ms per seek
  • HDD: ~100-400 IOPS
  • SSD: ~100,000-500,000 IOPS
  • Used by: index lookups, joins
🗃

The Buffer Pool

The buffer pool (or buffer cache) is an in-memory area where the database caches frequently accessed pages. When a page is needed, the engine first checks the buffer pool. If it's a hit, no disk I/O is needed. On a miss, the page is fetched from disk and placed in the pool, potentially evicting a less-recently-used page (LRU or clock-sweep algorithm). A well-tuned buffer pool can serve 99%+ of reads from memory.

SSD vs HDD Characteristics

PropertyHDDSSD (SATA)SSD (NVMe)
Random read latency3–10 ms50–100 μs10–20 μs
Random IOPS100–40050K–100K200K–1M
Sequential read100–200 MB/s500–560 MB/s2–7 GB/s
Write enduranceUnlimitedLimited (TBW)Limited (TBW)
Random/Seq ratio~1000x gap~5-10x gap~2-4x gap

The massive random vs sequential gap on HDDs is why B-Trees (with their large fan-out minimizing random reads) were so critical. SSDs narrow the gap but don't eliminate it — sequential I/O is still faster, which is why LSM trees still outperform for write-heavy workloads even on SSDs.

3 Row Store vs Column Store

The way data is physically laid out on disk fundamentally determines what operations are fast. The two dominant approaches — row-oriented and column-oriented storage — optimize for entirely different access patterns.

Data Layout on Disk

 Table: users(id, name, age, city)
 ┌────┬─────────┬─────┬──────────┐
 │ id │  name   │ age │   city   │
 ├────┼─────────┼─────┼──────────┤
 │ 1  │ Alice   │ 30  │ NYC      │
 │ 2  │ Bob     │ 25  │ London   │
 │ 3  │ Charlie │ 35  │ Tokyo    │
 │ 4  │ Diana   │ 28  │ Paris    │
 └────┴─────────┴─────┴──────────┘

 ROW STORE layout (on disk):
 ┌──────────────────────────────────────────────────────┐
 │ [1,Alice,30,NYC] [2,Bob,25,London] [3,Charlie,35,...│
 └──────────────────────────────────────────────────────┘
 → All columns of a row are stored together contiguously
 → Great for: SELECT * FROM users WHERE id = 2

 COLUMN STORE layout (on disk):
 ┌───────────────────────┐
 │ id:   [1, 2, 3, 4]    │  ← column file 1
 ├───────────────────────┤
 │ name: [Alice,Bob,...]  │  ← column file 2
 ├───────────────────────┤
 │ age:  [30, 25, 35, 28] │  ← column file 3
 ├───────────────────────┤
 │ city: [NYC,London,...] │  ← column file 4
 └───────────────────────┘
 → Each column stored separately
 → Great for: SELECT AVG(age) FROM users

Row Store (OLTP)

  • PostgreSQL, MySQL, Oracle, SQL Server
  • Fast point queries (fetch entire row)
  • Fast INSERT / UPDATE / DELETE
  • Good for transactional workloads
  • Typically B-Tree indexed
  • Poor at full-table aggregations

Column Store (OLAP)

  • ClickHouse, DuckDB, Redshift, BigQuery
  • Fast aggregations (SUM, AVG, COUNT)
  • Excellent compression (similar values together)
  • Read only needed columns — skip rest
  • Vectorized execution on columns
  • Slow for single-row lookups, updates

Compression Advantage of Column Stores

Because each column file contains values of the same type and often similar values, column stores achieve dramatically better compression:

TechniqueHow It WorksBest For
Run-Length EncodingStore (value, count) pairs for repeated valuesSorted low-cardinality columns
Dictionary EncodingMap distinct values to short integer codesString columns with repeated values
Bit-PackingUse minimal bits per value (e.g., 3 bits for 0–7)Integer columns with small ranges
Delta EncodingStore differences between consecutive valuesTimestamps, sequential IDs
🔄

HTAP — Hybrid Transactional/Analytical Processing

Systems like TiDB, CockroachDB, SingleStoreDB, and SAP HANA attempt to serve both OLTP and OLAP workloads by maintaining row-oriented storage for transactional writes and a columnar replica or in-memory column index for analytical queries. The challenge is keeping both representations consistent without sacrificing performance of either workload.

4 B-Trees & B+ Trees

The B+ Tree is the most important data structure in database engineering. Nearly every relational database uses B+ Trees for its primary indexes. It was designed specifically for disk-based systems, where minimizing the number of I/O operations is paramount.

B+ Tree Structure

                        ┌─────────────────────────┐
                        │    [30 | 60 | 90]       │  ◄─ Root (internal node)
                        │   /    |     |     \    │     Keys act as separators
                        └──/─────|─────|──────\───┘
                          /      |     |       \
            ┌────────────┘  ┌────┘     └────┐   └────────────┐
            ▼               ▼               ▼                ▼
 ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
 │ [10|15|20|25]│ │ [35|40|45|50]│ │ [65|70|75|80]│ │ [95|97|99]   │
 │              │ │              │ │              │ │              │
 │ Internal     │ │ Internal     │ │ Internal     │ │ Internal     │
 └──┬──┬──┬──┬─┘ └──┬──┬──┬──┬─┘ └──┬──┬──┬──┬─┘ └──┬──┬──┬──┘
    │  │  │  │       │  │  │  │       │  │  │  │       │  │  │
    ▼  ▼  ▼  ▼       ▼  ▼  ▼  ▼       ▼  ▼  ▼  ▼       ▼  ▼  ▼
 ┌────┐┌────┐     ┌────┐┌────┐     ┌────┐┌────┐     ┌────┐┌────┐
 │Leaf││Leaf│ ... │Leaf││Leaf│ ... │Leaf││Leaf│ ... │Leaf││Leaf│
 │data││data│     │data││data│     │data││data│     │data││data│
 └──┬─┘└──┬─┘    └──┬─┘└──┬─┘    └──┬─┘└──┬─┘    └──┬─┘└──┬─┘
    └──►───┘──►──────┘──►──┘──►──────┘──►──┘──►──────┘──►──┘
    ▲                                                         ▲
    └─── Leaf nodes linked for range scans ───────────────────┘

Key Properties

High Fan-Out

Each internal node holds hundreds of keys (e.g., with 8 KB pages and 8-byte keys + 8-byte pointers, a node can hold ~500 keys). This means a tree of depth 3 can index ~125 million rows. Most queries touch just 3-4 pages.

Balanced by Design

All leaf nodes are at the same depth. Insertions and deletions maintain balance through page splits and merges, guaranteeing O(log n) access time for any key regardless of data distribution.

Leaf-Linked List

In B+ Trees (unlike B-Trees), all data lives in leaf nodes, which are linked together. This makes range queries (WHERE age BETWEEN 20 AND 30) efficient: find the starting leaf, then follow pointers sequentially.

Disk-Optimized

Node size matches page size, so each node = one I/O operation. The high fan-out means even billion-row tables need only 3-4 levels, i.e., 3-4 disk reads for any lookup.

Fan-Out & Depth Calculation

Page SizeKey+Ptr SizeFan-Out (f)Depth for 1B rowsPages for lookup
4 KB16 bytes~25044 pages
8 KB16 bytes~50044 pages
16 KB16 bytes~100033 pages

Formula: depth = ⌈logf(n)⌉ where f is fan-out and n is number of rows. With fan-out 500: log500(1,000,000,000) ≈ 3.3, so depth = 4.

Operations

  1. Search: Start at root. At each internal node, binary search for the correct child pointer. Descend until reaching a leaf. Cost: O(logf n) page reads, typically 2-4.
  2. Insert: Search for the correct leaf. If the leaf has space, insert the key. If the leaf is full, split it into two leaves and push the middle key up to the parent. Splits can cascade to the root, increasing tree height by 1.
  3. Delete: Find and remove the key from the leaf. If the leaf becomes less than half full, merge with a sibling or redistribute keys. In practice, many databases delay merges (tombstone-based) to avoid I/O.
  4. Range scan: Search for the start key, then follow the leaf linked list until the end key is reached. Extremely efficient for ORDER BY and BETWEEN queries.
"B-Trees are the workhorse of database indexing — not because they're theoretically optimal, but because they map perfectly to the realities of disk-based storage: large pages, high fan-out, and sequential leaf scans." — Adapted from Database Internals by Alex Petrov

5 LSM Trees

The Log-Structured Merge Tree (LSM Tree) is the alternative to B-Trees, optimized for write-heavy workloads. Instead of updating data in-place, LSM Trees buffer writes in memory and periodically flush them as immutable sorted files to disk.

Write Path

Write Request → WAL (durability) → Memtable (in-memory sorted tree) → Flush to SSTable on disk → Compaction
 WRITE PATH:
 ┌──────────┐     ┌─────────────────────────────────────┐
 │  Write   │────►│  Write-Ahead Log (append-only)      │
 │  Request │     └─────────────────────────────────────┘
 └────┬─────┘                    │
      │                          │ (crash recovery)
      ▼                          │
 ┌─────────────────────────┐     │
 │     MEMTABLE            │◄────┘
 │  (Red-Black / Skip List)│
 │  Sorted in memory       │
 │  [key1→v, key2→v, ...]  │
 └─────────┬───────────────┘
           │ (when full, ~64MB)
           ▼
 ┌─────────────────────────┐
 │    Immutable Memtable   │──── flush to disk ────┐
 └─────────────────────────┘                       │
                                                   ▼
 DISK (sorted runs / SSTables):
 ┌─────────────────────────────────────────────────────┐
 │ Level 0:  [SST-1] [SST-2] [SST-3]  (may overlap)   │
 ├─────────────────────────────────────────────────────┤
 │ Level 1:  [────── SST-A ──────] [──── SST-B ────]  │
 ├─────────────────────────────────────────────────────┤
 │ Level 2:  [────────────── SST-X ──────────────────] │
 │           (10x larger than level 1)                 │
 └─────────────────────────────────────────────────────┘
           ▲                           │
           └─── COMPACTION ────────────┘
                (merge-sort overlapping SSTables)

Compaction Strategies

Leveled Compaction

  • Used by: LevelDB, RocksDB (default)
  • Each level is 10x larger than previous
  • No overlap within a level (except L0)
  • Lower read amplification
  • Higher write amplification
  • More predictable read performance
  • Better space efficiency

Size-Tiered Compaction

  • Used by: Cassandra (default), HBase
  • Merge SSTables of similar size together
  • SSTables within a level can overlap
  • Lower write amplification
  • Higher read amplification & space usage
  • Simpler implementation
  • Better for write-heavy workloads

Amplification Trade-offs

MetricB-TreeLSM (Leveled)LSM (Size-Tiered)
Write amplification~2-4x Medium~10-30x High~4-10x Medium
Read amplification~1x Low~1-2x Low~5-20x High
Space amplification~1.5x Medium~1.1x Low~2-3x High

Read Path

  1. Check the active memtable (in memory — fastest).
  2. Check immutable memtable(s) awaiting flush.
  3. Check Level 0 SSTables (may overlap, check all).
  4. For each subsequent level, use the SSTable's index block and optional Bloom filter to quickly determine if the key exists in that file.
  5. If found, return the value. If a tombstone is found, the key was deleted.

6 Hash Indexes

Hash indexes provide O(1) average-case lookups for exact-match queries. They work by applying a hash function to the key and using the result to locate the data directly.

 Hash Index Mechanism:
                                      ┌─────────────┐
     hash("alice") = 3  ──────────►   │  Bucket 0   │
     hash("bob")   = 1  ──────┐      ├─────────────┤
     hash("carol") = 3  ───┐  │      │  Bucket 1   │◄── bob → row_ptr
     hash("dave")  = 0  ─┐ │  │      ├─────────────┤
                          │ │  │      │  Bucket 2   │
                          │ │  │      ├─────────────┤
                          │ │  └────► │  Bucket 3   │◄── alice → row_ptr
                          │ └───────► │             │    carol → row_ptr
                          │           ├─────────────┤    (chaining)
                          └─────────► │  Bucket 0   │◄── dave → row_ptr
                                      └─────────────┘

Characteristics

AspectHash IndexB-Tree Index
Exact lookupO(1) avgO(log n)
Range queriesNot supportedEfficient
ORDER BYNot supportedSupported
Prefix matchingNoYes
Worst caseO(n) with collisionsO(log n) always
ResizingExpensive (rehash)Incremental splits
Disk-friendlyPoor (random access)Good

Where Hash Indexes Are Used

PostgreSQL CREATE INDEX ... USING hash

Supported but rarely recommended. Before v10, hash indexes weren't crash-safe (not WAL-logged). Now they're safe but still only support equality. Useful for very large text keys where B-Tree overhead is high.

In-Memory Systems Primary use case

Redis, Memcached, and in-memory hash tables in query execution (e.g., hash join build phase) use hash indexes heavily. Without disk I/O concerns, the O(1) lookup shines.

Bitcask (Riak) Log-structured

Bitcask uses an in-memory hash map pointing to file offsets on disk. All writes are appended to a log file. Fast reads (one disk seek) and writes, but the entire key set must fit in memory.

7 Bloom Filters

A Bloom filter is a space-efficient probabilistic data structure that tests whether an element is a member of a set. It can tell you "definitely not in the set" or "probably in the set" — but never gives false negatives.

 Bloom Filter: bit array of m bits, k hash functions

 Insert "alice":
   h1("alice") = 2    h2("alice") = 5    h3("alice") = 9

   Bit array:  [0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]
                       ▲           ▲                 ▲
                   set bit 2   set bit 5         set bit 9

 Insert "bob":
   h1("bob") = 1      h2("bob") = 5      h3("bob") = 12

   Bit array:  [0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0]
                    ▲                                  ▲
                set bit 1                          set bit 12
                           (bit 5 already set — shared!)

 Query "carol":
   h1("carol") = 2    h2("carol") = 7    h3("carol") = 12

   Check: bit[2]=1 ✓   bit[7]=0 ✗   → DEFINITELY NOT IN SET
   (one bit is 0, so "carol" was never inserted)

 Query "dave":
   h1("dave") = 1     h2("dave") = 5     h3("dave") = 9

   Check: bit[1]=1 ✓   bit[5]=1 ✓   bit[9]=1 ✓  → PROBABLY IN SET
   (all bits set — could be a FALSE POSITIVE if bits were set by others)

False Positive Rate

The probability of a false positive is approximately: p ≈ (1 − e−kn/m)k

Bits per element (m/n)Optimal kFalse positive rateSpace per element
862.2%1 byte
1070.82%1.25 bytes
1280.31%1.5 bytes
16110.046%2 bytes
20140.0066%2.5 bytes

Usage in Databases

LSM Trees Critical

Each SSTable has an associated Bloom filter. Before reading an SSTable from disk, the filter is checked. If it says "not present," the SSTable is skipped entirely — saving a costly disk read. This transforms LSM read performance from checking all SSTables to checking only relevant ones.

Distributed Queries Network savings

Cassandra uses Bloom filters to avoid querying nodes that don't have the requested partition. In distributed joins, Bloom filters can be broadcast to filter rows before network transfer (Bloom join / semi-join optimization).

8 SQL Query Lifecycle

Understanding how a SQL statement goes from text to results is crucial for performance tuning and debugging.

 ┌──────────────────────────────────────────────────────────────┐
 │  SELECT u.name, COUNT(o.id)                                  │
 │  FROM users u JOIN orders o ON u.id = o.user_id              │
 │  WHERE u.age > 25                                            │
 │  GROUP BY u.name                                             │
 │  HAVING COUNT(o.id) > 5                                      │
 │  ORDER BY COUNT(o.id) DESC                                   │
 └──────────────────────┬───────────────────────────────────────┘
                        │
          ┌─────────────▼──────────────┐
          │     1. LEXER / TOKENIZER   │
          │  Break SQL into tokens:    │
          │  SELECT, u.name, COUNT,    │
          │  FROM, users, JOIN, ...    │
          └─────────────┬──────────────┘
                        │
          ┌─────────────▼──────────────┐
          │     2. PARSER              │
          │  Build Abstract Syntax     │
          │  Tree (AST). Check syntax. │
          │  Resolve table/column      │
          │  names against catalog.    │
          └─────────────┬──────────────┘
                        │
          ┌─────────────▼──────────────┐
          │     3. QUERY REWRITER      │
          │  Apply rewrite rules:      │
          │  • View expansion          │
          │  • Subquery flattening     │
          │  • Predicate pushdown      │
          │  • Constant folding        │
          └─────────────┬──────────────┘
                        │
          ┌─────────────▼──────────────┐
          │     4. QUERY OPTIMIZER      │
          │  Generate candidate plans.  │
          │  Estimate cost of each.     │
          │  Choose cheapest plan.      │
          │  (join order, index usage,  │
          │   join algorithm, etc.)     │
          └─────────────┬──────────────┘
                        │
          ┌─────────────▼──────────────┐
          │     5. PLAN EXECUTOR        │
          │  Execute the chosen plan.   │
          │  Volcano model:             │
          │  open() → next() → close()  │
          │  Pull tuples through the    │
          │  operator tree.             │
          └─────────────┬──────────────┘
                        │
          ┌─────────────▼──────────────┐
          │     6. RESULT SET           │
          │  Format and send results    │
          │  back to client via wire    │
          │  protocol.                  │
          └────────────────────────────┘

Logical vs Physical Plan

Logical Plan

  • Describes what to compute
  • Uses relational algebra operators
  • Scan, Filter, Project, Join, Aggregate
  • No implementation details
  • Multiple possible physical plans

Physical Plan

  • Describes how to compute
  • Specific algorithms chosen
  • Index Scan vs Seq Scan
  • Hash Join vs Merge Join
  • Includes cost estimates

9 Query Optimizer

The query optimizer is arguably the most complex component of a database. Its job: given a logical query, find the cheapest physical execution plan that produces correct results. This is an NP-hard problem in general, so optimizers use heuristics and dynamic programming.

Cost-Based vs Rule-Based

Cost-Based Optimizer (CBO)

  • Used by: PostgreSQL, MySQL 8+, Oracle, SQL Server
  • Enumerates possible plans
  • Estimates I/O, CPU, memory cost of each
  • Uses table statistics (row count, distinct values, histograms)
  • Picks the plan with lowest estimated cost
  • Can make bad choices with stale statistics

Rule-Based Optimizer (RBO)

  • Used by: older systems, some NoSQL
  • Applies fixed transformation rules
  • "Always use an index if available"
  • "Push predicates down before joins"
  • Simpler but less adaptive
  • Can miss better plans

Selectivity Estimation

The optimizer must estimate how many rows each operation produces. Selectivity is the fraction of rows that pass a filter.

Predicate TypeEstimation MethodAccuracy
col = value1 / n_distinct (uniform assumption) or histogram lookupGood with histograms
col > valueFraction of histogram buckets above valueGood
col LIKE 'abc%'Range estimation on textModerate
col1 = x AND col2 = yAssume independence: sel(A) × sel(B)Often wrong — correlated columns
func(col) = valueDefault estimate (often 0.1% or 1%)Poor

Join Ordering

For a query joining n tables, there are n! possible join orders (ignoring commutativity, it's still Catalan numbers — exponential). A 10-table join has over 17 billion possible orderings. Optimizers use:

10 Join Algorithms

Joins are the most expensive operation in SQL processing. The choice of join algorithm dramatically affects performance, and the optimizer must choose wisely based on table sizes, available indexes, sort orders, and available memory.

The Three Major Join Algorithms

 NESTED LOOP JOIN:
 for each row r in outer_table:       ┌─────────┐
   for each row s in inner_table:     │ Outer   │──┐
     if r.key == s.key:               │ (small) │  │  for each outer row
       emit (r, s)                    └─────────┘  │
                                                    ▼
 Can use index on inner table         ┌──────────────────┐
 for Index Nested Loop Join           │ Scan Inner table │
 (avoid full inner scan each time)    │ (or index lookup)│
                                      └──────────────────┘

 HASH JOIN:
 Build phase:                         ┌─────────┐
   Build hash table from              │ Build   │ → Hash Table
   smaller table (build side)         │ (small) │   {key: [rows]}
 Probe phase:                         └─────────┘
   Scan larger table, probe           ┌─────────┐
   hash table for matches             │ Probe   │ → Lookup each key
                                      │ (large) │   in hash table
                                      └─────────┘

 MERGE JOIN:
 Both inputs must be sorted on        ┌────────┐  ┌────────┐
 the join key (or index provides      │Sorted A│  │Sorted B│
 sort order)                          │ 1,3,5,7│  │ 2,3,5,8│
                                      └───┬────┘  └───┬────┘
 Two pointers advance together:           │           │
 Like merging two sorted arrays           └─────┬─────┘
                                                ▼
                                          Match: 3, 5
AlgorithmTime ComplexityRequiresBest WhenMemory
Nested Loop O(N × M) or O(N × log M) with index Nothing special Small outer table, indexed inner table Low
Hash Join O(N + M) Equality join only, memory for hash table Large unsorted tables, no useful indexes High (build table)
Merge Join O(N log N + M log M) or O(N + M) if pre-sorted Sortable join keys Both inputs already sorted (index), or result needs ORDER BY on join key Medium
⚙

When the Optimizer Chooses Each

Nested Loop: One side is very small (< ~1000 rows) and the other has an index on the join column. The "index nested loop join" turns O(N×M) into O(N × log M). Hash Join: Both tables are large, no useful indexes, and enough memory for the hash table (or it can spill to disk). Most equi-joins on large tables use this. Merge Join: Both inputs are already sorted (from an index scan or a previous sort), especially if the query also needs ORDER BY on the join key.

11 Indexing Strategies

Choosing the right indexes is the single most impactful thing you can do for query performance. But more indexes aren't always better — each index adds write overhead and storage cost.

Index TypeSyntax (PostgreSQL)Use CaseHow It Works
Single Column CREATE INDEX idx ON t(col) Equality and range queries on one column B-Tree on one column's values
Composite CREATE INDEX idx ON t(a, b, c) Queries filtering on leftmost prefix: a, (a,b), (a,b,c) B-Tree sorted by (a, then b, then c). Left-prefix rule applies.
Covering CREATE INDEX idx ON t(a) INCLUDE (b, c) Index-only scans — avoid table heap lookup B-Tree on (a), with (b,c) stored in leaf pages only
Partial CREATE INDEX idx ON t(col) WHERE active = true Index only a subset of rows — smaller, faster B-Tree that only includes rows matching the predicate
Expression CREATE INDEX idx ON t(LOWER(email)) Queries with function calls in WHERE B-Tree on computed expression values
Hash CREATE INDEX idx ON t USING hash(col) Equality-only lookups on large keys Hash table — O(1) lookup, no range support
GIN CREATE INDEX idx ON t USING gin(col) Full-text search, JSONB containment, array membership Inverted index: maps each element/token to list of row IDs
GiST CREATE INDEX idx ON t USING gist(col) Geometric/spatial data, range types, nearest-neighbor Generalized search tree — balanced tree of bounding boxes

The Composite Index Left-Prefix Rule

 Index on (country, city, street):

 Can satisfy:                            Cannot satisfy:
 ✓ WHERE country = 'US'                  ✗ WHERE city = 'NYC'
 ✓ WHERE country = 'US' AND city = 'NYC' ✗ WHERE street = 'Broadway'
 ✓ WHERE country = 'US' AND city = 'NYC' ✗ WHERE city = 'NYC' AND street = '...'
   AND street = 'Broadway'
 ✓ WHERE country = 'US' ORDER BY city    (skips leftmost prefix!)

 Think of it like a phone book sorted by:
 last_name → first_name → middle_name
 You can look up by last_name, or (last, first), but NOT by first_name alone.
⚠

Index Anti-Patterns

Over-indexing: Every index slows writes (INSERT, UPDATE, DELETE must update all indexes). Unused indexes: Monitor with pg_stat_user_indexes and drop unused ones. Low-selectivity indexes: An index on a boolean column (50/50 split) rarely helps — the optimizer will choose a sequential scan anyway. Wrong column order in composite indexes: The equality columns should come first, then range columns.

12 ACID Properties

ACID is the set of guarantees that make database transactions reliable. But each letter means something very specific — and most explanations only scratch the surface.

Atomicity A

Not about concurrency (that's Isolation). Atomicity means a transaction is all-or-nothing. If any part fails — whether from a constraint violation, a crash, or an explicit ROLLBACK — the entire transaction's effects are undone. Implementation: the database uses the WAL (undo log) to reverse partial changes, or shadow paging to avoid modifying data in-place until commit.

Failure scenario: A bank transfer debits account A but crashes before crediting account B. Atomicity ensures the debit is rolled back on recovery — the money isn't lost.

Consistency C

The weakest guarantee — partly application-level. The database enforces declared constraints (foreign keys, CHECK constraints, UNIQUE, NOT NULL). But "business logic consistency" is the application's job. If you transfer $100 but the application only subtracts it from one account, the DB can't know the total should be preserved.

Failure scenario: An INSERT violates a UNIQUE constraint. The database rejects it and the transaction is aborted — the database remains consistent. But if your application logic allows negative balances and you didn't add a CHECK constraint, that's on you.

Isolation I

Concurrent transactions should behave as if they ran serially (in the strongest isolation level). In practice, databases offer weaker isolation levels for better performance. Isolation prevents anomalies like dirty reads, non-repeatable reads, and phantom reads. Implementation: locks (2PL), MVCC, or optimistic concurrency control.

Failure scenario: Transaction A reads a row, transaction B updates and commits it, then A reads again and sees a different value — a non-repeatable read. Proper isolation level prevents this.

Durability D

Once a transaction commits, its changes survive any subsequent failure — power loss, crash, disk failure (with replication). Implementation: the WAL is flushed to disk (fsync) before the commit is acknowledged. Even if the data pages haven't been written yet, the WAL contains enough information to reconstruct the changes on recovery.

Failure scenario: The server crashes immediately after COMMIT returns. On restart, the database replays the WAL and applies any committed changes that weren't yet in the data files. No committed data is lost.

13 Isolation Levels

SQL defines four isolation levels, each preventing different anomalies. Stronger isolation means fewer anomalies but more overhead (locking, version tracking, aborts).

Anomaly Reference

AnomalyDescriptionExample
Dirty ReadReading data written by an uncommitted transactionT1 writes row, T2 reads it, T1 rolls back — T2 used phantom data
Non-Repeatable ReadA row read twice within a transaction returns different valuesT1 reads row, T2 updates and commits, T1 reads again — different value
Phantom ReadA range query returns different rows when re-executedT1 queries WHERE age > 25 (3 rows), T2 inserts a matching row, T1 re-queries (4 rows)
Write SkewTwo transactions read overlapping data, make disjoint writes, creating inconsistencyTwo doctors both read "2 on-call", each withdraws themselves, leaving 0 on-call

Isolation Level vs Anomaly Matrix

Isolation Level Dirty Read Non-Repeatable Read Phantom Read Write Skew
Read Uncommitted Possible Possible Possible Possible
Read Committed Prevented Possible Possible Possible
Repeatable Read Prevented Prevented DB-dependent* Possible
Serializable Prevented Prevented Prevented Prevented

*PostgreSQL's "Repeatable Read" actually uses Snapshot Isolation, which prevents phantom reads but still allows write skew. MySQL/InnoDB uses gap locks at Repeatable Read to prevent phantoms but with different trade-offs.

Default Levels

DatabaseDefault LevelImplementation
PostgreSQLRead CommittedMVCC (snapshot per statement)
MySQL/InnoDBRepeatable ReadMVCC + gap locks
OracleRead CommittedMVCC (undo segments)
SQL ServerRead CommittedLocking (or RCSI with MVCC)
SQLiteSerializableDatabase-level locking

14 MVCC (Multi-Version Concurrency Control)

MVCC is the key insight that enables high-concurrency databases: readers never block writers, and writers never block readers. Instead of locking rows, the database keeps multiple versions of each row and shows each transaction only the versions it's allowed to see.

PostgreSQL MVCC

 PostgreSQL stores version info IN the heap tuple itself:

 Tuple Header:
 ┌──────────┬──────────┬────────────┬────────────────────┐
 │  xmin    │  xmax    │  ctid      │  infomask bits     │
 │ (created │ (deleted │ (location  │  (committed,       │
 │  by txn) │  by txn) │  on disk)  │   aborted, etc.)   │
 └──────────┴──────────┴────────────┴────────────────────┘

 Example: UPDATE users SET name='Bob2' WHERE id=1;
 (executed by transaction 150)

 BEFORE:
 ┌─────────────────────────────────────────┐
 │ xmin=100  xmax=∞  data: {id:1, Bob}    │  ← visible to all
 └─────────────────────────────────────────┘

 AFTER:
 ┌─────────────────────────────────────────┐
 │ xmin=100  xmax=150 data: {id:1, Bob}   │  ← old version (dead)
 └─────────────────────────────────────────┘
 ┌─────────────────────────────────────────┐
 │ xmin=150  xmax=∞   data: {id:1, Bob2}  │  ← new version (live)
 └─────────────────────────────────────────┘

 Visibility rule (simplified, for Read Committed):
 A tuple is visible if:
   xmin is committed AND (xmax is not set OR xmax is not committed)

MySQL/InnoDB MVCC

 InnoDB stores UNDO LOG pointers in each row:

 Clustered Index Row:
 ┌──────────┬──────────┬──────────────┬──────────┐
 │  trx_id  │ roll_ptr │  actual data │  ...     │
 │ (last    │ (pointer │              │          │
 │  modifier)│ to undo) │              │          │
 └──────────┴────┬─────┴──────────────┴──────────┘
                 │
                 ▼
 ┌──────────────────────────────────┐
 │  Undo Log Entry (prev version)  │
 │  trx_id=140, data: {old values} │
 │  roll_ptr ──────────────────────┼──►  Even older version
 └──────────────────────────────────┘

 → InnoDB builds version chain via undo log
 → PostgreSQL stores all versions in heap (needs VACUUM)

Garbage Collection

PostgreSQL: VACUUM

  • Dead tuples accumulate in heap pages
  • VACUUM scans and marks dead tuples as reusable
  • VACUUM FULL reclaims space (rewrites table)
  • autovacuum runs periodically
  • Long-running transactions prevent cleanup
  • Table bloat is a real operational concern

MySQL/InnoDB: Purge Thread

  • Old versions live in undo tablespace
  • Purge thread removes unneeded undo entries
  • Heap pages stay cleaner (only latest version)
  • Undo tablespace can grow with long transactions
  • Less bloat than PostgreSQL in practice
  • But undo purge lag causes performance issues

15 Write-Ahead Log (WAL)

The WAL is the mechanism that makes durability possible without writing every changed data page to disk on every commit. The core rule: log the change before applying it. If the system crashes, the log is replayed to reconstruct any committed changes.

Why WAL Exists

💡

The Problem

Writing modified data pages directly to disk on every transaction would be enormously slow — pages are scattered across the disk (random I/O), and a single UPDATE might modify an index page, the heap page, and TOAST pages. Instead, the database writes a compact description of the change to an append-only log file (sequential I/O — very fast), and periodically flushes dirty data pages in the background.

Write Flow with WAL

 Transaction: UPDATE accounts SET balance = 900 WHERE id = 1;
              UPDATE accounts SET balance = 1100 WHERE id = 2;
              COMMIT;

 ┌────────────────────────────────────────────────────────────┐
 │ 1. Modify pages in BUFFER POOL (in memory)                 │
 │    Page 42: account 1 balance → 900                        │
 │    Page 87: account 2 balance → 1100                       │
 │    (pages marked "dirty" but NOT written to disk yet)      │
 └───────────────────────┬────────────────────────────────────┘
                         │
                         ▼
 ┌────────────────────────────────────────────────────────────┐
 │ 2. Write WAL records (append to WAL file)                  │
 │    LSN 1001: page 42 offset 128 old=1000 new=900           │
 │    LSN 1002: page 87 offset 128 old=1000 new=1100          │
 │    LSN 1003: COMMIT transaction 500                        │
 └───────────────────────┬────────────────────────────────────┘
                         │
                         ▼
 ┌────────────────────────────────────────────────────────────┐
 │ 3. FSYNC the WAL to disk ← DURABILITY GUARANTEE           │
 │    Only after WAL is on disk does COMMIT return to client  │
 └───────────────────────┬────────────────────────────────────┘
                         │
                         ▼
 ┌────────────────────────────────────────────────────────────┐
 │ 4. Later: CHECKPOINT                                       │
 │    Background writer flushes dirty pages to data files     │
 │    Records checkpoint position in WAL                      │
 │    Old WAL segments can be recycled                        │
 └────────────────────────────────────────────────────────────┘

 CRASH RECOVERY:
 ┌────────────────────────────────────────────────────────────┐
 │ 1. Find last checkpoint in WAL                             │
 │ 2. REDO: replay all WAL records after checkpoint           │
 │    → applies committed changes not yet in data files       │
 │ 3. UNDO: roll back uncommitted transactions                │
 │    → ensures atomicity                                     │
 │ 4. Database is consistent — ready to accept connections    │
 └────────────────────────────────────────────────────────────┘

Checkpoint

A checkpoint flushes all dirty buffer pool pages to disk and records the current WAL position. After a checkpoint, the database only needs to replay WAL records after that point on crash recovery. Without checkpoints, recovery would need to replay the entire WAL from the beginning — potentially hours of work. PostgreSQL triggers checkpoints based on time interval (checkpoint_timeout, default 5 min) or WAL volume (max_wal_size, default 1 GB).

16 Replication

Replication copies data across multiple nodes for fault tolerance (survive node failures), read scaling (distribute read load), and geographic proximity (serve users from nearby datacenters).

Replication Topologies

 SINGLE-LEADER (Primary/Replica):
 ┌─────────┐    WAL / binlog stream     ┌─────────┐
 │ PRIMARY │ ──────────────────────────► │ REPLICA │
 │ (R/W)   │ ──────────────────────────► │ (Read)  │
 └─────────┘                             └─────────┘
      │                                  ┌─────────┐
      └────────────────────────────────► │ REPLICA │
                                         │ (Read)  │
                                         └─────────┘

 MULTI-LEADER:
 ┌─────────┐ ◄──────────────────────► ┌─────────┐
 │ LEADER  │    bidirectional          │ LEADER  │
 │ DC-East │    replication            │ DC-West │
 └─────────┘                           └─────────┘
      │                                      │
      ▼                                      ▼
 ┌─────────┐                           ┌─────────┐
 │ REPLICA │                           │ REPLICA │
 └─────────┘                           └─────────┘

 LEADERLESS (Dynamo-style):
 ┌────────┐  ┌────────┐  ┌────────┐  ┌────────┐  ┌────────┐
 │ Node 1 │  │ Node 2 │  │ Node 3 │  │ Node 4 │  │ Node 5 │
 └────────┘  └────────┘  └────────┘  └────────┘  └────────┘
      Client writes to W nodes, reads from R nodes
      Consistency if W + R > N (quorum)

Synchronous vs Asynchronous

Synchronous Replication

  • Primary waits for replica acknowledgment
  • Zero data loss on failover
  • Higher write latency (network round-trip)
  • Primary blocked if replica is down
  • Semi-sync: wait for 1 of N replicas

Asynchronous Replication

  • Primary doesn't wait — fire and forget
  • Low write latency
  • Possible data loss on failover (replication lag)
  • Replicas may serve stale reads
  • Most common in practice

Conflict Resolution (Multi-Leader / Leaderless)

StrategyHow It WorksTrade-off
Last Write Wins (LWW)Highest timestamp winsSimple but loses data; clock skew issues
Merge valuesUnion of concurrent updates (CRDTs)Only works for certain data types
Application-levelStore all conflicting versions, let app resolveMost flexible but complex (Riak sibling values)
Conflict avoidanceRoute all writes for a key to same leaderReduces availability; doesn't handle leader changes

17 Sharding & Partitioning

When a single node can't hold all the data or handle all the traffic, we partition (shard) the data across multiple nodes. Each node is responsible for a subset of the data.

Partitioning Strategies

 RANGE PARTITIONING:
 ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
 │  Shard 1     │ │  Shard 2     │ │  Shard 3     │
 │  A - F       │ │  G - N       │ │  O - Z       │
 │  user_ids    │ │  user_ids    │ │  user_ids    │
 │  1-1M        │ │  1M-2M       │ │  2M-3M       │
 └──────────────┘ └──────────────┘ └──────────────┘
 ✓ Range queries efficient   ✗ Hotspots if data skewed

 HASH PARTITIONING:
 shard = hash(key) % num_shards
 ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
 │  Shard 0     │ │  Shard 1     │ │  Shard 2     │
 │  hash%3 == 0 │ │  hash%3 == 1 │ │  hash%3 == 2 │
 └──────────────┘ └──────────────┘ └──────────────┘
 ✓ Even distribution          ✗ Range queries need all shards
 ✓ No hotspots                ✗ Resharding is expensive

 DIRECTORY-BASED:
 ┌────────────────────┐
 │  Lookup Service     │     key → shard mapping
 │  user_1 → shard_2  │     stored in a directory
 │  user_2 → shard_1  │     (e.g., ZooKeeper, etcd)
 │  user_3 → shard_3  │
 └────────────────────┘
 ✓ Flexible placement    ✗ Directory is single point of failure
 ✓ Easy rebalancing      ✗ Extra lookup on every request

Rebalancing

When adding or removing nodes, data must be redistributed. Strategies include:

  1. Fixed number of partitions: Create many more partitions than nodes (e.g., 1000 partitions across 10 nodes). When adding a node, move entire partitions. Used by Elasticsearch, Riak, Couchbase.
  2. Dynamic partitioning: Split partitions that grow too large, merge small ones. Used by HBase, MongoDB. Adapts to data size but more complex.
  3. Consistent hashing: Nodes are placed on a hash ring. Adding a node only moves data from adjacent nodes. Minimizes data movement. Used by Cassandra, DynamoDB.

Cross-Shard Queries

⚠

The Scatter-Gather Problem

Queries that don't include the partition key must be sent to all shards (scatter), and results must be collected and merged (gather). This is expensive and gets slower as you add more shards. This is why choosing the right partition key is critical — it should match your most common query pattern. Cross-shard joins are even worse, often requiring two-phase protocols or denormalization to avoid.

18 CAP Theorem & Consistency Models

"The CAP theorem is widely misunderstood. It does NOT say 'pick 2 of 3'. It says: during a network partition, you must choose between consistency and availability. When there's no partition, you can have both." — Martin Kleppmann, Designing Data-Intensive Applications

What CAP Actually Means

 CAP Theorem (Brewer, 2000; proven by Gilbert & Lynch, 2002):

 In a distributed system, when a NETWORK PARTITION occurs,
 you must choose between:

 ┌─────────────────────────────────┐
 │         CONSISTENCY (C)         │  Every read receives the most
 │                                 │  recent write or an error
 └────────────────┬────────────────┘
                  │
      ┌───────────┴───────────┐
      │   PARTITION (P)       │  Network partition is
      │   (inevitable)        │  not a choice — it WILL happen
      └───────────┬───────────┘
                  │
 ┌────────────────┴────────────────┐
 │         AVAILABILITY (A)        │  Every request receives a
 │                                 │  non-error response
 └─────────────────────────────────┘

 During a partition:
   CP system: refuse to serve requests (unavailable) to maintain consistency
              → e.g., HBase, MongoDB (with majority write concern), etcd
   AP system: serve potentially stale data to remain available
              → e.g., Cassandra, DynamoDB, CouchDB

 When there's NO partition (normal operation):
   You can have BOTH consistency AND availability!
   This is why "CA" is misleading — ALL systems are CA when the network works.

Consistency Models Spectrum

ModelGuaranteePerformanceUsed By
Linearizability Strongest Operations appear instantaneous; real-time ordering Slowest — requires coordination etcd, ZooKeeper, Spanner
Sequential Consistency Operations ordered consistently, but not necessarily real-time Moderate Some cache systems
Causal Consistency Causally related operations are ordered; concurrent operations may diverge Good — only tracks dependencies MongoDB (sessions), COPS
Eventual Consistency Weakest If no new updates, all replicas eventually converge Fastest — no coordination Cassandra, DynamoDB (default), DNS
🔍

Beyond CAP: PACELC

CAP only describes behavior during partitions. The PACELC framework extends it: "If Partition, choose Availability or Consistency; Else (normal operation), choose Latency or Consistency." This captures the fact that even without partitions, there's a fundamental trade-off between response time and consistency — synchronous replication is consistent but slow; async replication is fast but eventually consistent.

19 Consensus Algorithms

Consensus is the problem of getting multiple nodes to agree on a value despite failures. It is the foundation of fault-tolerant distributed systems — used for leader election, distributed locking, and replicated state machines.

Raft — Simplified Consensus

 RAFT: Three Roles
 ┌──────────┐    ┌──────────┐    ┌──────────┐
 │ LEADER   │    │ FOLLOWER │    │ FOLLOWER │
 │ (1 only) │───►│          │    │          │
 │ Handles  │───►│ Replicate│    │ Replicate│
 │ all      │    │ entries  │    │ entries  │
 │ writes   │───►│          │◄───│          │
 └──────────┘    └──────────┘    └──────────┘

 LEADER ELECTION (term-based):
 ┌──────────────────────────────────────────────────────────┐
 │ 1. Leader sends heartbeats every ~150ms                  │
 │ 2. If a follower doesn't hear from leader for            │
 │    election timeout (150-300ms), it becomes CANDIDATE    │
 │ 3. Candidate increments term, votes for itself           │
 │ 4. Requests votes from all other nodes                   │
 │ 5. Wins if gets majority (N/2 + 1) votes                │
 │ 6. New leader starts sending heartbeats                  │
 └──────────────────────────────────────────────────────────┘

 LOG REPLICATION:
 Client ──► Leader
                │
                ├──► Append to local log
                ├──► Send AppendEntries RPC to all followers
                ├──► Wait for majority acknowledgment
                ├──► COMMIT entry (apply to state machine)
                └──► Respond to client

 Leader Log:    [1:set x=1] [2:set y=2] [3:set x=3] committed
 Follower A:   [1:set x=1] [2:set y=2] [3:set x=3] committed
 Follower B:   [1:set x=1] [2:set y=2]             catching up...
                                         ▲
                                 majority (2/3) = committed

Split Brain Prevention

 NETWORK PARTITION:
 ┌─────────────────────┐        ┌─────────────────────┐
 │  Partition A         │   ✗   │  Partition B         │
 │  ┌────────┐          │  ███  │          ┌────────┐  │
 │  │Leader  │          │  ███  │          │Follower│  │
 │  │(old)   │          │  ███  │          │        │  │
 │  │        │          │  ███  │          └────────┘  │
 │  └────────┘          │  ███  │          ┌────────┐  │
 │  ┌────────┐          │  ███  │          │Follower│  │
 │  │Follower│          │  ███  │          │        │  │
 │  └────────┘          │       │          └────────┘  │
 └─────────────────────┘        └─────────────────────┘
   2 nodes — can't get             3 nodes — CAN elect
   majority (need 3/5)             new leader (3/5 = majority)
   Old leader can't commit!        New leader takes over.

 → Only ONE partition can have a majority → no split brain
 → This is why clusters use odd numbers (3, 5, 7 nodes)

Paxos vs Raft

Paxos

  • Original consensus algorithm (Lamport, 1989)
  • Provably correct, minimal assumptions
  • Notoriously difficult to understand/implement
  • Multi-Paxos for log replication
  • Used by: Google Chubby, Spanner

Raft

  • Designed for understandability (Ongaro, 2014)
  • Equivalent safety guarantees to Paxos
  • Clear leader election + log replication
  • Easier to implement correctly
  • Used by: etcd, CockroachDB, TiKV, Consul

20 PostgreSQL Internals

PostgreSQL is an excellent case study because it's open-source, well-documented, and contains a full-featured implementation of nearly every concept discussed in this guide.

Architecture Overview

 ┌─────────────────────────────────────────────────────────────┐
 │                    PostgreSQL Architecture                   │
 ├─────────────────────────────────────────────────────────────┤
 │                                                             │
 │  Client ──► Postmaster (listener, PID 1)                    │
 │                │                                            │
 │                ├──► Backend Process 1  (one per connection)  │
 │                ├──► Backend Process 2                        │
 │                └──► Backend Process 3                        │
 │                                                             │
 │  Background Processes:                                      │
 │  ┌──────────────┐ ┌──────────────┐ ┌─────────────────────┐ │
 │  │ WAL Writer   │ │ BG Writer    │ │ Autovacuum Launcher │ │
 │  │ (flush WAL)  │ │ (dirty pages)│ │ → Autovacuum Workers│ │
 │  └──────────────┘ └──────────────┘ └─────────────────────┘ │
 │  ┌──────────────┐ ┌──────────────┐ ┌─────────────────────┐ │
 │  │ Checkpointer │ │ Stats        │ │ Logical Replication │ │
 │  │              │ │ Collector    │ │ Workers             │ │
 │  └──────────────┘ └──────────────┘ └─────────────────────┘ │
 │                                                             │
 │  Shared Memory:                                             │
 │  ┌─────────────────────────────────────────────────────┐   │
 │  │ Shared Buffers (default 128MB, tune to 25% of RAM)  │   │
 │  │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐      │   │
 │  │ │Page 1│ │Page 2│ │Page 3│ │ ...  │ │Page N│      │   │
 │  │ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘      │   │
 │  ├─────────────────────────────────────────────────────┤   │
 │  │ WAL Buffers │ Lock Table │ CLOG │ Proc Array      │   │
 │  └─────────────────────────────────────────────────────┘   │
 │                                                             │
 │  On Disk:                                                   │
 │  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐  │
 │  │ Data Dir │ │ WAL Dir  │ │ CLOG     │ │ pg_stat_tmp  │  │
 │  │ (base/)  │ │ (pg_wal/)│ │(pg_xact/)│ │              │  │
 │  └──────────┘ └──────────┘ └──────────┘ └──────────────┘  │
 └─────────────────────────────────────────────────────────────┘

Process Model

PostgreSQL uses a process-per-connection model (not threads). Each client connection gets a dedicated backend process via fork(). This provides excellent isolation (a crash in one backend doesn't affect others) but higher memory overhead compared to thread-per-connection (MySQL) or async I/O (Node.js-style) models. Connection poolers like PgBouncer or pgcat are essential for high-connection workloads.

Key Subsystems

Shared Buffers Buffer Pool

PostgreSQL's buffer cache for data pages. Default 128 MB — typically tuned to ~25% of system RAM. Uses a clock-sweep algorithm for page eviction (similar to LRU but more efficient). Each 8 KB page has a pin count and usage count.

WAL (pg_wal/) Durability

16 MB segment files. WAL records are written by backends, flushed by the WAL writer or at commit time (synchronous_commit). Supports streaming replication to replicas. WAL is also the basis for Point-in-Time Recovery (PITR).

MVCC & CLOG Concurrency

Transaction status is tracked in the Commit Log (pg_xact/). Each transaction gets a 32-bit XID. When checking visibility, the backend looks up the XID's status (in-progress, committed, aborted) in the CLOG, cached in shared memory.

VACUUM Maintenance

Removes dead tuples left by MVCC. Also updates the visibility map (tracks all-visible pages for index-only scans) and free space map (tracks available space). Autovacuum triggers based on tuple change thresholds. VACUUM FREEZE prevents XID wraparound — critical for long-lived databases.

TOAST Large Values

The Oversized-Attribute Storage Technique. When a row exceeds ~2 KB (1/4 of 8 KB page), large column values are compressed and/or moved to a separate TOAST table. The main row stores a pointer to the TOAST data. Transparent to the application — you never interact with TOAST directly.

Statistics (pg_statistic) Optimizer

ANALYZE collects statistics: row count, distinct values per column, most common values, histogram of value distribution, correlation (physical vs logical ordering). The query planner depends entirely on these statistics for cost estimation.

21 Database Comparison

Each database makes different architectural trade-offs. Understanding these trade-offs helps you choose the right tool for your workload.

Feature PostgreSQL MySQL/InnoDB SQLite MongoDB Redis Cassandra
Type Relational Relational Relational (embedded) Document Key-Value / Data Structure Wide-Column
Storage Engine Custom (heap + B-Tree) InnoDB (B+ Tree) B-Tree (single file) WiredTiger (B-Tree + LSM) In-memory + optional AOF/RDB LSM Tree (SSTables)
Concurrency MVCC (in-heap versions) MVCC (undo log) WAL mode: readers don't block writers Document-level locking + MVCC Single-threaded event loop No locks (eventual consistency)
Process Model Process-per-connection Thread-per-connection (thread pool in Enterprise) In-process library Thread pool Single-threaded (+ I/O threads in 6.0+) Thread-per-core (SEDA)
Replication Streaming (WAL-based) + logical Binlog (statement/row/mixed) Litestream / LiteFS (community) Replica set (Raft-like) Async primary-replica Leaderless (Dynamo-style)
Sharding Citus extension / app-level Vitess / ProxySQL / app-level N/A (embedded) Built-in (auto-sharding) Redis Cluster (hash slots) Built-in (consistent hashing)
ACID Full ACID Full ACID Full ACID Multi-doc txns (4.0+) Lua scripts only Eventual (tunable)
Best For Complex queries, extensions, correctness Web apps, read-heavy, mature ecosystem Embedded, mobile, edge, testing Flexible schema, rapid iteration Caching, sessions, real-time leaderboards High write throughput, time-series, geographic distribution
Weakness VACUUM overhead, connection model Fewer advanced features, optimizer less sophisticated Single writer, no network access Joins, complex transactions Data must fit in memory, limited queries No joins, no ad-hoc queries, read-before-write expensive

Architecture Diagrams

PostgreSQL Process-based

Multi-process, shared-nothing between backends (except shared buffers). Each backend has its own memory context for sorts, hash tables, etc. Extensible: custom types, operators, index methods, procedural languages, foreign data wrappers.

MySQL/InnoDB Pluggable engines

Separation between SQL layer and storage engine. InnoDB: clustered index (primary key IS the table), change buffer for secondary indexes, doublewrite buffer for torn-page protection, adaptive hash index.

SQLite Embedded

Entire database in a single file. No server process — it's a library linked into your application. Uses a single B-Tree per table (rowid-based) and per index. WAL mode enables concurrent readers. Remarkably, it's the most deployed database in the world (every phone, browser, OS).

MongoDB Document store

BSON documents stored in WiredTiger. Each collection can have B-Tree or LSM indexes. Replica sets use a Raft-inspired election protocol. Auto-sharding with configurable shard keys. Aggregation pipeline for analytical queries.

Redis In-memory

All data in memory with optional disk persistence (RDB snapshots + AOF log). Rich data structures: strings, hashes, lists, sets, sorted sets, streams, HyperLogLog. Single-threaded command execution ensures atomicity without locks. Sub-millisecond latency.

Cassandra Wide-column

LSM tree storage, leaderless replication, tunable consistency (ONE, QUORUM, ALL). Partition key determines node placement. Clustering columns determine sort order within a partition. Designed for massive write throughput across multiple datacenters.

"There is no 'best' database — only the best database for your specific workload, consistency requirements, operational capacity, and scale. The engineering art is in understanding the trade-offs deeply enough to make an informed choice." — Adapted from Designing Data-Intensive Applications by Martin Kleppmann