The Execution Order Model
The one mental model that explains almost everything else in SQL.
Why the written order is misleading
A query reads SELECT ... FROM ... WHERE ..., but the engine does not run it top to bottom. It runs a fixed six-stage pipeline, and each stage only sees what survived the stages before it. Nearly every 'surprising' SQL behaviour is this pipeline doing exactly what it should.
Stage | What it does |
|---|---|
FROM | Assemble the source rows, including any joins. |
WHERE | Discard rows that don't match. No aggregate exists yet. |
GROUP BY | Collapse the surviving rows into buckets, one per distinct key. |
HAVING | Discard whole buckets based on a condition on an aggregate. |
SELECT | Compute the output columns and create their aliases. |
ORDER BY | Sort whatever SELECT produced. |
RuleA clause can only reference something the pipeline has already built. WHERE runs before SELECT, so a SELECT alias doesn't exist yet when WHERE evaluates. ORDER BY runs last, so it can use anything SELECT produced — including the alias.
Why an alias fails in WHERE but works in ORDER BY
SELECT Salary AS AnnualSalary
FROM Employees
WHERE AnnualSalary > 50000;
-- error: no such column "AnnualSalary" — WHERE runs before SELECT names itFix it by repeating the expression (WHERE Salary > 50000), or by computing it once in a subquery or CTE so WHERE and SELECT read the same value instead of two copies that can drift apart. See Reusable Expressions for that pattern.
SELECT Salary AS AnnualSalary
FROM Employees
ORDER BY AnnualSalary DESC;
-- works: SELECT has already run, so the alias existsWHERE vs. HAVING is the same rule in different clothes
WHERE filters rows before grouping. HAVING filters groups after grouping. If a condition can be expressed in WHERE, put it there — rows removed early are never grouped, sorted, or computed over again. Reach for HAVING only when the condition depends on an aggregate that doesn't exist until GROUP BY has run.
DISTINCT and column order
DISTINCT runs after SELECT and before ORDER BY — which is why a query can sort by a selected column but not by one it dropped. The order of columns inside SELECT changes only the output shape: never which rows come back, and never how the engine finds them.
Key takeawayBefore editing any query, narrate it in pipeline order out loud: what rows FROM produces, what WHERE removes, what buckets GROUP BY builds. The narration itself catches most mistakes before you run anything.
One engine that quietly breaks this rule
The playground below runs on SQLite. SQLite resolves WHERE against the SELECT list too, so the query below succeeds there instead of erroring — the same query genuinely fails on SQL Server and Postgres, which enforce the standard rule strictly. The lesson isn't the error message; it's that never assume a rule holds across engines without checking — the exact instinct the rest of this series keeps coming back to.
Try it
SELECT Salary AS AnnualSalary
FROM Employees
WHERE AnnualSalary > 50000;