🗄 SQL & Databases · Chapter 1
SELECT, WHERE, ORDER BY — your first question
SQL is not a programming language. You describe the answer you want and the database works out how to get it.
Study this lesson free in the app →
The basics
SQL is the language for asking a database questions. A SELECT query describes the rows and columns you want, and the database decides how to fetch them.
The problem it solves: your ride data lives in tables with millions or billions of rows. You need answers like "the ten biggest Shillong fares this month" without writing loops, reading files or choosing indexes yourself. You describe the answer; the database's optimiser picks the route.
The mental model: ordering at a restaurant. You say "paneer butter masala, no chilli" — not which shelf the paneer is on. The second idea that unlocks SQL: you write clauses in one order but the database runs them in another — FROM → WHERE → SELECT → ORDER BY → LIMIT. SELECT runs near the end, which explains most beginner errors.
Words you must know 5
- SELECT
- which columns (or expressions) to return
- WHERE
- which rows to keep; only rows where the condition is true survive
- ORDER BY
- how to sort the result; without it, row order is not guaranteed
- LIMIT
- how many rows to return
- NULL
- a missing value; nothing equals it, so test with
IS NULL
Name your columns, filter the date, and LIMIT while you explore.
Explained
SELECT ride_id, fare
FROM rides
WHERE city = 'Shillong' AND fare > 100
ORDER BY fare DESC
LIMIT 10;Read it aloud: give me the ride id and fare, from the rides table, where the city is Shillong and the fare is over 100, biggest first, top ten.
You never said how to find them. No loop, no index lookup, no file reading. You described the result and the database chose the strategy — and that's the whole idea.
Ordering at a restaurant
You don't walk into a kitchen and say "go to the fridge, take the second shelf, find the paneer…". You say "paneer butter masala, no chilli."
You described what you want. The kitchen decided the route.
That's declarative. Python is a recipe — do this, then this. SQL is an order — here's the dish, you figure it out.
The consequence matters: two people can write very different-looking SQL for the same answer, and the database may execute both identically. It's not following your instructions; it's reading your description and making its own plan.
The six clauses, and the order they run in
You write them in this order:
SELECT columns you want
FROM which table
WHERE which rows to keep
GROUP BY how to bucket them (next lesson)
HAVING which buckets to keep (next lesson)
ORDER BY how to sort the result
LIMIT how many rows to returnBut the database runs them in a different order:
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMITThat one difference explains most beginner errors. SELECT happens near the end — which is why:
- you can't use a column alias you defined in
SELECTinsideWHERE(it doesn't exist yet) - but you can use it in
ORDER BY(which runs after)
SELECT fare * 1.05 AS fare_with_tax
FROM rides
WHERE fare_with_tax > 100 -- ✗ error: no such column
ORDER BY fare_with_tax; -- ✓ fineLearn the execution order now and you will pre-empt a dozen confusing errors later.
Watch the query execute
FROM → WHERE → SELECT → ORDER BY → LIMIT. Every confusing SQL error becomes obvious once you know which step you're in.
WHERE: the operators you need
-- comparison
WHERE fare > 100
WHERE fare >= 100 AND fare <= 500
WHERE fare BETWEEN 100 AND 500 -- same thing, inclusive both ends
WHERE status != 'cancelled' -- or <> in strict standard SQL
-- sets
WHERE city IN ('Shillong', 'Guwahati')
WHERE city NOT IN ('Delhi')
-- text matching
WHERE ride_id LIKE 'ride_9%' -- % = any number of characters
WHERE ride_id LIKE 'ride_90_1' -- _ = exactly one character
WHERE lower(city) = 'shillong' -- case-insensitive comparison
-- missing values (never use = for this)
WHERE fare IS NULL
WHERE fare IS NOT NULL
-- dates
WHERE event_time >= '2026-07-01'
AND event_time < '2026-08-01' -- half-open: safer than BETWEEN
-- combining
WHERE city = 'Shillong'
AND (status = 'completed' OR fare > 0) -- brackets matter with ORTwo habits worth forming immediately: IS NULL, never = NULL, and half-open date ranges (>= start AND < end) so a timestamp at 23:59:59 on the last day isn't silently dropped.
Why does this return nothing?
The rides table has 40,000 rows. About 300 have no fare recorded.
SELECT count(*) FROM rides WHERE fare = NULL;It returns 0. No error, no warning.
The query that ran for six hours
A new analyst runs this against a 400-million-row table:
SELECT * FROM ride_events;The client hangs. They add LIMIT 100 and it returns instantly.
Two things happened, both worth internalising on day one.
SELECT * reads every column. In a warehouse with columnar storage, asking for 3 of 40 columns can be ten times cheaper than asking for all of them. SELECT * opts out of that saving entirely.
No WHERE means no pruning. If the table is partitioned by date, a query with a date filter reads one day; without one it reads every day ever stored.
The habits that follow:
- Name your columns.
SELECT *is for exploring, never for anything saved or scheduled - Always filter the partition column — usually a date — even when exploring
LIMITwhile exploring, always. It costs nothing and prevents this
On a laptop database none of this matters. On a cloud warehouse, SELECT * without a filter is a query that costs real money and appears on someone's bill.
Written order ≠ execution order. FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT.
IS NULL, never = NULL.
Name your columns. LIMIT while exploring. Filter the date.
Words to know
| Declarative | You describe the result you want; the engine decides how to compute it. |
|---|---|
| Clause | One part of a SQL statement: SELECT, FROM, WHERE, ORDER BY, LIMIT. |
| Predicate | A condition that evaluates to true, false or unknown — what WHERE filters on. |
| Alias | A temporary name for a column or table, given with AS. |
| NULL | The absence of a value. Not equal to anything, including another NULL. |
| Wildcard | % (any characters) and _ (one character) in a LIKE pattern. |
| Half-open range | >= start AND < end — the safe way to filter timestamps. |
| Partition pruning | Skipping whole files or partitions because a filter excludes them. |
In two minutes
- SQL is declarative: you describe the result, the database chooses the plan.
- Written order is SELECT…FROM…WHERE…ORDER BY…LIMIT; execution order is FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT.
- Because SELECT runs late, its aliases cannot be used in WHERE but can be used in ORDER BY.
- WHERE keeps only rows where the condition is true; comparisons with NULL are unknown, so
= NULLreturns nothing — use IS NULL. - Filter dates with half-open ranges (>= start AND < next start); BETWEEN is inclusive and drops most of the last day on timestamps.
- LIMIT without ORDER BY gives arbitrary rows; AND binds tighter than OR, so bracket your ORs.
- On a warehouse, name your columns and filter the partition column — SELECT * with no WHERE scans and bills for everything.
Test yourself
1. In what order does the database actually execute a query?
- SELECT → FROM → WHERE → ORDER BY
- FROM → WHERE → SELECT → ORDER BY → LIMIT
- WHERE → FROM → SELECT → LIMIT
- The order you wrote it in
Show the answer
FROM → WHERE → SELECT → ORDER BY → LIMIT — SELECT runs late — which is why a column alias defined there can't be used in WHERE, but can be used in ORDER BY.
2. WHERE fare = NULL returns zero rows on a table with 300 missing fares. Why?
- The rows were deleted
- Nothing equals NULL — the comparison is unknown, and WHERE keeps only true
- NULL is stored as zero
- It needs quotes
Show the answer
Nothing equals NULL — the comparison is unknown, and WHERE keeps only true — It fails silently with a confident 0. Use IS NULL. This is the most common source of quietly wrong SQL.
3. What is wrong with LIMIT 10 and no ORDER BY?
- Nothing
- You get an arbitrary 10 rows, not the top 10 — and they can differ between runs
- It is a syntax error
- It always returns the oldest rows
Show the answer
You get an arbitrary 10 rows, not the top 10 — and they can differ between runs — Without an order there is no 'top'. The engine returns whatever is cheapest to produce, which is not stable.
4. Why avoid BETWEEN on timestamps?
- It is slower
- BETWEEN is inclusive, so '…AND 2026-07-31' stops at midnight and drops that whole day
- It doesn't work on dates
- It requires an index
Show the answer
BETWEEN is inclusive, so '…AND 2026-07-31' stops at midnight and drops that whole day — Use a half-open range: >= '2026-07-01' AND < '2026-08-01'. It's correct regardless of time precision.
5. Why is SELECT * a bad habit in a saved query?
- It is slower to type
- It reads every column and silently changes output when a column is added upstream
- It is not valid SQL
- It breaks ORDER BY
Show the answer
It reads every column and silently changes output when a column is added upstream — In columnar warehouses column pruning is a large saving, and downstream code that expects a fixed shape breaks when the source adds a field.
6. Does WHERE fare BETWEEN 100 AND 500 include rides with a fare of exactly 100 and exactly 500?
- Yes, both — BETWEEN is inclusive at both ends
- Only 100
- Only 500
- Neither
Show the answer
Yes, both — BETWEEN is inclusive at both ends — BETWEEN a AND b means >= a AND <= b. That inclusivity is harmless for whole numbers and harmful for timestamps, where the upper bound '2026-07-31' means midnight at the start of the 31st.
There are 9 questions, 3 puzzles and 7 recall cards for this lesson in the app — the recall cards come back on later days, just before you would forget.
Start this lesson →