Day 38 ยท The index at the back of the book

Database Internals

You will be able to
  • Explain why databases use B-trees rather than binary trees or hash maps
  • Read EXPLAIN QUERY PLAN output and tell a scan from an index search
  • Create the right index for a query and measure the speedup
  • Demonstrate a transaction's atomicity with a deliberate mid-way failure
  • Recognize and fix the N+1 query problem
Today's ~120 minutes
Spaced-rep warm-up: due cards (joins, BSTs, binary search)10 min
ELI5 + tech read; trace a key descent in the B-tree visualizer18 min
Guided: index physics + atomic money + N+1 hunt42 min
Practice: slow-query clinic20 min
Project: index audit of the tracker schema20 min
Quiz + flashcards10 min

Builds on: Day 37 โ€” SQL II โ€” joins & modeling ยท Day 29 โ€” Binary trees & BSTs ยท Day 33 โ€” Binary search

The analogy

Find every mention of "entropy" in a 600-page textbook. Plan A: read all 600 pages โ€” a full scan. Plan B: flip to the index at the back, which says "entropy: pages 214, 388" โ€” three seconds. The index is a separate, alphabetically sorted mini-book of "term โ†’ where to find it," maintained precisely so you never have to read everything.

A database index is exactly that, with one twist driven by physics. Disks hand data over in big pages, not single words, so the index is built as a B-tree: like Day 29's search tree, but each node is a page holding HUNDREDS of sorted keys with hundreds of children. Hundreds of branches per step means a billion rows sit only three or four page-reads from the root โ€” a binary tree would need thirty.

The catch is the same as a book index: every edit to the book means updating the index too. Write a row, update every index on that table. Indexes are a tax on writes to subsidize reads, and choosing WHICH columns deserve one is the craft. The other star today: transactions. Move money between accounts and crash halfway โ€” the database promises all-or-nothing, so the half-finished transfer simply never happened.

Why this matters on the job

"The app is slow" is, in an enormous fraction of real incidents, "a query is scanning instead of seeking" โ€” and the fix is a one-line CREATE INDEX found by reading a query plan, one of the highest-leverage skills an engineer carries on-site. The N+1 problem is its evil twin: ORMs generate it silently, and you will diagnose it in customer codebases as an FDE. Transactions are why your Day 42 service can crash mid-write without corrupting data. And this whole toolbox transfers upward: Day 115's vector indexes make the same read-vs-write, exact-vs-fast trade-offs, and interviewers love "why B-trees and not hash maps?" as a systems probe.

Watch it happen

The index at the back of the book โ€” scan vs B-tree descent

step 1 / 6
p1p2p3p4p5p6

Query: WHERE id = 61. Without an index the database FULL-SCANS: reads every disk page and checks every row. Watch the cost mount.pages read: 3 and countingโ€ฆ

Guided practice

guided 1

Watch an index change the physics

22 min
  1. Create internals_lab.py. The starter builds a 200,000-row events table with random user_ids and timestamps. Run it (a few seconds).
  2. Time the query "events for user 4242" and read its plan: EXPLAIN QUERY PLAN says SCAN events โ€” the 600-page read.
  3. Create the index on user_id. Re-run the SAME query: plan now says SEARCH events USING INDEX, and the timing drops by orders of magnitude. Record both numbers โ€” this before/after is the most persuasive demo in databases.
  4. Check the fine print: run a query filtering on kind (unindexed) โ€” still a SCAN; the index on user_id is irrelevant. Then ORDER BY user_id LIMIT 10 โ€” the index serves ordering too. Explain both from the sorted-B-tree picture.
  5. Measure the write tax: time inserting 20,000 more rows with the index present vs after DROP INDEX (starter has both paths). Writes pay for reads โ€” say the trade out loud.
๐Ÿ python โ€” editable, runs in your browser
Ctrl/โŒ˜+Enter runs ยท Tab indents ยท numpy/pandas/sklearn auto-load on import (torch and network calls need a local run)
guided 2

Atomic money + hunting the N+1

20 min
  1. Add the accounts table (starter) with two accounts of 100 each and a CHECK (balance >= 0). Write transfer(a, b, amount) as two UPDATEs inside with conn:.
  2. Transfer 150 โ€” more than Amara has. The second UPDATE violates the CHECK, the exception fires, and with conn: rolls back BOTH updates: print balances and verify no money was created or destroyed. Now comment out the transaction (autocommit each UPDATE separately) and repeat โ€” money vanishes. Restore the transaction and never speak of it again.
  3. The N+1 hunt. The starter fetches 200 recent events, then loops fetching each event's user row โ€” 201 queries. Time it.
  4. Rewrite as ONE join query. Time it. On an in-process engine the gap is real but modest; say why it becomes catastrophic when each query crosses a network to Postgres (~1ms round trip ร— N).
  5. Count queries with the starter's counter wrapper to prove it: 201 vs 1. That counter trick โ€” logging actual query counts โ€” is exactly how you will catch ORMs doing this behind your back.
๐Ÿ python โ€” editable, runs in your browser
Ctrl/โŒ˜+Enter runs ยท Tab indents ยท numpy/pandas/sklearn auto-load on import (torch and network calls need a local run)

On your own

The slow-query clinic, database edition

20 min

Three "production complaints" against your 200k-row events table. For each: read the plan, diagnose, fix, prove with before/after timings. (1) "The activity page is slow": SELECT * FROM events WHERE user_id = ? AND kind = 'purchase' โ€” a single-column index exists on user_id; would a composite (user_id, kind) beat it, and does column order matter? Test both orders. (2) "The export is slow": SELECT user_id, ts FROM events WHERE user_id = ? โ€” make the query covering so the plan says the table is never touched (add ts to the index) and find the plan's covering-index marker. (3) "Inserts got slow after we added five indexes": drop to the minimum set that keeps queries 1โ€“2 fast, and defend your choice in two sentences.

Hints: EXPLAIN QUERY PLAN before and after every change; (user_id, kind) serves user_id-only queries too, so it can REPLACE the single-column index.

Ship before you stop

Index audit of your tracker schema

Return to yesterday's task-tracker schema with today's eyes. In tracker_audit.py: (1) seed it up to ~50,000 tasks with realistic skew (a few users own most tasks); (2) take your five Day 37 product queries, EXPLAIN each, and record which ones SCAN; (3) design and create the minimal index set that turns every hot query into a SEARCH โ€” with the reasoning for each index (and each index you deliberately did NOT add) as comments; (4) demonstrate one transaction: "complete task and log the completion" as an atomic pair with a forced failure showing rollback; (5) record before/after timings in the docstring. Commit โ€” the Day 42 service inherits this schema and its indexes.

Rubric โ€” check what you completed (0/5)

Common mistakes & misconceptions

  • Indexing every column "to be safe." Each index taxes every INSERT/UPDATE and bloats the file; index the columns your actual queries filter, join, and sort on.
  • Expecting an index on (a, b) to speed queries filtering only on b. Composite indexes sort by the FIRST column; b-only queries scan. Order composites by how you query.
  • Believing a hash map would beat a B-tree for a database index. Hashes cannot do ranges, prefixes, or ORDER BY โ€” the questions databases live on.
  • Writing multi-statement changes without a transaction. A crash between the UPDATEs leaves half-transferred money; with conn: makes it all-or-nothing.
  • Diagnosing slowness by staring at code instead of running EXPLAIN. The plan tells you scan-vs-search in one line; guessing tells you nothing.
  • Shipping an ORM loop that queries per row (N+1) because each line "looks innocent." Count actual queries; fetch related rows in one JOIN or IN.
Knowledge check

Q1. Why do databases build indexes as B-trees with huge nodes instead of binary search trees?

Q2. EXPLAIN QUERY PLAN shows "SCAN orders" for WHERE customer_id = 7, and the query is slow. Best next move?

Q3. A crash occurs after "debit A" but before "credit B". With both UPDATEs in one transaction, what happens on restart?

Go deeper โ€” curated resources

bookUse The Index, Luke โ€” Anatomy of an Index โ†—30 mindocsSQLite โ€” The Query Planner (official) โ†—20 mincourseCMU 15-445 โ€” Database Systems (index lectures for the third hour) โ†—20 min
If you have a third hour
  • Isolation levels, concretely โ€” Look up read-uncommitted through serializable and the anomalies each allows (dirty read, non-repeatable read, phantom). Postgres defaults to READ COMMITTED โ€” connect that to why Day 46 cares about concurrent writers.
Done means
  • Before/after index timings recorded with matching plan changes
  • Write-tax measurement done and the trade-off stated
  • Rollback demo leaves balances consistent; N+1 fixed and query-counted
  • Index audit committed with reasoning for every index kept and rejected
  • Quiz โ‰ฅ 2/3
How this connects

โ† Back: The B-tree is Day 29's search tree re-engineered for Day 33's halving at page scale; the queries being accelerated are Day 36โ€“37's. The timing-first discipline is Day 22's lab, pointed at storage.

Forward โ†’: Day 42's service inherits this schema, its indexes, and EXPLAIN as its debugging tool. Day 47 adds caching in front of the database, Day 115's vector indexes rerun the same read/write trade-offs for embeddings, and Day 118 joins SQL filters with semantic search.

Unlocks: D42 Week 6 Checkpoint: API + DB Mini-Service ยท D44 Security, AuthN & AuthZ ยท D46 Distributed Systems Fundamentals ยท D48 System Design Method