The relational model
SQL is strange until you understand that it is not a programming language in the usual sense — it is a way of describing a set of rows you want, resting on a piece of 1970s mathematics that turned out to be extraordinarily durable. Ten minutes here makes the next 27 lessons click.
After this lesson you can…
- Explain what a table, row, column and key actually are — precisely.
- Say why "declarative" changes how you should think about writing queries.
- Describe the one idea (keys) that lets separate tables be reconnected.
- Say when a relational database is the wrong choice.
1. The problem relations solve
The spreadsheet that ate itself
You track orders in one big sheet. Every row repeats the customer's name, phone and address. Then
Asha moves house — and her address is now wrong in 43 rows, right in 12, and you have no way to
know which. Someone types "Kochi " with a trailing space and your city report splits in two. You
cannot record a customer who has not ordered yet, because a row is an order.
The relational model's answer: store each fact exactly once, in a table about that kind of
thing, and link tables by keys. Everything else in SQL follows from that.
JOIN is for (lesson 09).
2. The vocabulary, precisely
| Everyday word | Formal name | What it really means |
|---|---|---|
| Table | Relation | A set of rows with a fixed set of named, typed columns |
| Row | Tuple | One fact. "Customer 7 is Asha Nair in Kochi" |
| Column | Attribute | A named property with one data type for every row |
| Cell value | — | One value, or NULL meaning "not known / not applicable" |
| Primary key | PK | The column(s) that uniquely identify a row. Never NULL, never reused |
| Foreign key | FK | A column pointing at another table's primary key |
| Schema | — | The full definition: tables, columns, types, constraints |
A mathematical set has no order. That is why a query without ORDER BY
may return rows in a different order tomorrow, even with the same data — the engine is free to
return them however is fastest. If order matters, say so. This is not a bug you can
work around; it is the model.
3. Declarative: say what, not how
results = []
for order in orders: # YOU choose to loop
if order.status == "PAID": # YOU choose the filter order
for customer in customers: # YOU choose the join algorithm
if customer.id == order.customer_id:
results.append((customer.city, order.amount))
totals = {}
for city, amount in results: # YOU choose how to group
totals[city] = totals.get(city, 0) + amount
You specified an algorithm. If customers grows to 10 million, this nested loop is
now catastrophically slow — and nothing will fix it but rewriting your code.
SELECT c.city, SUM(o.amount) AS total FROM orders o JOIN customers c ON c.customer_id = o.customer_id WHERE o.status = 'PAID' GROUP BY c.city;
You specified a result. The optimiser decides whether to use an index, which table to scan first, and whether to hash-join or merge-join — and it re-decides as your data grows, without you touching the query.
Fighting the declarative model — writing loops with cursors, or pulling rows into your application to join them there — produces slow, fragile systems. The skill in SQL is expressing the question precisely and then, when it is slow, reading the plan to see what the optimiser chose (lesson 27).
4. Keys: the idea that makes it all work
CREATE TABLE customers (
customer_id INTEGER PRIMARY KEY, -- unique, never NULL
full_name VARCHAR(100) NOT NULL,
email VARCHAR(150) UNIQUE -- a "candidate key": also unique
);
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER REFERENCES customers(customer_id), -- foreign key
order_date DATE NOT NULL
);
What the foreign key buys you
- You cannot insert an order for customer 999 if that customer does not exist. The database refuses. Your application code does not have to remember to check.
- You cannot delete a customer who still has orders (unless you say what should happen — cascade, set null, or restrict).
- The optimiser learns from it and can sometimes eliminate a join entirely.
A natural key is real-world data that happens to be unique — email, ISBN, national ID.
A surrogate key is a meaningless number the database generates. Surrogates win in
practice, because real-world "unique" values turn out not to be: people change email, countries reissue
IDs, and two suppliers use the same SKU. Use a surrogate PK, and add a UNIQUE constraint
on the natural key (lesson 21).
5. The three relationship shapes
6. When a relational database is the wrong tool
| Situation | Better fit |
|---|---|
| Deeply nested documents with a shape that varies per record | Document store (MongoDB) — or a JSON column (lesson 19) |
| Millions of writes/sec of simple key-value data | Cassandra, DynamoDB, Redis |
| Relationships are the main thing you query (friends-of-friends, N hops) | Graph database (Neo4j) |
| Analytical scans over petabytes | Columnar warehouse / lakehouse (Snowflake, BigQuery, Spark) |
| Full-text relevance search | Elasticsearch / OpenSearch |
| Almost everything else | A relational database. It is the default for good reasons |
Teams routinely reach for NoSQL to avoid schema design, then reimplement joins, constraints and transactions in application code — badly. Modern Postgres and MySQL handle JSON, full-text search and surprisingly large scale. Start relational; move a specific workload out when you have measured a specific reason.
A short history — useful for interviews
- 1970 — Edgar Codd publishes "A Relational Model of Data for Large Shared Data Banks" at IBM, arguing data should be independent of how it is physically stored.
- 1974 — IBM's System R implements SEQUEL, later renamed SQL.
- 1979 — Oracle ships the first commercial SQL database, beating IBM to market.
- 1986 — SQL becomes an ANSI standard. Every vendor then extends it differently, which is why dialects exist.
- 1995–96 — MySQL and PostgreSQL arrive; open-source databases become viable.
- 2000s — "NoSQL" promises to replace SQL for web scale.
- 2010s–now — SQL comes back everywhere: NoSQL stores add SQL layers, and Spark, Kafka, Flink and every warehouse speak it. Window functions, CTEs and JSON are now standard.
The through-line: a declarative query language over a well-defined data model has outlasted every challenger, because it lets the storage engine change underneath without your queries changing.
Recap
- Store each fact once, in a table about that kind of thing; link with keys.
- A table is a set — unordered. No
ORDER BY, no guaranteed order. - Declarative: you describe the result; the optimiser picks the algorithm.
- Primary key identifies a row; foreign key points at one and is enforced by the database.
- Prefer surrogate keys, with a UNIQUE constraint on the natural key.
- Many-to-many is always two one-to-manys with a junction table.
Checkpoint
ORDER BY returned rows in id order yesterday and in a different order today, with unchanged data. What happened?
ORDER BY.UNIQUE constraint on email so the database enforces the business rule; you just do not build the relationships on top of it.