Logical query processing order (FROM → WHERE → GROUP BY →… — Cracked Java
MidTheoryBig Tech

Logical query processing order (FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT). Why does it matter?

SQL is declarative: you write SELECT first, but the engine evaluates it almost last. The clauses are processed in a fixed logical order, and nearly every "why can't I use that alias here?" or "why is this filter in the wrong place?" question dissolves once you internalize it.

The logical order

1. FROM (+ JOINs)   build and combine the source relations
2. WHERE            filter individual rows
3. GROUP BY         collapse rows into groups
4. HAVING           filter groups
5. SELECT           compute output columns / aggregates, assign aliases
6. DISTINCT         remove duplicate result rows
7. ORDER BY         sort the result
8. LIMIT / OFFSET   take a slice

This is the logical order that defines the result's meaning. The planner is free to execute differently (it might use an index to satisfy ORDER BY early, or filter inside a join), but the answer must be as if the steps ran in this sequence.

Why it matters — the consequences fall out of the order

Aliases from SELECT aren't visible in WHERE, GROUP BY, or HAVING. SELECT (step 5) runs after them, so the alias doesn't exist yet:

-- ERROR: column "revenue" does not exist
SELECT price * qty AS revenue FROM sales WHERE revenue > 100;
-- fix: repeat the expression, since WHERE can't see the alias
SELECT price * qty AS revenue FROM sales WHERE price * qty > 100;

PostgreSQL does grant one exception: ORDER BY (step 7) and GROUP BY may reference SELECT aliases, because they run at or after the projection. So ORDER BY revenue works even though WHERE revenue doesn't.

WHERE can't see aggregates; HAVING can. Aggregation happens at step 3, after WHERE. That's why an aggregate condition must live in HAVING, not WHERE.

LIMIT applies last, so LIMIT without ORDER BY returns an arbitrary slice — there's no defined order to take "the first 10" from. Likewise OFFSET skips already-ordered rows; pagination is only stable with a deterministic ORDER BY.

DISTINCT runs after SELECT but before ORDER BY, so you can only ORDER BY expressions that survive the DISTINCT projection — a frequent gotcha with SELECT DISTINCT ... ORDER BY <column not selected>.

The senior framing

Window functions slot in with SELECT (step 5), after GROUP BY/HAVING — which is why a window function can't appear in WHERE and why you wrap it in a subquery/CTE to filter on its result.

Mark your status