- Explain why database storage is organized around pages, indexes, and disk-aware data structures.
- Describe how B-Trees remain balanced and support logarithmic lookup.
- Compare fixed-width records with variable-length records stored in slotted pages.
- Distinguish heap-based table storage from index-organized storage.
- Trace SQL from tokenization through an iterator-based query plan.
- Explain external sorting, page caching, journaling, and network access in a toy database.
- Basic familiarity with files, bytes, and RAM.
- A general understanding of arrays, trees, and primary keys.
- Basic knowledge of SQL statements.
Begin with the On-Disk Layout
Start with a simple table file, then identify why fast primary-key access requires an index.
Databases support web, mobile, and enterprise software, but their internal storage is often hidden from application developers. Building a small engine reveals the reasons behind its design, from byte-level records to balanced B-Trees.
In the MySQL example, creating a database produces a folder, and its storage engine creates a file for a table. That file holds the rows and a primary-key index. The index uses a disk-persisted structure such as a B-Tree or B+Tree so that keys can be found without checking every row.
Try Equal-Sized Records First
Consider a users table containing a unique integeridand a variable-lengthname. CSV delimiters would make direct movement between rows impossible because the file must be scanned byte by byte to locate each separator.
An initial improvement is to assign every column a fixed width. If an ASCII name may contain at most 255 characters, one byte records its actual length because eight bits represent values through 255, while another 255 bytes reserve the character data.
- The integer
idoccupies 4 bytes. - The stored name length occupies 1 byte.
- The reserved name area occupies 255 bytes.
- Each complete record therefore occupies 260 bytes.
To locateid = 50, read a four-byte key and, after a mismatch, move to the next record, 260 bytes from the current record’s start. This linear search costs . It may work for a small file, but repeated disk access makes it unsuitable for a table with many rows.
See Why an Index Is Necessary
A primary-key index reduces lookup to . A balanced binary search tree could map each key to its row number, allowing the engine to calculate the row's byte position because every record has equal size. However, an ordinary binary tree presents two database problems.
- Ascending insertions can turn an unbalanced tree into a linked list, restoring the worst case.
- A persistent tree may be gigabytes large and unable to fit in RAM, so the engine must retrieve it from disk in pieces.
Use a B-Tree for the Index
A B-Tree places several ordered keys in each node and restores balance by promoting median keys.
Like a binary search tree, a B-Tree orders smaller keys to the left and larger keys to the right. Unlike a binary tree, one node may contain several keys and more than two child pointers. Let C represent the minimum child count used to derive the node limits.
- Maximum keys in one node: 2C - 1.
- Minimum keys in a non-root internal node: C - 1.
- Maximum children of one node: 2C.
With C = 2, a node can contain no more than three keys and can point to at most four children. These constraints support self-balancing behavior, so insertion order does not destroy search.
Insert Keys by Splitting Full Nodes
When the target node is full, its median key moves into the parent and the remaining keys separate into two sibling nodes. A full root has no parent, so the operation first creates a new root. Splits may propagate upward recursively as the tree becomes taller.
Repeating this process keeps the tree dense and balanced rather than allowing it to collapse into a linear chain.
Arrange the Tree in Disk Pages
Map one B-Tree node to a page, then use slot arrays to manage records whose lengths differ.
Disk reads operate on fixed-size blocks, such as 512 or 4,096 bytes. A database can define a page as one or more blocks; the toy engine commonly uses 4 KiB pages. Its file is a numbered sequence of pages, and one B-Tree node occupies one page.
A child pointer is the child's page number. Multiplying that number by the page size gives the file offset. The engine can therefore load one required node per disk operation without placing a multi-gigabyte index entirely in memory.
Fit Variable-Length Cells into a Page
Reserving 255 bytes for every short name wastes space. With variable-length records, however, row numbers no longer reveal record positions, and moving large records to preserve sorted order is expensive. A slotted page separates compact offsets from the record contents.
- A slot array grows rightward from the page's beginning. Each entry is a two-byte offset to a record.
- Variable-length cells grow leftward from the page's end.
- The page becomes full when the slot array and cell area meet.
A new cell is placed in the cell area and receives a new slot entry. Binary search operates over the fixed-size slot array. To maintain key order, the engine shifts two-byte slot entries instead of moving all the variable-length record bytes.
Each B-Tree cell also carries a child page number. The keys reached through that pointer fall in the ordered range determined by the cell's key and the preceding key.
Choose Where Table Rows Live
Heap-based storage separates rows and indexes, while index-organized storage makes the table itself a primary-key B-Tree.
Two principal layouts connect table rows with their indexes.
- Heap-based storage, represented by PostgreSQL in the lesson, keeps rows in a separate slotted-page table file without requiring primary-key order. An external B-Tree maps keys to page-and-slot locations, while a page directory tracks available space. This produces three disk structures: index, page directory, and table file.
- Index-organized storage, represented by SQLite, stores the table as a primary-key-sorted B-Tree. Every B-Tree cell is a full row, allowing the table and indexes to share the same tree implementation.
In an index-organized table, balancing can move a row. A secondary index such as an email index must therefore store the associated primary key rather than a physical row address. An email lookup performs one B-Tree search in the secondary index and another in the table. A primary-key query goes straight to the table B-Tree and needs only one search.
Continue Oversized Rows onto Overflow Pages
A row larger than one page, such as a long blog post, must be divided. Once it crosses a chosen threshold—for example, one quarter of the page size—the first chunk remains in the B-Tree cell. The cell's final four bytes point to an overflow page, whose header points to the next overflow page. These pages form a linked list, and reading the row requires joining the chunks again.
Turn SQL Text into Executable Work
Convert an SQL string into a checked and optimized AST, then compile it into a tuple-producing plan tree.
Once storage is available, the engine still must translate an SQL statement into operations over its on-disk structures.
Build and Refine the Syntax Tree
- The tokenizer converts characters into keywords such as
SELECTandFROM, along with identifiers, operators, literals, and other token types. - The parser turns those tokens into an Abstract Syntax Tree (AST) describing the statement's structure. Top-Down Operator Precedence (TDOP) parsing is one way to build expression trees.
- The analyzer performs context-dependent checks, including whether a table exists and whether a value has the right type for its column.
- The optimizer simplifies work that would otherwise be repeatedly evaluated by the interpreter, including constant computations.
Execute an Iterator Plan
The completed AST is compiled into a query-plan tree. Its nodes are iterators that produce tuples, the relational term for rows, one at a time.
- A
SequentialScanreads table rows and sends them upward. - A
Filtertests each tuple against a condition. - A
Projectionkeeps the requested columns.
When the root plan needs another tuple, it invokes.next()on its child, which repeats the request down the tree until the scanner advances its cursor. Only the current tuple and cursor state are required, allowing very large tables to flow through the plan without filling RAM.
Sort Data That Exceeds Memory
If all tuples fit in an in-memory buffer, the engine can sort the array directly. Larger results require K-way external merge sort over temporary files.
- During pass 0, load K pages, sort them in memory, and write each sorted run to a temporary file.
- On later passes, merge K runs into a larger run with K input buffers and one output buffer. Every pass expands the run size by a factor of K.
For K = 5, successive passes handle five times more data. Increasing K lowers the number of passes and disk I/O while consuming more RAM. The method can sort terabytes while holding only a small number of pages in memory.
Keep Active Pages in a Clock Cache
Use reference and dirty bits to decide which cached page to replace and whether it must first be written to disk.
A page cache reduces repeated disk reads. The clock replacement policy arranges cached page slots in a circle and associates two bits with every slot.
- The circular array contains the cached pages, each with a reference bit and a dirty bit.
- Reading a page sets its reference bit to 1. Modifying it sets both bits to 1.
- The clock hand clears a reference bit of 1 and advances. It evicts the first page it finds with reference 0, writing that page to disk beforehand if its dirty bit is set.
This policy approximates least-recently-used replacement without recording access timestamps.
Protect Transactions with a Journal
Preserve original pages before modification so a transaction can either commit completely or be rolled back.
Atomicity means that a transaction saves all its changes on commit or leaves none of them after rollback. A journal file supplies a straightforward implementation for a single-writer toy engine.
- Copy each page's original bytes into the journal before changing that page.
- Apply modifications in the cache, from which dirty pages may later reach the database file.
- To commit, remove the journal file.
- To roll back, restore the original pages from the journal to the database file.
The same restoration supports crash recovery when a client never reaches commit. Production systems instead use a Write-Ahead Log (WAL), which records transaction changes and permits multiple concurrent writers with isolation. The toy implementation allows only one transaction at a time.
Expose the Engine through a Protocol
A TCP interface lets clients in different languages communicate with the database through an agreed network format.
The database runs as a TCP server and accepts commands through a custom protocol inspired by the simple Redis protocol. Any language can provide a client as long as it follows the same wire format.
Why does a B-Tree place several keys in each node, and how does that arrangement lower the number of disk reads required for a lookup?
A node can match the size of a disk page and hold many ordered keys plus child page numbers. Each page read therefore examines more keys and chooses among more subtrees, keeping the tree shallow and reducing the nodes that must be loaded.
Common misconceptions
Watch out: A serious database can efficiently reserve the maximum string length for every value.
Instead: Fixed-width strings make offsets simple, but reserving 255 bytes for every short value wastes space. Production-oriented storage allocates variable-length strings according to the bytes they need.
Watch out: Any binary search tree guarantees lookup.
Instead: An ordinary binary tree may become a linked list after ordered insertions, producing search. The logarithmic guarantee depends on keeping the tree balanced.
Lesson recap
A small database still requires several cooperating systems.
- Represent the database file as a sequence of fixed-size pages.
- Combine B-Trees with slotted pages to support indexes and variable-length table rows.
- Parse SQL into an AST and compile it into an iterator-based execution plan.
- Use external merge sort when the data set cannot fit in RAM.
- Reduce disk access with a page cache governed by the clock algorithm.
- Provide atomic commit and rollback by preserving original pages in a journal.
- Serve clients over TCP through a defined network protocol.
Together, these components respond to three persistent constraints: disk access is slow, memory has limits, and stored information must survive failures. Their interaction—from page bytes and slot offsets to recursive B-Tree balancing—shows why database engines are demanding systems programs.