SQL Execution Order: How a Query Really Runs
You write SELECT first, but the database runs it almost last. A SQL query is processed in a fixed logical order that explains a lot of otherwise-confusing behaviour — like why you can filter on a column alias in ORDER BY but not in WHERE.
▶ Open the full-pipeline example in SQL Visualizer
The logical order of operations
Whatever order you type the keywords, the engine resolves them in this sequence:
- FROM — choose the source table(s)
- JOIN — combine rows from other tables
- WHERE — filter individual rows
- GROUP BY — collapse rows into groups
- HAVING — filter the groups
- SELECT — pick / compute the output columns (aliases are born here)
- ORDER BY — sort the result
- LIMIT — keep the first N rows
A query that uses every stage
This query touches all eight stages. Open it in the visualizer to watch each clause transform the rows in turn:
SELECT d.name AS director, m.genre,
SUM(m.revenue) AS total_revenue, COUNT(*) AS films
FROM movies m
INNER JOIN directors d ON m.director_id = d.director_id
WHERE m.year >= 2000 AND m.budget > 20
GROUP BY d.name, m.genre
HAVING SUM(m.revenue) > 200
ORDER BY total_revenue DESC
LIMIT 81–2. FROM and JOIN: assemble the rows
First the engine reads movies, then matches each movie to its director. See SQL JOINs explained for how matching actually works.
3. WHERE: filter rows
Rows that fail the predicate (here, films before 2000 or with a budget ≤ 20) are dropped before any grouping happens. WHERE sees raw row values, never aggregates.
4–5. GROUP BY and HAVING: groups, then group filters
Surviving rows collapse into one row per (director, genre) pair, and aggregates like SUM and COUNT are computed per group. HAVING then filters those groups — see WHERE vs HAVING and GROUP BY and HAVING.
6–8. SELECT, ORDER BY, LIMIT
SELECT computes the output columns and names them (total_revenue is created here). ORDER BY can use that alias because it runs later; LIMIT finally trims to the top 8 rows.
Why the order matters in practice
- You cannot reference a
SELECTalias inWHEREorGROUP BY— they run earlier. - Filtering with
WHEREbeforeGROUP BYis cheaper than filtering groups afterward, so push row filters intoWHERE. - Aggregate conditions (
SUM(...) > 200) must go inHAVING, notWHERE.