There's No Magic in Your Database
You type SELECT and rows come back. In between is a stack of twelve layers, each solving one very sensible problem. Here's the whole journey, following a single query from your terminal to the disk and back.
You run a query:
SELECT name FROM users WHERE id = 42;
A few milliseconds later, a name comes back. Somewhere in between, a lot happened — and if you’re like most engineers who use databases every day, that “in between” is a black box. It’s just magic behind the scenes.
I want to kill that feeling, because it’s holding you back from a mental model you can actually reason about. So here’s the reframe, and it’s the most important sentence in this whole post:
A database is just a normal program reading and writing files.
Your table users is a file on disk — literally, a file in a folder. When you
query it, the database opens that file, reads some bytes, and works out which
bytes are the row you asked for. That’s the entire trick. Everything else —
every layer we’re about to walk through — exists to make that reading fast,
safe, and answerable in SQL instead of raw byte offsets.
We’ll follow that one query, id = 42, the whole way down and back. Keep it in
your head.
The query has two jobs
Every query splits cleanly into two halves, and this split is the spine of everything that follows:
- The top half — figure out what to do. The database receives your text, understands it, and decides on a plan. This is the brain. Nothing has been fetched yet; it’s all thinking.
- The bottom half — go get the bytes. The plan runs, which means descending to where the data physically lives and pulling row 42 back up.
Let’s take the brain first, because that’s where your query lands.
The brain: turning text into a plan
It arrives — the client protocol
Before anything, your query has to physically get to the database. Postgres is a separate process, often on a separate machine. So your client opens a socket and speaks an agreed-upon language over it — the wire protocol — sending a message that means “here’s a query, this many bytes, here’s the text.”
Without this layer: the database is a sealed box with no door.
It gets understood — the parser
Now the database has your query, but only as text — a string of characters.
It doesn’t yet know that SELECT means “read” or that users is a table. The
parser turns that flat string into a structured tree: this is a SELECT, the
target column is name, the table is users, there’s a filter id = 42.
It’s the same move a compiler makes parsing source code before it can run it.
Without this layer: code can’t act on an ambiguous, unstructured string.
It gets a strategy — the planner
Here’s the layer most people don’t know exists. There’s more than one way to
answer your query. To find id = 42, the database could:
- read every row in
usersand check each one (a sequential scan — slow), or - use an index on
idto jump almost straight to it (an index scan — fast).
The planner weighs what’s available and picks the cheapest path, producing a step-by-step plan. This single decision is the difference between a query returning in one millisecond and one returning in ten seconds.
Without this layer: the database does the naive thing every time and falls over at scale.
It runs — the executor
The plan is just a recipe. The executor actually cooks it: “open an index
scan on id, seek to 42, fetch that row, pull out the name column, hand it
back.” It’s the layer that turns inert instructions into real actions against
the storage below.
Without this layer: you have a plan and no one to carry it out.
That’s the whole brain. Your query has been received, understood, strategized, and the executor is now reaching down into storage to actually fetch row 42. Notice that no table data has been touched yet. That reach-down is where the second half begins.
The storage: going to get the bytes
A quick aside — how does it even know what users is?
How did the planner know there’s an index on id? How does the executor know
users has a name column that holds text? The database stores facts about
itself in the catalog — and here’s the lovely part: the catalog is itself
just tables. A table listing your tables. A table listing your columns. A table
listing your indexes. The database describes itself using its own storage
machinery, then bootstraps on top of it.
Finding it fast — the index (a B-tree)
The executor wants id = 42. The dumb way: read all ten million rows and check
each one. The smart way: an index — a separate, sorted, tree-shaped structure
that lets you find 42 by walking a handful of branches instead of scanning
everything.
It’s the index at the back of a textbook. You don’t read all 900 pages to find
“mitochondria”; you flip to the M’s. The B-tree answers exactly one question:
for id = 42, which page holds the row, and where in that page?
Where rows actually live — the heap
The index told us where. The row itself lives in the heap — the table’s real data, organized into fixed-size pages of 8KB each. Inside a page, individual rows are tuples, and a small directory at the top of the page records the exact byte offset of each one.
Fixed-size pages are the unit everything else is built on. Remember that number, 8KB — it shows up everywhere.
Skipping the disk — the buffer pool
We know row 42 is in, say, page 17. But reading from disk is slow — orders of magnitude slower than RAM — and popular pages get read over and over. So the database keeps a big pool of memory that caches recently-used pages. Before touching disk, the executor asks the buffer pool: “got page 17?”
- Yes → handed over instantly, no disk touched.
- No → go to disk, read it, and keep a copy for next time (evicting some cold page if the pool is full).
A hot table effectively lives in RAM, and repeated queries never pay the disk cost.
The floor — disk I/O
If the pool didn’t have page 17, we finally hit bottom. The disk manager opens the file, computes where page 17 sits (17 × 8KB = byte offset 139,264), and reads exactly 8KB from that spot.
This is where “page 17” stops being an abstraction and becomes a real read from a real file.
And back up
Now it all reverses. The 8KB page rises from disk → into the buffer pool
(cached for next time) → the heap layer finds tuple 42 inside it → the executor
extracts the name column → hands it up through the plan → the protocol layer
packages it → and it travels back over the socket to your terminal.
Query answered. That’s the full descent and return — one round trip.
The three layers that wrap everything
So far we only read. The moment you write, three more layers wake up. Change the query:
UPDATE users SET name = 'Ada' WHERE id = 42;
Transactions and MVCC. What if someone reads row 42 while you’re
mid-update? What if your change is half-done? What if you ROLLBACK? A
transaction is the promise that a group of changes happens all-or-nothing, and
that concurrent users never see each other’s half-finished work. Postgres pulls
this off with MVCC — it keeps multiple versions of a row and shows each
transaction the version that was correct when it started. Readers never block
writers; writers never block readers.
Write-ahead logging (WAL). Your update modified page 17 — but that page is sitting in the buffer pool, in memory, not yet on disk. The power dies. The change is gone, even though you told the user “committed.” That’s a lie a database can’t tell. The fix: before changing anything, append a tiny note to a separate, append-only log on disk — “about to set row 42’s name to Ada.” Only once that note is durable do you report success. On restart, replay the log and redo anything that didn’t make it. It’s counterintuitive — to make writes safer, you write more — but the log write is a fast sequential append, and it’s the price of “committed” actually meaning committed.
Replication. One machine is a single point of failure. Replication keeps copies of the database on other machines — and the mechanism falls right out of WAL: the primary streams that same log to the replicas, and each one replays it to stay in sync. Reads spread across copies; if the primary dies, a replica takes over. It comes almost for free, because the log already describes every change.
The whole thing on one page
| Layer | Its one job |
|---|---|
| Client protocol | Get the query in, send rows back, over a socket |
| Parser | Text → structured query tree |
| Planner | Choose the cheapest strategy (scan vs. index) |
| Executor | Run the plan, step by step |
| Catalog | The database’s facts about itself (tables of tables) |
| Index / B-tree | Find a row fast without scanning |
| Heap | Where rows physically live, in 8KB pages |
| Buffer pool | Cache hot pages in RAM, skip the disk |
| Disk I/O | Read and write real 8KB pages in the file |
| Transactions / MVCC | All-or-nothing changes and safe concurrency |
| WAL | Log before data, so “committed” survives a crash |
| Replication | Ship the WAL to copies for safety and scale |
The takeaway
A query comes in the top, falls through the brain and into storage to fetch some bytes, and three safety layers wrap the whole thing so it stays correct, durable, and available. That’s it. That’s the database.
None of these layers is magic. Each one is a sensible answer to a specific, concrete problem: how does the query get in? how do we understand it? how do we avoid scanning everything? how do we not touch the slow disk? how do we survive a crash? Once you can name the problem, the layer that solves it stops being mysterious.
So carry the map. The next time a query is slow, or a “committed” write goes missing, or two transactions step on each other, you’ll know exactly which of these twelve rooms to go looking in.