Blog›SQL Order of Execution: How Queries Actually Run
SQL SkillsGetting Started
The Order SQL Actually Runs Your Query
You read a query from SELECT down. The database runs it in a completely different order, starting with FROM and reaching SELECT almost last. Press play and watch a real query run, stage by stage.
CR
Conor Robertson
July 9, 2026 · 8 min read
Frequently asked questions
The questions that come up most once people realise their query runs in a different order than they wrote it.
SQL runs a query in a fixed logical order that is different from how you write it: FROM and JOIN first, then WHERE, GROUP BY, HAVING, SELECT, ORDER BY, and finally LIMIT. You write SELECT at the top, but it is evaluated fifth, after the rows have been filtered and grouped. This order is why a column alias created in SELECT cannot be used in WHERE, and why aggregates and window functions cannot be filtered there either.
The logical execution order is: 1) FROM and JOIN, 2) WHERE, 3) GROUP BY, 4) HAVING, 5) SELECT, 6) ORDER BY, 7) LIMIT / OFFSET. So the database builds and filters the row set first, groups it, filters the groups, then computes the SELECT columns, sorts them, and finally trims to the LIMIT. The clause you write first, SELECT, is the fifth thing to actually run.
Because WHERE runs at step 2 and the alias is not created until SELECT at step 5. When WHERE is evaluated, the alias simply does not exist yet, so the database throws an unknown-column error. The fix is to repeat the full expression in WHERE, or to compute the alias in a CTE or subquery and filter it in the outer query. An alias does work in ORDER BY, because ORDER BY runs at step 6, after SELECT.
Window functions are calculated during the SELECT step (step 5), which runs after WHERE (step 2). At the point WHERE is evaluated, the window result, such as a ROW_NUMBER or RANK, has not been computed yet, so you cannot filter on it. Wrap the query in a CTE or subquery and filter in the outer query, or use QUALIFY in databases that support it (Snowflake, BigQuery, DuckDB).
WHERE filters individual rows before grouping (step 2), so it cannot see aggregates. HAVING filters whole groups after grouping (step 4), so it can use COUNT, SUM and other aggregates. Rule of thumb: filter raw rows with WHERE, filter aggregated groups with HAVING. Writing WHERE COUNT(*) > 1 fails because WHERE runs before GROUP BY has produced any groups to count.
It comes down to timing. ORDER BY runs at step 6, after SELECT has created the alias at step 5, so the alias exists and is usable. WHERE runs at step 2, long before SELECT, so the alias does not exist yet. Same alias, different steps: that is the whole reason one works and the other errors.
Not exactly. The order described here is the logical query processing order that defines what a query means. The actual engine is free to reorder operations for speed, for example applying a filter earlier using an index, as long as the final result matches what the logical order would produce. So you reason about correctness with the logical order, while the optimiser handles physical performance behind the scenes.
Window functions run in the SELECT step (step 5), after WHERE, GROUP BY and HAVING, and before ORDER BY and LIMIT. That placement is the reason you cannot reference a window function in WHERE or GROUP BY, but you can sort by one in ORDER BY. To filter on a window result you compute it first (in a CTE or subquery) and filter in an outer query.
No. The logical order of execution is part of the SQL standard and is the same in PostgreSQL, MySQL, SQL Server, SQLite, BigQuery, Snowflake and Redshift. The only differences are surface syntax for the final step, such as LIMIT versus TOP or OFFSET ... FETCH, and extras like QUALIFY for filtering window results. The order the clauses evaluate in does not change.
DISTINCT runs after SELECT has computed the columns (so, between SELECT at step 5 and ORDER BY at step 6). It removes duplicate rows from the projected result, which is why DISTINCT applies to the full set of selected columns, not just the first one. Because it runs after SELECT, a SELECT alias is visible to it.
Ready to practice on real data?
QueryCase teaches SQL through mystery cases on real datasets. Music, sports, film, and more. Your first cases are completely free.
You read it SELECT first. The engine runs it FROM first, and reaches SELECT only at step 5.
after FROM7rows
namesquadxp
FoxAlpha3,950
RavenAlpha3,200
ValaAlpha1,500
OwliverBravo2,100
SterlingBravo1,450
QuillBravo600
KestrelGamma1,800
✓ rows✗ COUNT(*)✗ cases_closed
FROM
The database starts here, not at SELECT. It loads every row from the source table before anything else can happen.
Almost everyone writes a SQL query the same way: SELECT first, then FROM, then the filters. So it is natural to assume that is the order it runs in. It is not. The database ignores the order you typed and follows its own, starting with FROM and reaching SELECT near the end.
This is not trivia. The order of execution is the single mental model that explains a whole category of confusing errors: why an alias works in ORDER BY but not WHERE, why HAVING exists at all, and why you cannot filter on a window function. Press Run above and watch a real query move through its seven stages. Notice the numbers down the left of the query: that is the order the clauses actually fire, and they are not 1 to 7 top to bottom.
Once you can see those stages, the rules stop being things to memorise and start being obvious.
You write it in one order, SQL runs it in another
Here is the order the database actually follows, as a staircase from first step to last. You typed SELECT at the very top of your query, yet it does not run until step five, after the rows have been filtered and grouped. Only sorting and trimming happen after it.
The reason it works this way is logical, not arbitrary. You cannot filter groups before you have made the groups. You cannot compute a SELECT column that averages a group before the group exists. So the database has to build the row set (FROM), narrow it (WHERE), gather it into groups (GROUP BY), filter those groups (HAVING), and only then work out what each output row looks like (SELECT). Sorting and trimming come last because they only make sense once the final rows exist.
Logical order, not physical order
One honest caveat. This is the logical order of execution: the sequence that defines what your query means. The engine's optimiser is free to shuffle the physical work for speed, like using an index to filter earlier, as long as the result is identical to what this order would produce. So you reason about correctness with the order below, and let the database worry about performance. For every rule in this post, the logical order is the one that matters.
Why this explains half your SQL errors
Now the payoff. Nearly every "why won't this run?" moment with filtering comes down to one rule: a clause can only see what earlier steps have already produced. Pick a query below and see exactly which step trips it up.
SELECT xp * 2 AS double_xp
FROM detectives
WHEREdouble_xp > 5000
✗ ERRORon double_xp
WHERE runs at step 2, but the alias double_xp is not created until SELECT at step 5. It does not exist yet when WHERE looks for it.
FixRepeat the expression: WHERE xp * 2 > 5000. Or compute it in a CTE and filter the outer query.
Every one of these is the same story told with different clauses. WHERE runs early, so it cannot see aliases, aggregates or window functions, because none of them exist yet. HAVING and ORDER BY run late, so they can. That timing gap is exactly the difference between WHERE and HAVING: one filters rows before grouping, the other filters groups after. Learn the order once and you stop guessing which filter goes where.
The full order, and what each step can see
Here is the whole sequence in one table. The last column is the one to internalise, because "what a step can see" is what decides whether your query runs or errors.
That is the entire model. FROM builds, WHERE filters rows, GROUP BY gathers, HAVING filters groups, SELECT computes, ORDER BY sorts, LIMIT trims. Seven steps, always the same order, in every major database.
The fastest way to lock this in is to use it. Next time a query throws an "unknown column" or "cannot use aggregate here" error, do not guess. Find the clause, ask which step it runs at, and ask whether the thing it references exists yet. Nine times out of ten the answer is right there. Once you have it, JOIN, CASE and window functions all stop being separate rules to remember and become one pipeline you can see.
SQL Order of Execution: How Queries Actually Run | QueryCase