You write SELECT first. SQL runs it fifth.
The hidden order your database uses to run every query, and why it explains your most confusing errors.
Every SQL query runs in two different orders.
The one you wrote. And the one your database actually uses to run it.
That’s it. Strip away the syntax, and that’s what’s happening underneath: the clauses you typed top to bottom get quietly reshuffled into a completely different sequence before a single row is touched.
Most people learn to write SQL. Almost no one learns how it runs.
And that gap (between writing order and execution order) is where the confusing errors live. The alias that “doesn’t exist.” The WHERE clause that refuses to filter your COUNT(). The query that flies on 100 rows and crawls on 100 million.
None of it is random. It all traces back to one thing: the order in which the engine actually processes your query.
I’ve written more SQL than I can remember, and I still run that order in my head every time a query surprises me. It’s the single mental model that’s saved me the most debugging time.
Today’s issue breaks down what it means for SQL to be declarative, why writing order and execution order diverge, and what happens at each of the seven steps.
SQL tells the database what, not how
SQL is a declarative language. You describe the result you want; you don’t spell out the steps to compute it.
Think of ordering at a restaurant. You ask for the dish. You don’t walk into the kitchen and dictate which pan gets heated first, when to salt, how to plate it. You declare the outcome, the kitchen decides the sequence.
SQL works the same way. You declare the shape of the result, and the engine’s query planner decides how to get there. Which means it’s free to evaluate your clauses in whatever order is logically correct, regardless of the order you typed them.
So before anything else, it’s worth separating the two orders in your head.
Coding order vs. Execution order
This is the structure you write, almost every time:
SELECT→FROM→WHERE→GROUP BY→HAVING→ORDER BY→LIMIT
And this is the order the engine actually runs it in:
FROM/JOIN→WHERE→GROUP BY→HAVING→SELECT→ORDER BY→LIMIT
Look at where SELECT lands.
You write it first. It runs fifth.
That one detail causes more SQL confusion than anything else, and we’ll come back to exactly why in a moment. This logical sequence even has a name: logical query processing, the term Itzik Ben-Gan has spent a career teaching. The order in which clauses are evaluated, as opposed to the order you read them. Once it clicks, SQL stops feeling arbitrary.
Let’s walk the seven steps.
The seven steps, command by command
#1 — FROM (and JOIN)
The starting point. Before SQL can filter or group anything, it needs the data. FROM identifies the source table, and when your data lives across several tables, JOIN stitches them together right here, first. Everything downstream operates on this assembled result.
#2 — WHERE
Now SQL filters individual rows, keeping only the ones that meet your condition. Crucially, this runs on raw rows, before any grouping exists.
#3 — GROUP BY
SQL collapses rows into groups: one row per distinct value, or combination of values. This is where aggregation lives: COUNT(), SUM(), AVG(), MIN(), MAX().
#4 — HAVING
A second filter, but this one runs after grouping, so it can filter on aggregated values. “Only groups with more than five members” is a HAVING job, never a WHERE job.
#5 — SELECT
Only now does SQL pick the columns you asked for, apply your aliases (AS), and compute final expressions. Fifth. Not first.
#6 — ORDER BY
With the result assembled, SQL sorts it in ascending (ASC) or descending (DESC) order.
#7 — LIMIT
Finally, it trims the output to the number of rows you want. Invaluable when you’re testing against a huge table and don’t want the whole thing back.
Two bugs that suddenly make sense
Here’s the payoff. Two errors every analyst hits, both explained by a single line on that list.
WHEREcan’t filter an aggregate.You try
WHERE SUM(minutes) > 100and SQL rejects it. Of course it does:WHERE(step 2) runs beforeGROUP BY(step 3). At that point the sum doesn’t exist yet. That’s the entire reasonHAVINGexists, it’s the filter that runs after aggregation.A
SELECTalias is invisible toWHERE.You name a column
AS total_time, then try to usetotal_timein yourWHEREclause, and you get an error. The alias is born inSELECT(step 5), three steps afterWHEREalready ran. You’re reaching for something that doesn’t exist yet.
Neither is a quirk. Both fall straight out of execution order.
Watch it run: a worked example
Say a phone company has two tables. CLIENTS holds each customer and their monthly limit. USAGE holds their daily minutes.
The goal: find the clients who blew past their monthly limit.
SELECT DISTINCT
usage.id,
clients.owner_name,
SUM(usage.usage_minutes) AS total_browsing_time,
clients.browsing_minutes_limit
FROM usage
LEFT JOIN clients
ON clients.ID = usage.ID
WHERE usage.Date > “2023-05-01” AND usage.Date < “2023-06-01”
GROUP BY 1,2,4
HAVING SUM(usage.usage_minutes) > clients.browsing_minutes_limit
ORDER BY clients.browsing_minutes_limit DESCYou read that from SELECT down. The engine runs it like this:
FROM+JOINmergeCLIENTSandUSAGEinto one combined table.WHEREdrops every usage record outside of May.GROUP BYrolls each client’s daily rows into a single total.HAVINGkeeps only the clients whose total exceeded their limit — a filter on an aggregate, only possible now.SELECTpicks the columns and surfaces the summed minutes.ORDER BYsorts the offenders by their limit.LIMITwould cap the rows returned (we kept them all here).
Same query. A completely different order from the one on the page — and every step depends on the one before it.
The shift that makes SQL click
Stop reading your queries top to bottom. Read them the way the engine does, and the language stops fighting you.
You’ll know exactly where each clause can reach: why WHERE can’t see your groups, why HAVING can, why your alias works in ORDER BY but not in WHERE. The error messages turn from mysteries into signposts.
Don’t memorize the order — internalize the flow: build the data, filter the rows, group them, filter the groups, then select, sort, and trim. Once you see SQL the way SQL sees itself, the bugs stop being bugs.
Next week: the same order, but with JOINs and a real dataset — where execution order goes from useful to essential.
Here's this week's cheatsheet 👇🏻
—Josep
Are you still here? 🧐
👉🏻 I want this newsletter to be useful, so please let me know your feedback!
Before you go, tap the 💚 button at the bottom of this email to show your support—it really helps and means a lot!
Any doubt? Let’s start a conversation! 👇🏻
Want to get more of my content? 🙋🏻♂️
Reach me on:
LinkedIn and X (Twitter) to get daily posts about Data Science.
My Medium Blog to learn more about Data Science, Machine Learning, and AI.
Just email me at rfeers@gmail.com for any inquiries or to ask for help! 🤓





