Foundations of Storage, Pages, and the Buffer Pool
Foundations of Storage, Pages, and the Buffer Pool
A modern database spends more time deciding what *not* to read from disk than it does executing queries. The central question this chapter answers is direct: if a single disk access costs microseconds and a single CPU instruction costs nanoseconds, how does any database appear to be fast? The answer is not magic—it is a deliberately engineered abstraction called the *page*, sitting on top of an even more deliberately engineered cache called the *buffer pool*. Everything else in this book—transactional logging, B-tree layout, hash joins, vacuum, write-ahead logging—depends on the contracts established in this chapter. If you understand what the buffer pool promises, what it cannot promise, and what it costs you when it fails, you will understand why the rest of the chapters look the way they do.
This chapter is a guided tour from raw storage devices up to the buffer pool API. We start by quantifying the latency gap, because without numbers, every caching decision looks like over-engineering. We then introduce the *page* as the universal unit of transfer between memory and disk, dissect its anatomy, and discuss alignment constraints that no textbook mentions but every real-world system trips over. Next, we move into the buffer pool itself: its hash-table page table, its replacement policies, and the precise moment a page is considered pinned, dirty, or evictable. We analyze LRU, LRU-K, 2Q, and Clock-Pro, because these four policies span the design space from naive to production-grade. We close with prefetching, the secret reason a sequential scan is not the disaster it should be. A worked example in Go implements a small buffer pool manager and benchmarks it against a naïve `mmap` loop—the result is not what beginners expect.
1.1 The Latency Gap and the Economics of Caching
To justify a buffer pool, we first need to feel the latency gap in our hands. The classic numbers are worth memorizing not as trivia, but as the calibration scale for every architectural decision in this book.
| Layer | Typical Latency | Bandwidth | Byte-Addressable? | |-------|-----------------|-----------|-------------------| | L1 cache | ~1 ns | TB/s | Yes | | L2 cache | ~4 ns | TB/s | Yes | | L3 cache | ~10–30 ns | hundreds of GB/s | Yes | | DRAM | ~80–120 ns | 50–100 GB/s | Yes | | NVMe SSD (3D NAND) | 25–100 μs | 3–7 GB/s | No | | SATA/SAS SSD | 100–500 μs | 0.5 GB/s | No | | 3D XPoint / Optane | 8–10 μs | 1–2 GB/s | Yes (limited) | | Rotational HDD | 5–15 ms | 0.1–0.2 GB/s | No | | Remote NVMe (RDMA) | 200 μs – 1 ms | 5–10 GB/s | No | | Cross-continental network | 50–200 ms | variable | No |
The headline number: a single rotational disk access is roughly 100,000 times slower than a DRAM access, and roughly 10,000 times slower than an L3 cache access. Even comparing DRAM to NVMe SSD, the gap is roughly 250× to 1,000×. This means that a single page miss on an SSD costs the same time budget as millions of instructions.
This is not a small imbalance. It is the central architectural pressure that shaped every modern database. The rest of this book can be read as the consequence of one engineering response: *do not let the disk be the critical path*. Caching is the obvious first answer, but caching has a price—memory, consistency, replacement overhead, and serialization on shared structures. The buffer pool is the carefully designed boundary between "fast" and "slow," and like every well-designed boundary, it has a contract. The contract is: *if you ask for a page, you will either get a freshly loaded page from disk and pay the cost, or you will get a hot page already in DRAM and pay nothing. You will not get a partial page. You will not get a page in an inconsistent state. You will not get a page that another transaction is mid-write on.* Everything else in the chapter explains how this contract is implemented.
A second observation that frames every caching decision: the cost of a cache miss is asymmetric across layers. Hitting L2 costs essentially nothing; missing DRAM to read from SSD costs 100–1000× the DRAM hit. Therefore, designing a buffer pool is mostly about ensuring the working set fits in DRAM. If it does, your buffer pool will absorb 99%+ of accesses. If it does not, your database will spend most of its time waiting for the device. The role of replacement policies is therefore not "make caching work"—caching works if the working set fits—but "decide what to evict under memory pressure when working sets do not fit cleanly." This subtle reframe is why a deep treatment of replacement policies matters: they are the system you need *only when the easy case fails*, which means they are the system you need precisely when your database is on fire.
Before moving on, two more ideas deserve emphasis because they recur throughout the chapter. First, *page size is a knob, not a constant*. A 4 KB page is good for OLTP because it minimizes wasted I/O when touching a single row. A 64 KB or 256 KB page is good for OLAP because it minimizes per-page overhead and amortizes device latency over more useful bytes. Modern systems support multiple page sizes (PostgreSQL's 8 KB default versus its 32 KB TOAST pages, RocksDB's block sizes configurable from 4 KB to 4 MB, DuckDB's 256 KB defaults). Second, *the buffer pool does not own the entire memory story*. The OS page cache, the kernel slab allocator, the JIT compiler's code cache, the connection state, and the sort/hash buffers all compete with the buffer pool. A production database often pins its buffer pool to specific NUMA nodes, uses huge pages to reduce TLB pressure, and uses `mlock` or `MAP_POPULATE` to prevent the buffer pool from being paged out by the kernel. These are not exotic optimizations—they are the difference between a database that hits its SLO and one that does not.
1.2 The Page as the Universal Unit of Transfer
Every database in this book speaks *pages*. Pages are not a database invention; they are an inevitable response to the architecture of storage devices. A page is the smallest unit the device is willing to read or write atomically (or close to it), the smallest unit the OS will hand you via `read()` without surprise, and the smallest unit that aligns with SSD erase blocks and OS virtual memory pages. The classic page size is 4 KB or 8 KB; newer systems trend larger.
The page as an abstraction has three properties worth naming explicitly. First, *a page is the atomic transfer unit*. When the buffer pool reads a page, it reads the entire page, even if the caller wanted only one tuple. This is wasteful for tiny reads but immensely simpler for everything else: it makes checksums atomic, makes write-ahead logging tractable, and makes prefetching coherent. Second, *a page is addressed independently of its content*. The database does not ask "give me the row with id 42"; it asks "give me page 17 of file 5," and a higher layer (the access method) figures out which slot within page 17 contains row 42. Third, *a page has a stable on-disk identity* that survives crashes. Even after a power loss, the page header must be parseable, the checksum must be verifiable, and the slot directory must be walkable. The page is the unit of crash recovery as much as it is the unit of transfer.
Anatomy of a Page
A page is not a flat buffer of bytes. It has internal structure, and that structure encodes invariants the rest of the database depends on. A typical page looks like this:
``` +-----------------------------------+ | Page Header (32-64 bytes) | | - LSN (8 bytes) | | - Checksum (4-8 bytes) | | - Page type / flags (1-2 bytes) | | - Slot count (2 bytes) | | - Free space pointer (2 bytes) | | - Lower fence (2 bytes) | | - Upper fence (2 bytes) | | - Special area length (2 bytes) | +-----------------------------------+ | Slot Directory (grows down) | | - Each slot: