Learn Database Engineering

Two layers. The practical layer — fifteen modules from what a database is to distributed consistency — answers how do I use databases correctly. The internals layer underneath answers why do databases behave this way, and every practical lesson links down into it.

Practical layer

How do I use databases correctly?

Database Fundamentals

What a database actually is, what the engine does with your query, and where the data physically lives.

SQL

From SELECT to window functions: filtering, aggregation, every join, subqueries, CTEs and the NULL rules that trip everyone up.

Relational Modeling

Turning requirements into tables: entities, relationships, keys, constraints — and the access patterns that decide all of it.

Normalization & Denormalization

1NF to BCNF as a cure for anomalies, then when duplicating data on purpose is the right answer.

Indexes

B-trees, hash, composite, partial, covering, expression, full-text — what each can answer, and what an index costs.

Query Execution & Optimization

Parser, planner, executor; scans and joins; reading EXPLAIN ANALYZE; finding the actual bottleneck.

Transactions

ACID as four separate guarantees, what a rollback really undoes, and why the write-ahead log exists.

Concurrency & Isolation

Lost updates, dirty reads, phantoms, write skew; isolation levels; MVCC; locks and deadlocks.

PostgreSQL

The concrete implementation: types, JSONB, full-text search, extensions, partitioning, VACUUM, connection management.

Redis

Not "a cache": strings, hashes, lists, sets, sorted sets, streams, TTL, pub/sub, atomic operations — and when not to use it.

NoSQL & Data Models

Document, key-value, wide-column, graph, search, time-series, vector: what each model is actually good at.

Vector Databases & Retrieval

Embeddings, cosine similarity, ANN and HNSW, metadata filtering, hybrid search — the storage layer under RAG.

Scaling

One user to millions: connection pooling, read replicas, caching, partitioning, sharding — in that order.

Distributed Databases

Replication, CAP without the slogans, quorums, consensus, leader election, and what a partition really costs you.

Caching

Cache-aside, read-through, write-through, write-behind; TTL, eviction, invalidation, stampedes and hot keys.

Internals layer

Why do databases work this way under the hood?

An interactive systems textbook: every mechanism derived from the problem it solves. Internals overview → · Build AtlasDB →

Storage, Records & Pages

Bytes, records, fixed-size pages, slotted layouts: how a table physically exists on disk, and why the page is the unit of everything.

Index Internals

From "read every page" to a page-oriented B+ tree: derive the index, watch splits and merges, and see why fanout beats Big-O.

Sequential Scan, Page by Page
▶ interactive

With no index, `WHERE email = ?` is a loop over every page of the table file: fetch page, test each row, fetch the next page. It is linear, it cannot stop early without a uniqueness guarantee, and it is the cheapest I/O per page the storage layer can do — which is exactly why the planner keeps choosing it.

The Index, Derived from First Principles
▶ interactive

Start from "how can we avoid reading every page?" and you are forced, step by step, into a sorted array of (key → location), then binary search, then — because the array outgrows memory and storage is read in pages — into pages of entries with a page of separators, and finally a tree of pages. Nobody designed the B+ tree; it is what falls out.

B+ Tree Internals: Pages, Splits, Merges
▶ interactive

A B+ tree is a set of numbered pages: one root, internal pages of separator keys and child page ids, and leaf pages of (key → row locator) entries chained left to right. Lookups read one page per level; inserts split a full page and push a separator up; deletes borrow from or merge with a sibling. Every rule exists to keep pages between half-full and full and all leaves at the same depth.

Why B+ Trees: Fanout, Not Big-O
▶ interactive

A balanced binary tree and a B+ tree are both O(log n). One of them is 24 random page reads for 10 million keys and the other is 3. The base of the logarithm is the fanout, the fanout is how many decisions fit in one page, and a page is the unit of I/O — that is the whole argument, and it flips completely in memory.

Hash Index Internals
▶ interactive

A hash index skips the tree entirely: hash the key, take a bucket number, read that bucket, find the record reference. One page for equality, no matter the size. The price is that the hash destroys order — no ranges, no prefixes, no ORDER BY — and that buckets fill up: collisions chain into overflow pages and growth means rehashing.

Buffer Management

The page exists but reading it again is expensive: the buffer pool, hits and misses, dirty pages, pinning, LRU and Clock — then follow one read and one write through it.

The Buffer Pool
▶ interactive

A B+ tree finds the right page in four reads — but four SSD reads are 400 µs, and the same four pages are wanted ten thousand times a second. The buffer pool keeps recently used pages in RAM frames, maps page ids to frames with a hash table, tracks which frames are dirty or pinned, and evicts under its own rules instead of the operating system's.

Buffer Replacement: LRU, Clock and Scan Resistance
▶ interactive

When every frame is full, one page has to go. Least-recently-used is the obvious answer and it fails twice: its exact list costs a lock on every hit, and one sequential scan through a large table evicts the entire working set. Clock approximates LRU with a bit per frame; ring buffers, midpoint insertion and LRU-K keep a scan from flooding the pool.

Follow a Read Through the Engine
▶ interactive

`SELECT * FROM users WHERE id = 42` is nine stages from text to row: parse, plan, three index pages, the buffer pool decision on each, one heap page, one slot, one tuple. Where each page comes from — pool or storage — decides whether the query takes 4 µs or 400.

Follow a Write Through the Engine
▶ interactive

`UPDATE accounts SET balance = balance - 100 WHERE id = 42` finds the row like a read, then logs the change, modifies the page in memory, touches an index only if an indexed column changed, marks the page dirty, fsyncs the log at COMMIT — and writes the data page minutes later. The gap between COMMIT and that flush is where durability is decided.

WAL & Recovery

What survives if the machine dies after COMMIT: the write-ahead log, checkpoints, redo, undo and the restart sequence — with a crash button.

Transactions & MVCC Internals

What a transaction is inside the engine: lock tables, waits-for graphs, version chains, snapshots, dead tuples and the same workload under three isolation levels.

A Transaction, Inside the Engine
▶ interactive

BEGIN hands out a transaction id and a snapshot; reads consult it, writes stamp it onto row versions and into the WAL; COMMIT is one log record, one fsync and one bit flip in the transaction status table — and ROLLBACK, in PostgreSQL, writes almost nothing at all.

Concurrency Control: Schedules and Serializability
▶ interactive

Interleave four operations from two transactions and €20 disappears; the engine's job is to allow only interleavings whose result equals some serial order, and the two ways to do that — refuse conflicting steps (locking) or keep every version and check afterwards (multi-version / optimistic) — are the roots of every isolation mechanism.

The Lock Manager
▶ interactive

A lock is a row in a hash table keyed by the resource, with a list of who holds it in which mode and a queue of who is waiting; the compatibility matrix decides grant or wait, intention locks let row and table granularity coexist, and PostgreSQL avoids the table entirely for row locks by writing the holder's id into the row itself.

Deadlock Detection: The Waits-For Graph
▶ interactive

Two transactions each holding what the other needs will wait forever; the lock manager's wait queues already encode "who waits for whom" as a directed graph, a depth-first search finds the cycle, and the engine breaks it by aborting one participant — PostgreSQL after a one-second grace period, InnoDB immediately.

MVCC Internals: Version Chains and Snapshots
▶ interactive

If a row is never overwritten but versioned, a reader can be handed the version that was current when it started and never wait for a writer; the version chain is a linked list with a creating and a superseding transaction id on each node, the snapshot is three numbers and a list, and the visibility rule is a dozen lines that every read in the engine runs.

UPDATE, DELETE and Dead Tuples
▶ interactive

Under MVCC an UPDATE is an insert plus a stamp and a DELETE is only a stamp; neither frees a byte, so every write leaves a dead version behind that some later process — VACUUM, autovacuum, InnoDB purge — has to find, remove from the page and every index, and hand back to the free space map before the table stops growing.

Isolation Levels: The Mechanism Behind Each
▶ interactive

The same snapshot machinery produces three isolation levels by changing one thing — when the snapshot is taken — plus one rule for writers; Serializable then adds either dependency tracking that aborts (PostgreSQL) or locking reads that block (InnoDB), which is why the same level name costs retries on one engine and waits on the other.

LSM Trees

The write-optimised alternative: memtables, SSTables, bloom filters, compaction, the three amplifications — and an honest B+ tree vs LSM comparison.

LSM Trees: Why Some Engines Favour Writes
▶ interactive

When a database must absorb far more writes than it serves reads, updating a B+ tree page per row is the wrong shape. The log-structured merge tree appends every write, sorts in memory, flushes immutable sorted files and reconciles them later — trading cheap writes for a read path that has to look in several places.

SSTables: The Immutable Sorted File
▶ interactive

A memtable flush has to become a file that a reader can search without loading it, that compresses well, and that many readers can share without locks. The SSTable answers with sorted data blocks, a sparse index with one entry per block, a bloom filter and a footer that says where everything is — and it never changes after it is written.

Bloom Filters: Skipping Files That Cannot Contain the Key
▶ interactive

A point read in an LSM tree may have to consult ten files, and most of them do not hold the key. A bloom filter answers "definitely not here" from a few bits per key, with no false negatives and a tunable false-positive rate — turning ten block reads into one.

Compaction: The Merge That Pays for Cheap Writes
▶ interactive

Every flush adds a file; every update adds a version; every delete adds a tombstone. Compaction is the background k-way merge that folds files together, keeps the newest version of each key, drops the rest — and, depending on how files are chosen, decides whether the engine is cheap to write, cheap to read, or cheap on disk.

Write, Read and Space Amplification
▶ interactive

Every storage engine pays for a logical operation with more physical work than the operation itself: extra bytes written, extra pages read, extra bytes stored. Naming the three amplifications precisely — and seeing that no design minimises all of them — is the vocabulary for comparing B+ trees, leveled LSMs and tiered LSMs honestly.

Storage Engine Comparison: B+ Tree vs LSM Tree
▶ interactive

Two ways to organise bytes on disk: keep one sorted structure and update it in place, or append sorted runs and merge them later. Neither is better. Each is the right answer to a different workload, and the comparison is a table of dimensions, not a verdict.

Query Engine

Parser, AST, planner, cost model, join algorithms and the executor: follow one SQL statement from text to result through the real in-browser engine.

Inside the Query Engine
▶ interactive

A SELECT is a request, not a program. Seven stages turn it into one: parser, binder, planner, optimizer, executor and storage — each with its own data structure and its own way of failing.

Tokens, Parse Tree, AST
▶ interactive

Before a query can be planned it must become a tree. The lexer cuts text into tokens, a recursive-descent parser builds the tree by the grammar, and semantic analysis resolves every name against the catalog — the same front end every compiler has.

The Planner: Enumerating Ways to Answer
▶ interactive

For one bound query there are many correct procedures, differing by orders of magnitude. The planner lists them — access paths per table, join orders, join methods — and needs statistics to tell them apart. The tree it hands over is what EXPLAIN prints.

Cost-Based Optimization
▶ interactive

Statistics in, a number out: n_distinct, most-common values and histograms become a selectivity, a row estimate, and finally an I/O + CPU cost in units where a sequential page is 1.0 and a random page is 4.0. The arithmetic is simple; the inputs decide everything.

Join Algorithms: Nested Loop, Hash, Merge
▶ interactive

Three ways to pair rows from two inputs: loop over both (quadratic, needs nothing), hash one and probe with the other (linear, needs memory), or sort both and walk two cursors (linear after the sort, needs order). The planner picks by input sizes, available indexes and memory.

Follow the Query
▶ interactive

One real statement — best-selling products in a category — followed from text to result through every layer: parser, AST, candidate plans, cost, chosen plan, buffer pool, scans, joins, aggregate, sort, limit. At each step: what happens, why, the algorithm, the memory, the storage, the DSA concept underneath.

PostgreSQL & InnoDB Internals

The general mechanisms as two real engines implement them: heap tuples, xmin/xmax, shared buffers and VACUUM versus clustered primary keys, redo and undo logs.

Distributed Internals

How changes actually propagate: the replication stream, partition functions, quorums, leader election — and a failure simulator you can break.

Performance Internals

Why is it slow, one layer down: page reads, buffer misses, scan choice, index maintenance, WAL, compaction — and one central simulator to turn the knobs.