Almost every program eventually needs to remember something after it stops running. You could just write to files — and for a config file or a log, that's exactly right. But the moment you have lots of data, many users touching it at once, and a need to find specific records quickly, plain files fall apart. A database is the machinery that makes persistent data fast, concurrent, and trustworthy.
Specifically, a database gives you four things files don't:
- Efficient querying — find the rows you want without scanning everything.
- Concurrent access — many users reading and writing at once, safely.
- Transactions — groups of changes that either all happen or none do.
- Structure — indexes, relationships, and constraints that enforce rules about the data.
Analogy: A pile of files is a shoebox of receipts. A database is an accountant's filing system — indexed, cross-referenced, and protected against two people scribbling on the same page at once.
Relational Databases (SQL)
The dominant model for fifty years. Data lives in tables of rows and columns, and tables are linked by keys — a value in one table that refers to a row in another.
You query with SQL, a declarative language: you describe what you want, and the database figures out how to get it.
-- "Total spent by Alice, across all her orders"
SELECT u.name, SUM(o.price) AS total
FROM Users u
JOIN Orders o ON u.id = o.user_id
WHERE u.name = 'Alice'
GROUP BY u.name;
-- Result: Alice | 17.00
That JOIN is the relational model's superpower: data is stored once and stitched together at query time. The declarative style is also why two databases can run the same SQL with wildly different performance — the optimizer (Chapter 21) chooses the execution strategy.
ACID — The Guarantees That Make Money Safe
A transaction is a group of operations treated as a single unit. The classic guarantees a relational database provides are summarized by the acronym ACID:
| Property | Meaning | Example |
|---|---|---|
| Atomicity | All-or-nothing — if any part fails, the whole transaction rolls back | A bank transfer debits and credits, or neither happens |
| Consistency | Every transaction moves the DB from one valid state to another | A constraint that balances can't go negative is never violated |
| Isolation | Concurrent transactions don't step on each other | Two people withdrawing can't both grab the same $100 |
| Durability | Once committed, data survives crashes | After "transfer complete," a power outage can't lose it |
Atomicity is the one most worth internalizing: without it, a crash halfway through a transfer leaves money debited from one account but never credited to the other.
Indexes — Speed Up Reads
Without help, finding a specific row means scanning the whole table — O(n). An index is a separate data structure that maps column values to row locations, usually a B+ tree (a balanced tree designed for disk):
A B+ tree turns a query like WHERE name = 'Bob' from O(n) into O(log n) — and because each node holds many keys, even a billion-row table is reached in a handful of disk reads.
Analogy: An index is the index at the back of a textbook. Without it, you flip through every page to find "photosynthesis." With it, you jump straight to page 247. But the index isn't free — every time you add a page, you must update it too.
That trade-off is the whole story of indexing:
- Reads get faster ✓
- Writes get slower ✗ — every insert/update/delete must also update the index
- Storage grows ✗ — the index is extra data on disk
So you index the columns you filter and join on, not every column. Over-indexing is a common cause of mysteriously slow writes.
Normalization — Store Each Fact Once
Normalization is organizing tables so that each fact lives in exactly one place. The enemy is duplication, which leads to update anomalies — change a value in one row and forget the copies, and now your data contradicts itself.
The trade-off runs the other way too: heavily normalized data needs more joins to reassemble, which can be slow. So real systems sometimes denormalize deliberately — duplicating data to avoid expensive joins on hot read paths — accepting the maintenance cost for speed. Knowing when to break the rule is a senior skill.
NoSQL Databases
Relational databases assume a fixed schema and value consistency above all. When that doesn't fit — schemas that change constantly, data too large for one machine, or relationships that are the data — a family of NoSQL stores offers different trade-offs:
| Type | Examples | Best for | Mental model |
|---|---|---|---|
| Document | MongoDB, CouchDB | Flexible, JSON-like records | Folders of varied documents |
| Key-Value | Redis, DynamoDB | Simple lookups, caching | A giant dictionary |
| Column-Family | Cassandra, HBase | Wide rows, time-series, analytics | Rows with different columns each |
| Graph | Neo4j, Neptune | Relationship-heavy data | A social network map |
A document database embeds related data together instead of splitting it across tables — the opposite philosophy to normalization:
{
"_id": "user123",
"name": "Alice",
"orders": [
{ "item": "Book", "price": 15 },
{ "item": "Pen", "price": 2 }
]
}
Embedding makes reads fast (one fetch gets everything) but writes redundant (Alice's name is duplicated wherever she appears). It's denormalization as a default.
Analogy: SQL is a neatly ruled spreadsheet with strict rules about what goes in each cell. NoSQL is a folder of Post-it notes — more flexible, but the discipline is now your responsibility, not the database's.
Choosing: SQL or NoSQL?
This is one of the most consequential early decisions in a system, and the honest answer is start with a relational database unless you have a specific reason not to. Reach for SQL when you need complex queries, multi-row transactions, and strong consistency (most business and financial data). Reach for NoSQL when you have a clear scaling or shape mismatch: massive write volume, a genuinely schemaless domain, geographic distribution, or relationship-first data. Many mature systems use both — relational for the source of truth, a key-value cache and a search index alongside it.
CAP Theorem
The moment a database is spread across multiple machines, a fundamental limit appears. The CAP theorem says that during a network partition (when nodes can't talk to each other), you must choose between two goals:
- Consistency — every read returns the most recent write.
- Availability — every request gets a (non-error) response.
- Partition tolerance — the system keeps working despite dropped messages between nodes.
The subtlety beginners miss: partitions will happen in any real distributed system — networks fail. So partition tolerance isn't optional; you're really choosing, during a partition, between consistency and availability. That's why systems are described as CP (refuse some requests to stay correct) or AP (answer everything, possibly with stale data).
Analogy: A chain of stores should all show the same prices (consistency). A network outage cuts them off from headquarters (partition). Do they close until they can confirm prices match (CP — consistent but unavailable), or stay open and risk showing a slightly stale price (AP — available but inconsistent)? There's no third option that keeps both during the outage.
- CP: traditional RDBMS clusters, HBase, MongoDB (default)
- AP: Cassandra, DynamoDB, CouchDB
In practice this isn't all-or-nothing — modern systems tune consistency per operation. Before opening the engine up to see how these guarantees are built, the next chapter covers the skill that decides whether a database serves you well or fights you at every turn: designing the schema itself.