Databases & SQL
High SchoolA database is an organised, persistent store of data with efficient query and update. The dominant model is relational (Codd, 1970): data lives in tables (relations) of rows (records) and columns (fields). A primary key uniquely identifies each row; a foreign key in one table references the primary key of another, encoding relationships between tables.
SQL (Structured Query Language) is the declarative language for relational data. You state what you want, not how to fetch it — the database's query optimiser devises an efficient plan. Core verbs: SELECT (read), INSERT, UPDATE, DELETE, and JOIN (combine rows across tables on a matching key).
Normalisation organises tables to eliminate redundancy so that each fact is stored exactly once, preventing update anomalies. Transactions guarantee ACID properties: Atomicity (all-or-nothing), Consistency, Isolation (concurrent transactions don't interfere), and Durability (committed data survives crashes).
The relational model's genius is separating logical structure from physical storage. You query by describing relationships between values; the engine decides which indexes to use, in what order to scan, how to join. This declarative style is why the same SQL runs efficiently on a laptop or a thousand-server cluster — the optimiser adapts, you don't rewrite.
Why normalise? Consider storing a customer's address alongside every one of their orders. Change address and you must update dozens of rows; miss one and your data contradicts itself — an update anomaly. Normalisation splits customers and orders into separate tables linked by a foreign key, so the address lives in exactly one place. The JOIN reassembles the full picture on demand.
Trade-off. Fully normalised data avoids anomalies but needs joins to read. High-scale systems sometimes denormalise (deliberately duplicate) to trade storage and update-complexity for read speed — a conscious engineering choice, not sloppiness.
We create two related tables, insert data, and answer a real question with a JOIN plus aggregation: "How much has each customer spent?"
-- Two tables linked by a foreign key
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
city TEXT
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER REFERENCES customers(id), -- foreign key
amount REAL NOT NULL
);
INSERT INTO customers VALUES (1, 'Aisha', 'Kuala Lumpur'),
(2, 'Ben', 'Singapore'),
(3, 'Chen', 'Kuala Lumpur');
INSERT INTO orders VALUES (10, 1, 250.00), (11, 1, 100.00),
(12, 2, 75.50), (13, 3, 400.00);
-- Total spend per customer, highest first (JOIN + GROUP BY + aggregate)
SELECT c.name,
c.city,
COUNT(o.id) AS num_orders,
SUM(o.amount) AS total_spent
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.id
GROUP BY c.id, c.name, c.city
HAVING SUM(o.amount) > 100 -- filter groups after aggregation
ORDER BY total_spent DESC;
-- Result:
-- name | city | num_orders | total_spent
-- Chen | Kuala Lumpur | 1 | 400.00
-- Aisha | Kuala Lumpur | 2 | 350.00
Read it declaratively: JOIN stitches each order to its customer via the key; GROUP BY collapses rows per customer; SUM/COUNT aggregate each group; HAVING filters the groups; ORDER BY sorts. Ben is excluded because his 75.50 fails the HAVING test. You never told the engine how to loop — only what you wanted.
Q1 What is the difference between a primary key and a foreign key?
A primary key uniquely identifies each row within its own table and cannot repeat or be null. A foreign key is a column that points to another table's primary key, establishing a relationship. In the example, customers.id is a primary key; orders.customer_id is a foreign key referencing it, so every order is tied to exactly one customer.
Q2 Why is SQL called "declarative"?
Because you describe the result you want, not the step-by-step procedure to compute it. You write "give me customers who spent over 100, sorted by total"; the database's optimiser figures out which indexes to use and how to execute it efficiently. Compare imperative code, where you'd write the loops and comparisons yourself.
Q3 What does the "A" in ACID protect against?
Atomicity ensures a transaction is all-or-nothing. Transferring money debits one account and credits another; if the system crashes between the two steps, atomicity guarantees both are rolled back, never leaving money that vanished from one account without arriving in the other. The transaction either fully commits or fully aborts.
Q4 When would I choose a NoSQL database over a relational one?
When your data is unstructured or schema-fluid (documents, key-value pairs), when you need to scale writes across many machines horizontally, or when you can trade strict consistency for availability and speed. NoSQL stores relax parts of the relational model and ACID to gain flexibility and scale — but you give up powerful joins and strong guarantees, so relational remains the default for data with rich relationships.
How the ideas connect
Every key idea in this chapter, branching from the core concept — use it to see the whole picture at a glance.
The key facts, visualised
Worked problems, step by step
Follow each solution line by line, then try to reproduce it on paper before moving on.
Example 1Write SQL to get the names of students older than 18.
- Choose the name column.
- Filter rows with a WHERE clause.
Example 2Write SQL to count orders per customer.
- Group the rows by customer_id.
- Count rows in each group.
Now you try
Work each one out first, then tap to reveal the worked answer.