Why not just a file?
You could store data in a text file. It works until two things happen at once. Two users saving at the same moment overwrite each other. A crash halfway through a write leaves a half-file. Finding one record means reading the whole thing. And nothing stops you writing a customer with no email address.
A database is a program that has solved all four, carefully, over decades. That's what you're adopting — not storage, but the guarantees around it.
- Durability — once it says saved, it survives a power cut mid-write
- Concurrency — many readers and writers at once without corrupting each other
- Integrity — rules the data must satisfy, enforced by the database rather than by every developer remembering
- Speed at scale — finding one row among millions without reading all of them
Relational databases
The most common kind. Data lives in tables — rows and columns, like a spreadsheet with rules — and tables reference each other by id. PostgreSQL, MySQL, and SQLite all work this way, and they're queried with SQL.
CREATE TABLE customers ( id SERIAL PRIMARY KEY, email TEXT NOT NULL UNIQUE, name TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now()); CREATE TABLE orders ( id SERIAL PRIMARY KEY, customer_id INTEGER NOT NULL REFERENCES customers(id), total_cents INTEGER NOT NULL, placed_at TIMESTAMPTZ NOT NULL DEFAULT now());Read the constraints, not just the columns. `NOT NULL` means a row without it is rejected. `UNIQUE` means two customers can't share an email. `REFERENCES` means an order can't point at a customer who doesn't exist. Those three lines prevent a category of bug that would otherwise live in your application forever.
SELECT c.name, COUNT(o.id) AS orders, SUM(o.total_cents) AS spentFROM customers cJOIN orders o ON o.customer_id = c.idWHERE o.placed_at > now() - INTERVAL '30 days'GROUP BY c.nameORDER BY spent DESCLIMIT 10;That reads almost as English: which columns, from where, joined how, filtered by what, grouped, sorted, limited. SQL has depth, but this shape covers a large share of everyday work.
Document databases
The other common family stores documents — JSON-like objects — instead of rows. MongoDB and Firestore are the familiar names. Each document can have a different shape, which is flexible early and becomes a liability once six versions of the same record exist in production.
| Situation | Lean toward |
|---|---|
| Data with clear relationships — users, orders, invoices | Relational |
| You'll need to ask questions you haven't thought of yet | Relational |
| Records vary genuinely in shape | Document |
| You're unsure | Relational — it's the safer default and PostgreSQL stores JSON too |
Indexes, and the first slow query
An index is a lookup structure the database maintains so it can find rows without scanning the table. Like the index of a book: without it, finding a topic means reading every page.
-- Without this, "find orders for customer 42" reads every order.CREATE INDEX idx_orders_customer ON orders (customer_id);Indexes aren't free — each one makes writes slightly slower and takes disk. Add them for the queries you actually run, not for every column pre-emptively.
Setting one up sensibly
- Sketch your tables on paper firstNouns become tables, relationships become id columns. Ten minutes here saves a migration later.
- Add NOT NULL and UNIQUE from the startLoosening a constraint later is easy; tightening one against messy existing data is not.
- Use migrations, never manual editsSchema changes as versioned files, applied the same way everywhere.
- Set up backups before you have data worth losingA backup you have never restored from is a hypothesis, so test one restore.
Common mistakes
- Storing money as a floating point number instead of integer cents
- Storing timestamps without a timezone
- Skipping constraints because they're inconvenient during early development
- Never testing a restore, then discovering the backup was empty
- Adding indexes everywhere as a precaution, which slows every write
Key takeaways
- A database provides durability, concurrency, integrity, and speed — not merely storage
- Relational is the safe default; constraints belong in the schema, not only in your code
- SQL's common shape is readable within a day
- The first slow query is usually a missing index
Try it yourself
Sketch the tables for your product on paper — every noun a table, every relationship an id column. Then write the constraint for each column: can it be empty, must it be unique, what does it point to? That sketch is a schema.
