Modules/Database/ Execution Order Lab

Query Execution Order

You type SELECT first. The database runs it fifth. Step through the real order and watch the row count change at every stage.

Overview

Written order versus execution order

A query is written in this order:

SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT

It is executed in this one:

FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT

The database first works out which rows it is dealing with (FROM and JOIN), filters them (WHERE), groups what survives (GROUP BY), filters the groups (HAVING), and only then computes the output columns (SELECT). Sorting and limiting happen last, on the finished result.

Step Through It

Press Step to run the first stage: FROM.

Try Breaking It

The Query, Written

The Order It Actually Runs

Rows At This Stage

Row Count

Surviving Rows
8

 

Query Execution Order in SQL: A Practical Guide

SQL is written SELECT first and executed SELECT almost last. Nearly every confusing SQL error - unknown alias, aggregate in WHERE - is explained by that gap.

The two errors this explains

"Column does not exist" on an alias in WHERE:

SELECT price * quantity AS line_total
FROM   order_lines
WHERE  line_total > 100;          -- error

WHERE runs at step 2 and SELECT at step 5, so line_total does not exist yet. Repeat the expression, or wrap the query:

SELECT * FROM (
  SELECT price * quantity AS line_total FROM order_lines
) t WHERE line_total > 100;

"Aggregate not allowed in WHERE":

SELECT   customer_id, SUM(total)
FROM     orders
WHERE    SUM(total) > 1000        -- error
GROUP BY customer_id;

The sum is produced at step 3; WHERE runs at step 2. HAVING is the clause that runs after grouping, and is therefore the one that can see it.

The mirror image is why ORDER BY can use an alias: it runs at step 6, after SELECT has created it.

WHERE and HAVING are not interchangeable

Both filter, at different stages, and the distinction matters for correctness as well as speed:

  • WHERE filters individual rows, before grouping. Excluded rows never reach the aggregate.
  • HAVING filters groups, after aggregation. All rows contribute to the aggregate; whole groups are then discarded.
WHERE status = 'paid' computes totals from paid orders only. HAVING SUM(amount) > 1000 computes totals from every order and keeps the customers whose total exceeds 1000. Moving a condition between them changes the answer.

When a condition could go in either — a plain column filter — put it in WHERE. Filtering earlier means fewer rows to group, which is almost always faster.

Written order versus the order things happen

SQL is written in an order designed to read like English and executed in a different one. Almost every confusing error message in SQL comes from that mismatch.

WrittenExecuted
1. SELECT5. SELECT
2. FROM / JOIN1. FROM / JOIN
3. WHERE2. WHERE
4. GROUP BY3. GROUP BY
5. HAVING4. HAVING
6. ORDER BY6. ORDER BY
7. LIMIT7. LIMIT

So the real sequence is: assemble the rows, filter them, group them, filter the groups, work out what to display, sort, then cut.

(Strictly, this is the logical order. The planner may physically do things in another order entirely — pushing a filter down into an index scan, for instance — as long as the result is what this sequence would produce.)

Following one query through all seven steps

SELECT   c.country, COUNT(*) AS orders, SUM(o.total) AS revenue
FROM     orders o
JOIN     customers c ON c.id = o.customer_id
WHERE    o.status = 'complete'
GROUP BY c.country
HAVING   SUM(o.total) > 5000
ORDER BY revenue DESC
LIMIT    5;
  1. FROM/JOIN — build the combined set of order-and-customer rows. If 50,000 orders match 8,000 customers, this is where the row count is decided.
  2. WHERE — drop everything not complete. Say 30,000 rows survive.
  3. GROUP BY — collapse those into one row per country. Perhaps 40 rows now.
  4. HAVING — drop countries under 5,000 in revenue. Perhaps 12 rows.
  5. SELECT — compute and name the output columns, including the aliases orders and revenue.
  6. ORDER BY — sort those 12 rows by revenue, using the alias created one step earlier.
  7. LIMIT — keep the top 5.

Reading it this way answers most "why is this slow?" questions too: the expensive steps are the ones operating on the largest row counts, which are almost always the join and the filter. Narrowing step 2 helps everything after it.

Exploration guide

  1. Step through a query. Press Step repeatedly and watch which clause runs at each stage. FROM produces the rows, WHERE removes some, GROUP BY collapses the rest, and SELECT is near the end.
  2. Break it deliberately. Enable the option that uses a SELECT alias in WHERE. It fails, because at that point the alias has not been created.
  3. Then use the same alias in ORDER BY. Enable that option instead. It works — same alias, different clause, and the execution order is the only reason.
  4. Reset and compare. Press Reset and run a working query, watching how many rows survive each stage. Filtering early leaves far less work for everything downstream.

What usually goes wrong

  • Using a SELECT alias in WHERE or GROUP BY. Repeat the expression, or wrap the query in a subquery or CTE where the alias already exists.
  • Putting an aggregate in WHERE. Use HAVING.
  • Using HAVING for row filters. Correct results, more work — the rows are grouped before being discarded.
  • Expecting LIMIT to speed up an aggregate. LIMIT runs last, so the database has already grouped and sorted everything before it truncates.
  • Filtering an outer join in WHERE. A condition on the right-hand table in WHERE discards the NULL rows and silently turns the LEFT JOIN into an INNER JOIN. Put it in the ON clause instead.

Key takeaway

SQL executes FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT — not the order it is written in. That single fact explains why SELECT aliases work in ORDER BY but not WHERE, why aggregates belong in HAVING rather than WHERE, and why filtering early is faster. When a query does something surprising, walking it through the execution order usually answers it in one step.

Where the other clauses fit

Several things do not appear in the classic seven-step list and need placing.

  • DISTINCT runs after SELECT and before ORDER BY. That is why SELECT DISTINCT x FROM t ORDER BY y fails in strict engines — y was removed before the sort could use it.
  • Window functions are computed after HAVING and before DISTINCT. They can see the grouped rows, and WHERE/HAVING cannot see them. Filtering on a window function always needs an outer query.
  • CTEs (WITH) are conceptually evaluated first, as named inputs to the main query, though modern planners inline them and optimise across the boundary.
  • OFFSET runs with LIMIT, at the very end, after everything has been produced and sorted — which is exactly why deep offsets are slow.

Putting those together, the fuller order is:

FROM → WHERE → GROUP BY → HAVING → window functions → SELECT → DISTINCT → ORDER BY → LIMIT/OFFSET

What the planner is allowed to change

The logical order defines the result, not the physical work. The optimiser rewrites freely as long as the answer is identical:

  • Predicate pushdown — a WHERE condition can be applied while scanning a table, before the join, rather than after it.
  • Join reordering — inner joins can be executed in any order; the planner picks the one with the smallest intermediate results.
  • Index-provided ordering — if an index already returns rows in the requested order, step 6 costs nothing.
  • Early termination — with LIMIT and a matching index, the engine can stop as soon as it has enough rows, without producing the full result.

This is why EXPLAIN output does not look like the seven steps: it shows the physical plan. The logical order is what you reason about; the plan is what actually ran.

The order it is written in is not the order it runs

SQL is written SELECT first and evaluated SELECT almost last. Most of the confusing errors about aliases and aggregates follow from that -- and this engine is more forgiving than the standard, in a way worth knowing before you move the query somewhere stricter.

query.sqlSQLite
Result

Questions people ask

Why can ORDER BY use an alias but WHERE cannot? Because SELECT (which creates aliases) runs after WHERE and before ORDER BY.

Does LIMIT make the query faster? Sometimes dramatically — with a matching index the engine can stop early. Without one, it may still have to produce and sort everything first.

Is SELECT * slower? It transfers more data and can prevent index-only scans, so on wide tables, yes. Name the columns you need.

Where do subqueries fit? A subquery in FROM is evaluated as part of step 1; one in WHERE as part of step 2; a correlated one runs in the context of the row being tested.

Does this order apply to every database? The logical order is standard and consistent across engines. Which relaxations they allow — aliases in GROUP BY, for instance — varies.

Why does my query return more rows after adding a join? Because step 1 happens first, and a one-to-many join multiplies rows before any filtering or grouping gets a chance to run.

Recap in one screen

  • Execution order is FROM, WHERE, GROUP BY, HAVING, window functions, SELECT, DISTINCT, ORDER BY, LIMIT.
  • Aliases created in SELECT are unavailable in WHERE and HAVING, and available in ORDER BY.
  • Aggregates do not exist until GROUP BY has run, which is why they belong in HAVING.
  • The join happens first, so it decides the row count everything else works on.
  • The planner may reorder the physical work freely — read EXPLAIN for what actually happened.

Predict, then reveal

About to run: Step through a query. Before it does — what happens to the readout?

Committing to an answer first is the point — the reveal runs the experiment on the visualisation above and reads the real value back, so nothing here is scripted.

Recall check

0 of 4

Say the answer out loud before you reveal it — recalling it is what makes it stick, and rereading it is not.

  1. What is meant by “DISTINCT” here?

  2. What is meant by “Window functions” here?

  3. What is meant by “CTEs ( WITH )” here?

  4. What is meant by “OFFSET” here?

Cheat sheet

Query Execution Order in SQL

The database first works out which rows it is dealing with (FROM and JOIN), filters them (WHERE), groups what survives (GROUP BY), filters the groups (HAVING), and only then computes the output columns (SELECT). Sorting and limiting happen last, on the finished result.

DATABASE · vizlearn.in/database/query_execution_order.html

Further reading

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.