
Take a step back. Over twelve parts you assembled a stack, each layer solving the problem the layer above created:
┌──────────────────────────────────────────────────────────┐
│ QUERY (Part 5) you say "what," not "how" │
├──────────────────────────────────────────────────────────┤
│ INDEX (Part 6-9) hash for equality, B+ tree for range│
│ index stores TID, not the row │
├──────────────────────────────────────────────────────────┤
│ PAGE (Part 10-11) read/write by page, directory inside│
│ TID = (page, slot) │
├──────────────────────────────────────────────────────────┤
│ DISK + BUFFER (Part 10,12) pages on disk, cached in memory │
└──────────────────────────────────────────────────────────┘Now glue them into a single object. We'll keep it deliberately small but honest — every piece is the one you wrote:
class MiniDatabase {
constructor(file) {
this.file = file;
this.pages = new Map(); // pageId → page (the heap)
this.byId = new BPlusTree(); // id → TID (the index)
this.pool = new LruPool(16); // the buffer pool
}
insert(row) {
const page = this.getOrCreatePage(row);
const slot = appendRow(page, encode(row));
const tid = { page: page.header.pageId, slot };
this.byId.insert(row.id, tid); // index points at the address
}
findById(id) {
const tid = this.byId.search(id); // 1. walk the B+ tree
const page = this.pool.getPage(this.file, tid.page); // 2. cache-aware read
const bytes = readRow(page, tid.slot); // 3. inside the page
return decode(bytes);
}
}Read findById slowly, because it's the whole adventure in one line: index → TID → buffer pool → page → directory → row. Each step is a concept you built yourself.
Your MiniDatabase above is a heap table: rows live in pages (the heap), and the index points at TIDs. But Part 9 showed you there's a different choice: sort the rows inside the index itself.
HEAP TABLE (what you built, like PostgreSQL):
index: id 42 → TID {page, slot} jump to a page via the index
heap: rows in pages, unsorted the actual data lives here
INDEX-ORGANIZED (like MySQL InnoDB):
index (primary): the leaf IS the row data sorted inside the index
no separate heap the index IS the tableObserve what that tiny difference forces:
DB_ROW_ID.Now you can answer it with engineering, not memorization:
PostgreSQL's rows live in a heap and are only reached by an index, never arranged by one. An index is a map; the heap is the territory. If you remove the index (and its uniqueness constraint), the territory is untouched — rows stay in place, insertion order preserved. So PostgreSQL happily stores a table with no primary key.
InnoDB uses an index-organized table — the rowsaresorted inside the primary-key index. The primary key is not an add-on; it's the physical layout. So a primary key is mandatory — and if you don't give one, InnoDB fabricates a hidden one just to have a layout.
In short: PostgreSQL can have no primary key because its storage engine doesn't need one to arrange data; InnoDB does. You didn't memorize that — you derived it, from building the machine.
And the practical upside falls out too: because InnoDB sorts by the primary key, a random or ever-changing key makes inserts expensive (rows must shuffle → page splits). PostgreSQL is immune — the heap doesn't care what your key is.
You've built a toy, but every big name you've met — PostgreSQL, MySQL, even ClickHouse — is just this machine scaled up and hardened:
shared_buffers are exactly our PAGE_SIZE and LruPool.You've been building the skeleton of every real database from the ground up.
findById end-to-end: build the MiniDatabase, insert a few rows, then findById(2), and log each hop — index hit, buffer hit, page directory, row decode. See the whole chain fire.title too, so findByTitle(key) walks its own B+ tree to a TID. Notice the heap is shared — indexes are just maps over the same rows.Seq Scan them. Confirm it stores and retrieves — just without fast keyed lookup.Keywords: storage engine、heap table、index-organized table、primary key、B+ tree、TID、buffer pool、page、from zero
Congratulations — you built a database. Not a million-line production system, but the actual skeleton of one. Every real database you'll ever touch is this machine, scaled and hardened.
You now understand, by construction, the things most people only memorize: why Seq Scan is expensive, why the second query is faster, why indexes make lookups jump, and why tables can or can't go without a primary key.
That's the end of this series. If you want a refresher on the whole path, or to see how all the parts fit together again, head back to the series guide: