Visual walkthrough — SQL DML — SELECT, INSERT, UPDATE, DELETE
We will slowly follow one query through the machine. Here it is — don't try to understand it yet, we'll earn every word:
SELECT dept, AVG(salary) AS avg_pay
FROM employees
WHERE salary > 40000
GROUP BY dept
HAVING AVG(salary) > 60000
ORDER BY avg_pay DESC
LIMIT 1;Our data is the same 4-row table from the parent note:
| id | name | dept | salary |
|---|---|---|---|
| 1 | Asha | Sales | 50000 |
| 2 | Ben | Eng | 70000 |
| 3 | Cira | Eng | 90000 |
| 4 | Dia | Sales | 40000 |
Step 1 — The problem: written order ≠ run order
WHAT: We notice that the very first word you write — SELECT — is almost the last thing the database does.
WHY it matters: Every confusing SQL rule ("why can't I filter on my alias?", "why does WHERE reject AVG()?") comes from this mismatch. If you know which stage runs first, you can derive the rule instead of memorizing it.
PICTURE: The left column is the order your fingers type; the right column is the order the machine runs. The crossing arrows are exactly where the confusion lives.

Look at the red arrow: SELECT (typed 1st) executes 5th. That single crossing is the whole reason this page exists.
Step 2 — FROM: put the raw rows on the table
WHAT: The engine loads all 4 rows of employees. Nothing is filtered, nothing is grouped.
WHY first: You cannot filter, group, or sort rows you haven't fetched yet. FROM is the only stage that produces rows out of thin air (technically, off the disk); every later stage only shrinks or reshapes what FROM gave it.
PICTURE: All four rows sit on the conveyor belt, in the blue "input" tray. This is the material the rest of the machine sculpts.

FROM— the verb: "fetch rows".employees— the noun: which pile of rows to fetch. Result = all 4 rows.
Step 3 — WHERE: keep only rows that pass a per-row test
WHAT: We test each row against salary > 40000.
WHY here, before grouping: WHERE looks at individual rows. It runs before any grouping because grouping should only bucket the rows we actually want to keep — filtering first means less work later, and it lets us throw out rows that would pollute a group's average.
WHY WHERE cannot say AVG(salary): an average is a property of a group of rows, but at this stage groups don't exist yet — we're still looking at one lonely row. That's the derivation of the rule "no aggregates in WHERE."
PICTURE: Each row is checked. Dia (40000) fails > 40000 (40000 is not greater than 40000 — watch that strict >), so she is dropped in red. The other three continue in green.

Survivors: Asha (50000), Ben (70000), Cira (90000).
Step 4 — GROUP BY: collapse rows into buckets
WHAT: We bucket the 3 survivors by dept. Sales → {Asha}. Eng → {Ben, Cira}.
WHY: We want one answer per department, not per person. Grouping is the moment "3 rows" becomes "2 groups". This is why aggregate functions like AVG only make sense from here on — now there's a pile to average.
PICTURE: The three green rows fall into two labelled bins. Notice Eng holds two rows; Sales holds one.

Step 5 — HAVING: filter the buckets (this is not WHERE)
WHAT: We compute each bucket's AVG(salary) and keep only buckets over 60000.
- Sales bucket: just Asha → . Is ? No → drop.
- Eng bucket: Ben + Cira → . Is ? Yes → keep.
WHY HAVING exists at all: WHERE already left; it filtered rows and couldn't see averages. We need a second filter that runs after grouping to filter groups. That second filter is HAVING. Same idea (keep the TRUE ones), different unit (groups, not rows).
PICTURE: Two buckets sit on a scale marked at 60000. Sales (50000) tips below and is dropped in red; Eng (80000) clears the bar in green.

Recall
Why can WHERE not do this filtering? ::: WHERE runs before GROUP BY, so at WHERE time no groups (and no averages) exist yet.
Step 6 — SELECT: now, finally, choose the columns
WHAT: From the one surviving group (Eng), we output dept and AVG(salary), naming the average avg_pay.
WHY this is 5th, not 1st: You can only pick from what survived filtering and grouping. Choosing columns first would be meaningless — there'd be nothing chosen-from yet. This late position is exactly why an alias defined here cannot be used back in WHERE (Step 3): WHERE already finished four stages ago, before the name avg_pay was ever born.
PICTURE: The Eng bucket enters the SELECT box; out comes a clean one-row result with the new column heading avg_pay.

Result so far: Eng | 80000.
Step 7 — ORDER BY then LIMIT: sort, then cut
WHAT: ORDER BY avg_pay DESC sorts high→low; LIMIT 1 keeps the top row.
WHY last: Sorting and cutting only make sense on the final shape of the output. Also note ORDER BY can use the alias avg_pay — because ORDER BY runs after SELECT, so by now the nickname exists. Contrast that with WHERE, which ran before SELECT and therefore cannot.
PICTURE: Our result has only one row, so sorting is trivial and LIMIT 1 keeps it. The figure also shows the general case with several rows to make the sort-then-cut visible.

Final answer of the whole query: Eng | 80000.
Step 8 — The degenerate cases (what happens at the edges)
Never leave a reader to hit an untested scenario. Three edges:
PICTURE: The full pipeline with the two optional gears (WHERE, GROUP BY) greyed out, showing rows sliding straight through.

The one-picture summary
Everything above, compressed: the query text on top, the 7 execution stages as a numbered assembly line below, and our 4 rows narrowing to the single final answer Eng | 80000. The crossing arrow (write-order ↔ run-order) is drawn in red — the source of every rule we derived.

Recall Feynman: tell the whole walkthrough to a friend
Imagine a factory belt. First (FROM) we dump all four employee cards onto the belt. Next (WHERE) a guard checks each card one at a time and throws out anyone earning 40000 or less — so Dia is gone, and this guard has never heard of "averages" because he only ever sees one card at a time. Then (GROUP BY) a sorter drops the remaining cards into department bins: one Sales bin, one Eng bin. Now a second guard (HAVING) weighs each bin's average pay and tosses the whole Sales bin because its average (50000) is under 60000; Eng (80000) stays. Only now (SELECT) do we finally decide what to write on the output slip — dept and the average, which we nickname avg_pay. Then (ORDER BY / LIMIT) we sort the slips and keep the top one. The punchline: because the naming happens near the end, the early guard at WHERE could never have used that nickname — he'd already gone home. That's why "no alias in WHERE" isn't a rule to memorize; it's just when things happen.
Connections
- Parent: SQL DML overview — the four verbs this pipeline serves.
- WHERE clause and Predicates — the per-row test in Step 3.
- Aggregate Functions & GROUP BY — the bucketing and HAVING of Steps 4–5.
- SQL Joins — extend the FROM stage across multiple tables.
- Indexes — how FROM/WHERE fetch rows fast.
- Transactions — ACID, COMMIT, ROLLBACK — the safety net around any changes.