What is the logical processing order of a SQL SELECT statement?
SQL processes clauses in this order: FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT. This matters because it explains why you cannot use a SELECT alias in a WHERE clause, but you can use it in ORDER BY.
How to think about it
The order you write SQL differs from the order the engine evaluates it. This one mental model resolves almost every “why doesn’t this work?” SQL question — alias scoping, WHERE vs HAVING, the ORDER BY exception, all of it. The logical processing order:
FROM identify tables, apply JOINs
↓
WHERE filter individual rows (no aggregates, no SELECT aliases)
↓
GROUP BY collapse rows into groups
↓
HAVING filter groups (aggregates allowed here)
↓
SELECT compute expressions, assign aliases
↓
ORDER BY sort (SELECT aliases now visible)
↓
LIMIT/OFFSET truncate the sorted result
Two consequences fall straight out. A SELECT alias can’t be used in WHERE (step 2 runs before the alias is computed at step 5) but can be used in ORDER BY (step 6, after). And an aggregate works in HAVING (step 4, after grouping) but not in WHERE (step 2, before any groups exist).
A worked example — WHERE thins, then HAVING judges
Watch WHERE filter rows before grouping and HAVING filter groups after:
SELECT customer_id, COUNT(*) AS order_count, SUM(amount) AS total
FROM orders
WHERE year = 2024 -- step 2: per-row, drops the 2023 order first
GROUP BY customer_id -- step 3: collapse to one row per customer
HAVING COUNT(*) > 2; -- step 4: per-group, keep only busy customers
| customer_id | order_count | total |
|---|---|---|
| 3 | 3 | 205 |
Only customer 3 survives. Trace the pipeline: WHERE year = 2024 removes customer 1’s lone 2023 order before grouping, leaving customer 1 with two 2024 orders, customer 2 with one, and customer 3 with three. After GROUP BY, HAVING COUNT(*) > 2 keeps only customer 3. Try to move COUNT(*) > 2 into WHERE and it errors — at step 2 there’s no count yet to compare.