Data & Databases — A Developer's Field Guide
layout: cover kicker: A developer's field guide title: Data & Databases subtitle: From ACID and indexes to the whole zoo — and how to choose.
layout: agenda kicker: The map title: What we'll cover items:
- { topic: Foundations, desc: "what a database gives you that a file can't" }
- { topic: The relational core, desc: "tables · indexes · why they're fast" }
- { topic: The data zoo, desc: "the 8 database types & how to tell them apart" }
- { topic: Distributed & scale, desc: "copies · sharding · CAP & consensus" }
- { topic: Choosing well, desc: "a simple decision flow & common mistakes" }
layout: section index: "01" kicker: Part one title: Foundations subtitle: The guarantees you take for granted.
layout: statement kicker: Why databases exist title: A plain file can't do concurrency, integrity, durability, or search. A database can.
layout: define kicker: The core promise term: ACID definition: The guarantee that a transaction is all-or-nothing and survives crashes. points:
- "Atomicity — all the steps happen, or none do"
- "Consistency — your rules (constraints) always hold"
- "Isolation — running transactions don't see each other's half-done work"
- "Durability — once it says saved, a power cut can't lose it"
layout: diagram kicker: The classic race title: Two buyers, one seat build: true note: With no isolation, both reads see 1 seat — so both bookings succeed and you oversell.
sequenceDiagram
participant A as Buyer A
participant DB
participant B as Buyer B
A->>DB: SELECT seats → 1
B->>DB: SELECT seats → 1
A->>DB: UPDATE seats = 0
B->>DB: UPDATE seats = 0
Note over DB: 2 bookings, 1 seat
layout: reference kicker: Isolation levels, in plain terms title: Each level prevents one more bug groups:
- title: The bugs (anomalies)
items:
- { term: Dirty read, desc: "you read another transaction's not-yet-saved write" }
- { term: Non-repeatable read, desc: "you read one row twice, get two values" }
- { term: Phantom, desc: "you re-run a query and new rows have appeared" }
- title: The levels (low → strict)
items:
- { term: Read Committed, desc: "Postgres default — blocks dirty reads" }
- { term: Repeatable Read, desc: "MySQL default — also blocks non-repeatable reads" }
- { term: Serializable, desc: "strictest — as if transactions ran one at a time" }
layout: section index: "02" kicker: Part two title: The relational core subtitle: Done right, it solves most problems.
layout: vs kicker: Schema design title: The two moves left: title: Normalize items: - Store each fact in exactly one place - No data going out of sync - The default when you're writing data right: title: Denormalize items: - Copy data so you can skip joins - Faster reads - On purpose — for analytics or scale label: vs
layout: quote quote: Normalize till it hurts; denormalize till it works. author: Database folk wisdom
layout: define kicker: The single biggest lever term: What is an index? definition: A sorted lookup structure — usually a B-tree — so the database can jump to a row instead of reading them all. points:
- "No index — read every row to find one (slow as the table grows)"
- "B-tree — a handful of hops, even in a billion rows"
- "Speeds up reads, slightly slows writes, uses disk — always a trade"
layout: diagram kicker: Index title: A B-tree lookup aside: under the hood build: true highlight: [Root, M, Hit] note: Find 42 in ~3 hops — follow the glowing path, skip everything else.
flowchart TD
Root["50 · 100"] --> L["10 · 30"]
Root --> M["60 · 80"]
Root --> R["120 · 160"]
M --> Hit["42 ✓"]
layout: code-explain kicker: How is this fast? title: Read the real plan notes:
- "Seq Scan = no index, so it reads the whole table."
- "The filter keeps 1 row and throws away 99,999."
- "12.7 ms here — with an index it's ~0.02 ms."
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE customer_id = 42;
Seq Scan on orders (rows=1)
actual time=0.014..12.706
Rows Removed by Filter: 99999
Execution Time: 12.741 ms
layout: stats kicker: What an index buys title: The same query, indexed stats:
- { value: 635, unit: "×", label: faster lookup, icon: "lucide:zap", tone: good }
- { value: 3, label: hops, not 100k rows read, icon: "lucide:list-tree" }
- { value: 0.02, unit: " ms", label: down from 12.7 ms, tone: good }
layout: panels kicker: Curiosity title: Under the hood — four building blocks aside: deep dive panels:
- { icon: "lucide:list-tree", title: B-tree, items: ["sorted, balanced", "fast reads — Postgres default"] }
- { icon: "lucide:layers", title: LSM-tree, items: ["batches writes in memory", "fast writes — Cassandra, RocksDB"] }
- { icon: "lucide:scroll-text", title: Write-ahead log, items: ["records the change before making it", "how a crash can't lose data"] }
- { icon: "lucide:filter", title: Bloom filter, items: ["instantly says 'not here'", "skips pointless disk reads"] }
layout: section index: "03" kicker: Part three title: The data zoo subtitle: Not winners — different shapes for different jobs.
layout: statement kicker: The real question title: "Eight labels — but what really differs is the shape of your data and the one thing each makes fast."
layout: feature kicker: The landscape title: Eight kinds of database columns: 4 features:
- { icon: "lucide:table", title: Relational, desc: "rows, joins, SQL — Postgres, MySQL" }
- { icon: "lucide:key-round", title: Key–Value, desc: "a giant hash map — Redis" }
- { icon: "lucide:braces", title: Document, desc: "stores whole JSON objects — MongoDB" }
- { icon: "lucide:columns-3", title: Wide-column, desc: "built for massive writes — Cassandra" }
- { icon: "lucide:share-2", title: Graph, desc: "relationships first — Neo4j" }
- { icon: "lucide:activity", title: Time-series, desc: "time-stamped metrics — Timescale" }
- { icon: "lucide:search", title: Search, desc: "full-text search — Elasticsearch" }
- { icon: "lucide:locate-fixed", title: Vector, desc: "find similar items — pgvector, Qdrant" }
layout: panels kicker: Why NoSQL exists title: Four families, each gives up one relational feature panels:
- { icon: "lucide:key-round", title: Key–Value, items: ["gives up schema + joins", "→ instant lookups"] }
- { icon: "lucide:braces", title: Document, items: ["gives up joins", "→ the whole object in one read"] }
- { icon: "lucide:columns-3", title: Wide-column, items: ["gives up joins + fixed schema", "→ massive write scale"] }
- { icon: "lucide:share-2", title: Graph, items: ["makes relationships first-class", "→ cheap deep traversals"] }
layout: two-cols kicker: Same data, two models title: One record, two shapes
Relational — split across two tables, joined on demand.
users table
orders table
::right::
Document — one nested object, stored whole.
{
"id": 1,
"name": "Ada",
"orders": [
{ "item": "Book" },
{ "item": "Pen" }
]
}
layout: default kicker: At a glance · the general-purpose models title: How they actually differ
layout: default kicker: At a glance · the specialized engines title: Built for one job
These don't replace your database — they sit beside it for the one job it's bad at.
layout: diagram kicker: The vector engine, end to end title: How "find similar" works aside: deep dive note: Turn text into a vector, then find the nearest vectors. The pipeline behind RAG, semantic search & recommendations.
flowchart LR
Q[Query text] --> E[Turn into a vector]
E --> ANN[Similarity index]
ANN --> K[Closest matches]
K --> R[Re-rank & return]
layout: statement kicker: A different axis title: Shape is one axis. What you do with the data is the other — and it splits databases in two.
layout: vs kicker: The deepest split title: Your app vs analytics — two completely different jobs left: title: OLTP — your app items: - Many tiny transactions - Read & write single rows - Stored row-by-row - Postgres, MySQL right: title: OLAP — analytics items: - A few huge queries - Add up millions of rows - Stored column-by-column - Snowflake, DuckDB label: vs
layout: default kicker: Storage layout title: A column store reads just one column
layout: chart kicker: The payoff title: Adding up one column note: AVG(age) across 10M rows — the kind of gap columnar storage buys. chart: type: bar unit: ms categories: [Row store, Column store] series: - { name: scan time, data: [8100, 240] }
layout: metric kicker: One number value: "34" unit: "×" ghost: "×" label: faster on that sum — column store vs row store.
layout: default kicker: The honest default title: Postgres until it hurts
One boring relational database does most of these jobs — reach for a specialist only when you've measured the pain.
Every extra datastore is one more thing to run, back up, and keep in sync. Earn it.
layout: section index: "04" kicker: Part four title: Distributed & scale subtitle: Where the trade-offs get sharp.
layout: steps kicker: Scale in order title: Don't skip a rung steps:
- { title: Bigger box, desc: "the simplest fix — but there's a ceiling", icon: "lucide:move-up" }
- { title: Read replicas, desc: "copies to spread out reads (mind the lag)", icon: "lucide:copy" }
- { title: Partition, desc: "split one big table into pieces", icon: "lucide:divide" }
- { title: Shard, desc: "split across machines — last resort", icon: "lucide:network" }
layout: diagram kicker: Sharding title: One router, many shards build: true highlight: [C, RT, S2] note: hash(key) picks the shard — here it lands in 34–66 → Shard B. Queries that span shards are the cost.
flowchart TD
C[Client] --> RT{"Router · hash(key)"}
RT -->|0–33| S1[(Shard A)]
RT -->|34–66| S2[(Shard B)]
RT -->|67–99| S3[(Shard C)]
layout: define kicker: The famous one term: CAP theorem definition: If the network splits in two (a partition), you can keep Consistency or Availability — not both. points:
- "C — every read sees the latest write"
- "A — every request still gets an answer"
- "P — the system keeps working when nodes can't reach each other"
layout: statement kicker: The honest version title: Network splits are rare — so the everyday trade-off is speed vs consistency. That's PACELC.
layout: diagram kicker: Under the hood · consensus title: How nodes agree — Raft aside: under the hood note: One leader at a time; a write is saved once a majority of nodes agree.
stateDiagram-v2
[*] --> Follower
Follower --> Candidate: election timeout
Candidate --> Leader: wins majority
Candidate --> Follower: sees higher term
Leader --> Follower: sees higher term
layout: section index: "05" kicker: Part five title: Choosing well subtitle: And not shooting your own foot.
layout: steps kicker: How to choose title: Four questions, in order steps:
- { title: How will you query it?, desc: "by key · joins · aggregates · similarity", icon: "lucide:git-fork" }
- { title: How exact must it be?, desc: "exact (money) vs roughly-now (a feed)", icon: "lucide:scale" }
- { title: How big, really?, desc: "data size, write rate, query size", icon: "lucide:ruler" }
- { title: Default to boring, desc: "Postgres unless an answer rules it out", icon: "lucide:anchor" }
layout: panels kicker: The usual suspects title: Four common ways to shoot your foot panels:
- { icon: "lucide:gauge", title: Missing indexes, items: ["slow full scans at scale", "run EXPLAIN before you ship"] }
- { icon: "lucide:repeat", title: N+1 queries, items: ["1 query quietly becomes 1,000", "load it all at once (join)"] }
- { icon: "lucide:network", title: Sharding too early, items: ["10× the complexity", "scale up first"] }
- { icon: "lucide:blocks", title: Schemaless drift, items: ["'flexible' JSON turns to mud", "validate the shape in code"] }