DEV Community

Cover image for SQL Query Execution Order: Why Your Query Doesn't Run Top-to-Bottom
Neha Christina
Neha Christina

Posted on

SQL Query Execution Order: Why Your Query Doesn't Run Top-to-Bottom

If you've ever written a query like this and gotten a baffling error, this post is for you:

SELECT
    customer_id,
    SUM(amount) AS total
FROM orders
WHERE total > 100
GROUP BY customer_id;
Enter fullscreen mode Exit fullscreen mode
ERROR: column "total" does not exist
Enter fullscreen mode Exit fullscreen mode

Your first reaction is probably "but I just defined total right there in the SELECT." Totally reasonable reaction — and totally wrong assumption about how SQL works under the hood.

SQL reads like English, top to bottom: SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY. But that's the order you write it in, not the order the database executes it in. Once you internalize the real execution order, a whole category of beginner SQL errors stops being mysterious.

The order you write vs. the order SQL runs

Here's the logical processing order most SQL engines (Postgres, MySQL, SQL Server, Snowflake, etc.) actually follow:

  1. FROM / JOIN — figure out which tables you're working with and build the initial row set
  2. WHERE — filter individual rows, before any grouping happens
  3. GROUP BY — collapse the remaining rows into groups
  4. HAVING — filter the groups (this is where aggregates like SUM, COUNT, AVG become usable)
  5. SELECT — compute the actual output columns, including aliases
  6. DISTINCT — remove duplicate rows from the result
  7. ORDER BY — sort the final result set
  8. LIMIT / OFFSET — cut the result down to the rows requested

Notice SELECT is step 5, not step 1. That single fact is the root cause of most of the "why doesn't this work" confusion below.

Why WHERE can't see your alias (or your aggregates)

Go back to the broken query:

SELECT
    customer_id,
    SUM(amount) AS total
FROM orders
WHERE total > 100          -- ❌ fails
GROUP BY customer_id;
Enter fullscreen mode Exit fullscreen mode

By the logical order above, WHERE (step 2) runs before SELECT (step 5). At the point WHERE executes, the alias total simply doesn't exist yet — it hasn't been computed. WHERE also runs before GROUP BY (step 3), so it has no concept of aggregated values at all. It only ever sees one raw row at a time.

This is also why you can't write WHERE SUM(amount) > 100 — there's no group to sum yet when WHERE runs.

The fix: filter the group with HAVING

If you want to filter on an aggregated value, you need HAVING, which runs after GROUP BY:

SELECT
    customer_id,
    SUM(amount) AS total
FROM orders
GROUP BY customer_id
HAVING SUM(amount) > 100;   -- ✅ works
Enter fullscreen mode Exit fullscreen mode

HAVING operates on groups, not individual rows, so it has access to aggregate functions. Note that most databases require you to repeat the aggregate expression in HAVING rather than reference the SELECT alias — because, again, SELECT hasn't run yet at that point in the logical order. (Some engines, including Snowflake, are lenient about allowing the alias in HAVING as an extension — but relying on standard behavior will keep your SQL portable.)

The rule of thumb:

  • Filtering raw rows, before grouping → WHERE
  • Filtering aggregated groups, after grouping → HAVING

So why does ORDER BY get to use the alias?

Here's the part that trips people up next: if aliases don't exist until SELECT runs, why does this work fine?

SELECT
    customer_id,
    SUM(amount) AS total
FROM orders
GROUP BY customer_id
ORDER BY total DESC;        -- ✅ works
Enter fullscreen mode Exit fullscreen mode

Because ORDER BY is step 7 — it runs after SELECT (step 5). By the time sorting happens, the column alias total has already been computed and is sitting right there in the result set, ready to be referenced. Same underlying rule, opposite outcome, because of where each clause sits in the execution order.

A quick note on GROUP BY and aliases: standard SQL technically doesn't guarantee alias support in GROUP BY either, since it also runs before SELECT. Several engines (Snowflake included) allow it as a convenience, but don't assume every database will.

Does SELECT * change any of this?

No — SELECT * is still step 5 in the logical order, it just means "give me every column that exists at that point" instead of a specific list. The execution order itself doesn't change.

Putting it together: the full flow

Here's a slightly bigger example that touches every clause, with the logical order annotated:

SELECT                              -- 5. compute columns/aliases
    customer_id,
    SUM(amount) AS total
FROM orders                         -- 1. start with this table
JOIN customers USING (customer_id)  -- 1. combine rows
WHERE order_date >= '2026-01-01'    -- 2. filter raw rows
GROUP BY customer_id                -- 3. collapse into groups
HAVING SUM(amount) > 100            -- 4. filter groups
ORDER BY total DESC                 -- 7. sort final result
LIMIT 10;                           -- 8. trim to top 10
Enter fullscreen mode Exit fullscreen mode

Even though you type SELECT first and LIMIT last, the database works through this roughly as: build the joined rows → keep only matching rows → group them → keep only matching groups → compute the output → sort it → trim it.

Cheat sheet

  • Need to filter individual rows before grouping? Use WHERE.
  • Need to filter aggregated groups? Use HAVING.
  • Want to reference a SELECT alias? Safe in ORDER BY (and often GROUP BY, engine-dependent) — never safe in WHERE.
  • Written order: SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY
  • Logical execution order: FROM → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT

Wrapping up

This is one of those SQL fundamentals that feels obvious in hindsight but trips up almost every junior dev at least once — usually right before an interview or a production deploy. Once the logical order clicks, errors like "column does not exist" or "aggregate functions are not allowed in WHERE" stop feeling random and start making total sense.

If this was useful, I post SQL, Python, and Snowflake breakdowns like this regularly over on Instagram @techqueen.codes — come say hi.

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.