Article

SQL Execution Order Explained (With a Real Query Example)
SQL doesn't run in the order you write it. This post walks through the real logical execution order — FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT — using one worked query example, and explains the WHERE-vs-HAVING mistake it fixes for good.
We write SQL as SELECT, FROM, WHERE, GROUP BY, ORDER BY — but that's not the order it actually runs in.
The real logical execution order is:
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMITKnowing this order explains most of SQL's "gotchas": why WHERE can't use COUNT(), why HAVING exists separately from WHERE, and why ORDER BY can use a SELECT alias but WHERE can't.
Example query:
sql
SELECT department, COUNT(*) AS employee_count
FROM employees
WHERE salary > 50000
GROUP BY department
HAVING COUNT(*) >= 2
ORDER BY employee_count DESC
LIMIT 3;Walking through it:
FROM — pulls all rows from
employees.WHERE — drops employees earning ≤50,000, one row at a time, before any grouping happens.
GROUP BY — clusters the surviving rows by department, so per-group math like
COUNT(*)becomes possible.HAVING — filters entire groups, not rows — keeping only departments with 2+ employees. This is why aggregate conditions can't live in
WHERE: groups don't exist yet at that stage.SELECT — picks which columns make the output, including the alias
employee_count.ORDER BY — sorts using that alias, which only works because
SELECTalready ran.LIMIT — caps the final row count.
The rule that matters most:
WHERE asks which rows? — HAVING asks which groups?
That single distinction fixes one of the most common SQL bugs: putting an aggregate condition in WHERE instead of HAVING.
sql
-- Wrong: WHERE runs before grouping exists
SELECT department, COUNT(*)
FROM employees
WHERE COUNT(*) >= 2
GROUP BY department;
-- Right: HAVING runs after grouping
SELECT department, COUNT(*)
FROM employees
GROUP BY department
HAVING COUNT(*) >= 2;One caveat: this is the logical order — real query engines (MySQL, Postgres, SQL Server) may optimize execution internally. But for reasoning about what a query means, this order is exactly what to picture.
Next time you write a query, mentally run it through FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT before you hit execute — it catches more bugs than the database will.