The SELECT pipeline: order of operations
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT. Knowing this order is the difference between guessing and reasoning.
Why the order matters
The most common SQL bug after NULL handling is "I wrote my SELECT in the wrong mental order". Beginners read SQL top-to-bottom (SELECT ... FROM ... WHERE ...) and assume that's the execution order. It isn't. Knowing the logical order — and the gap between it and the typed order — is the difference between guessing and reasoning about every query you'll ever write.
Logical execution order
FROM pick rows from the source(s)
↓
WHERE filter rows BEFORE aggregation ◄── SELECT aliases NOT visible
↓
GROUP BY collapse rows into groups
↓
HAVING filter groups AFTER aggregation
↓
SELECT evaluate expressions, name aliases ──▶ aliases born here
↓
ORDER BY sort the result ◄── aliases ARE visible
↓
LIMIT take a slice (server-side, not client-side)
This is the logical order — what the query means. The optimizer can execute it differently (push filters down, reorder joins) as long as the result is identical; Module 6 covers that.
The arrows on WHERE and ORDER BY are the alias-visibility rule. WHERE runs before SELECT, so an alias defined in SELECT doesn't exist yet — Postgres errors with "column 'net' does not exist". ORDER BY runs after SELECT, so it can name the alias. Most SQL bugs come from reading the query in typed order instead of this logical order.
Alias visibility is asymmetric. A column alias defined in SELECT is not visible in WHERE (because WHERE runs before SELECT). It is visible in ORDER BY (which runs after).
-- Error: column "net" does not exist
SELECT total_cents * 0.9 AS net FROM orders WHERE net > 1000;
-- Works — ORDER BY runs after SELECT
SELECT total_cents * 0.9 AS net FROM orders ORDER BY net;
The fix: repeat the expression, wrap in a CTE (WITH), or use a subquery. Postgres will not silently let you reuse the alias in WHERE no matter how much you wish it would.
WHERE vs HAVING — finally
WHERE filters rows. HAVING filters groups.
WHERE total_cents > 1000works on every order, before aggregation.HAVING SUM(total_cents) > 1000works on the aggregated total per group, after aggregation.
If you WHERE SUM(total_cents) > 1000, Postgres errors with "aggregate functions are not allowed in WHERE". That error message is the database telling you the order of operations: aggregation hasn't happened yet.
Return the customers whose total **paid** revenue is at least 6000 cents. Output: `customer_id`, `total_cents` (sum across paid orders), sorted by `total_cents` descending. *No customer with cancelled or refunded orders should sneak into the total.*
Which clause is `LIMIT 10` evaluated *after*?
Quiz type not yet supported in MVP.
Takeaway: read every query in logical order, not typed order. Three rules of thumb:
- WHERE is for rows; HAVING is for groups.
- SELECT aliases are visible in ORDER BY but not in WHERE.
- LIMIT without ORDER BY returns some rows, not the first — the optimizer is free to pick whatever's fastest.