SQL Execution Flow¶
What is it: SQL is declarative — you describe what you want, not how to get it. The database engine determines the actual execution plan. Understanding the logical execution order explains counterintuitive behaviors (why can't you use a SELECT alias in WHERE?).
Logical execution order:
This is the logical order the SQL standard defines meaning by — not
necessarily the literal order the engine physically executes steps in
(the planner is free to reorder physical execution, e.g. pushing a
WHERE filter down before a JOIN if that's cheaper, as long as the
result is identical to running it in this logical order). What this
order actually constrains is name resolution: an identifier used in
step N can only refer to something already defined by an earlier step.
Why the order matters:
WHERE runs before SELECT, so you cannot use a SELECT alias in a WHERE clause:
-- WRONG: alias 'total' is not yet defined at WHERE stage
SELECT price * qty AS total FROM orders WHERE total > 100;
-- CORRECT
SELECT price * qty AS total FROM orders WHERE price * qty > 100;
HAVING runs after GROUP BY and after aggregate functions are computed, so you can use aggregates in HAVING but not in WHERE:
SELECT customer_id, SUM(amount) FROM orders
GROUP BY customer_id
HAVING SUM(amount) > 1000; -- correct: HAVING sees aggregate result
WHERE vs HAVING:
WHERE filters individual rows before grouping — it can use indexes, reduces the number of rows entering GROUP BY, and is always faster. HAVING filters groups after aggregation — it cannot use row-level indexes because it operates on computed aggregates. Always push conditions to WHERE if they do not depend on an aggregate. HAVING SUM(amount) > 1000 cannot be rewritten as WHERE, but HAVING customer_id > 5 can and should be moved to WHERE.
Real-world usage: Misplacing logic in HAVING instead of WHERE is a common source of slow queries. In EXPLAIN ANALYZE, a WHERE filter applied early reduces the row count shown at each plan node; a HAVING filter is applied late, after all the aggregation work is done.
Common pitfall¶
Trying to reference a SELECT-clause alias in a GROUP BY clause
works in some databases (PostgreSQL and MySQL allow it as an
extension) but not because of the logical order above — strictly by
the standard's logical order, GROUP BY (step 3) also runs before
SELECT (step 5) defines the alias, the same problem WHERE has.
Relying on a specific database's extension to this rule is a portability
trap: code that works on PostgreSQL can fail outright on a database
that follows the standard order strictly.