Databases & SQL — Where Data Lives
What you'll learn
- What a database is and why apps need one
- Tables, rows, columns — the spreadsheet model
- SQL — the language to ask questions of data
- SQL vs NoSQL, and you'll write query logic yourself
A giant, smart spreadsheet 📊
When you close an app, your data doesn't vanish — a database stored it permanently. The most common type, a relational database (MySQL, PostgreSQL, SQLite), organizes data like linked spreadsheets:
- A table = one sheet (e.g.
users) - A column = a field (
id,name,email) - A row = one record (one actual user)
users table
┌────┬─────────┬──────────────────┐
│ id │ name │ email │
├────┼─────────┼──────────────────┤
│ 1 │ Sara │ sara@mail.com │
│ 2 │ Omar │ omar@mail.com │
└────┴─────────┴──────────────────┘
You talk to it with SQL (Structured Query Language) — remarkably close to English:
SELECT name FROM users WHERE id = 1; -- get Sara's name
INSERT INTO users (name, email) VALUES ('Lina', 'lina@mail.com');
UPDATE users SET email = 'new@mail.com' WHERE id = 2;
DELETE FROM users WHERE id = 2;
These four — SELECT, INSERT, UPDATE, DELETE — are called CRUD (Create, Read, Update, Delete) and are 95% of what backends do all day.
SQL vs NoSQL: SQL databases have a strict structure (every user row has the same columns) — great when data is well-defined and relationships matter. NoSQL databases (MongoDB, etc.) store flexible documents — great when structure varies or you need massive scale. Most apps start with SQL; reach for NoSQL for specific needs.
Simulate a SELECT ... WHERE query. Given a list of user dictionaries in users, write find_by_email(users, email) that returns the user dict whose email matches, or None if not found.
This is exactly what a database does under the hood.
Indexes and JOINs — speed and relationships 🔗
Two ideas turn a plain table of data into a real, fast, relational database.
Indexes — the book's table of contents. Without an index, finding WHERE email = 'sara@mail.com' in a million-row table means scanning every single row, one by one — a full table scan. Slow. An index is a separate, pre-sorted lookup structure (like a book's index at the back) that lets the database jump almost straight to the matching rows instead of reading everything:
CREATE INDEX idx_users_email ON users(email);
-- now WHERE email = '...' is fast even with millions of rows
The trade-off: indexes speed up reads but slightly slow down writes (every INSERT/UPDATE must also update the index) and use extra disk space. You index the columns you filter or sort by often — not every column.
JOINs — connecting tables. Real data is rarely one flat table. An e-commerce app has users and separately orders, linked by a user_id column in orders pointing back to a user's id. A JOIN stitches matching rows from two tables into one result:
SELECT users.name, orders.total
FROM orders
JOIN users ON orders.user_id = users.id;
-- "Sara" | 45.00
-- "Omar" | 120.00
This is the core idea behind "relational" in relational database: tables relate to each other through shared keys (a foreign key like user_id), instead of duplicating every user's name inside every single order row.
Simulate what an index buys you. Build an index dict from a list of orders, then look up a user's orders instantly instead of scanning the whole list.
Write build_index(orders) that returns a dict mapping user_id → list of that user's orders. Then write orders_for_user(index, user_id) that returns the list for that user (or an empty list [] if none).
Real-world case study: this platform's own database 🔍
This is not hypothetical — this very learning platform runs on a real relational database. Its backend (a Cloudflare Worker) uses D1, Cloudflare's managed SQLite database, with a schema that starts like this:
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
github_id TEXT UNIQUE NOT NULL,
username TEXT NOT NULL,
email TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
Notice the exact concepts from this lesson: id is the primary key (uniquely identifies each row), github_id is marked UNIQUE so no two users can share one GitHub account, and there's a real index on username — because the app looks up users by username often enough that a full table scan would be wasteful as the user count grows.
Every query in the Worker's code uses parameterized queries like env.DB.prepare("SELECT * FROM users WHERE github_id = ?").bind(user.sub) — the ? placeholder plus .bind() is the standard, safe way to insert values into SQL without exposing the app to SQL injection (an attacker sneaking SQL commands through a text field). This lesson's abstractions aren't academic — they're exactly what runs in production for the app you're learning in right now.
Lab exercise 1
PythonGraded in Code LabChange data safely. The script builds a small products table in SQLite (id, name, category, price_cents). Standard input has one line: a category name.
Write one parameterised UPDATE that gives every product in that category a 10% discount: price_cents becomes price_cents * 90 / 100 (integer maths: SQLite truncates the division). Then the script prints every product ordered by name as name price_cents.
Never paste the category into the SQL text: bind it with ?. A category with no products must change nothing.
- Discounts the paper category only
- Discounts the bags category
- A category with one product
- Unknown category changes nothing
- SQL-injection text is treated as a plain value
Example run
- Input
paper
- Expected output
Backpack 4999 Marker 179 Notebook 1799 Pen 225 Pencil case 1299 Stapler 899
import sqlite3
category = input().strip()
db = sqlite3.connect(":memory:")
db.executescript('''
CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, category TEXT, price_cents INTEGER);
INSERT INTO products (name, category, price_cents) VALUES
('Notebook','paper',1999), ('Pen','paper',250), ('Backpack','bags',4999),
('Pencil case','bags',1299), ('Stapler','office',899), ('Marker','paper',199);
''')
More lines in Code Lab: 5
Hints and instant feedback are in Code Lab. Optional practice: it does not block finishing the lesson.
JavaScript and Python also run in your browser without an account; other languages use the secure server runner and need a free account.
Question 1 of 6
In a relational database, a 'row' is…
Question 2 of 6
What does CRUD stand for?
Question 3 of 6
When is a SQL database usually the better default?
Question 4 of 6
What does a database index mainly do?
Question 5 of 6
What does a JOIN do?
Question 6 of 6
Why does this platform's own users table use .prepare("...WHERE github_id = ?").bind(value)?
Summary
- Databases store data permanently as tables (rows + columns)
- SQL does CRUD: SELECT, INSERT, UPDATE, DELETE
- SQL = structured + relational; NoSQL = flexible + scalable
- You simulated a WHERE query in Python
- Indexes make lookups fast at scale; JOINs connect related tables through keys — you built an index-style lookup by user_id
- This platform's own D1 database shows these ideas in production, including safe parameterized queries against SQL injection
Practice: try free interactive SQL at sqlbolt.com. Next: APIs & REST — how the frontend and other services talk to your database-backed server.
Practice
Reading is not learning until you run something. Pick one.
Graded lab exercises
- Lab exercise 1PythonNot passed yet Try in Code Lab