How a query is processed
Two orders matter and they are different: the order the engine does its work in before executing anything, and the logical order the clauses evaluate in, which is not the order you wrote them.
The pipeline before execution
SQL text
├─ parse syntax tree; a syntax error stops here
├─ bind resolve names against the catalogue
├─ rewrite expand views, apply rules
├─ plan choose access paths and join order by cost
└─ execute run the physical planThe plan stage is where performance is decided. Two queries returning identical rows can differ by a factor of a thousand because the planner chose a sequential scan over an index, or a nested loop over a hash join. Nothing you do in application code affects that; the statistics do.
The logical order of clauses
Written order and evaluation order barely overlap:
| Written | Evaluated |
|---|---|
SELECT |
5 |
FROM / JOIN |
1 |
WHERE |
2 |
GROUP BY |
3 |
HAVING |
4 |
ORDER BY |
6 |
LIMIT |
7 |
This is not trivia — it explains rules people otherwise memorise:
SELECT author, count(*) AS n
FROM books
WHERE n > 1 -- ERROR: column "n" does not exist
GROUP BY author;WHERE runs at step 2 and the alias n is not created until step 5, so it does
not exist yet. The same alias works in ORDER BY, which runs at step 6:
SELECT author, count(*) AS n
FROM books
GROUP BY author
HAVING count(*) > 1 -- filter groups, step 4
ORDER BY n DESC -- alias is fine here, step 6
LIMIT 10;WHERE versus HAVING
That is also the whole WHERE versus HAVING distinction: WHERE filters
rows before grouping, HAVING filters groups after. A non-aggregate
condition written in HAVING costs nothing extra in PostgreSQL, which moves
it into the scan (SQL fundamentals); it
belongs in WHERE for the reader.
PostgreSQL and MySQL also accept a SELECT alias in GROUP BY
(GROUP BY c), an extension to the standard; HAVING n > 1 still fails
with column "n" does not exist.
Gotcha: the logical order is a semantic definition, not a promise about execution. The planner reorders freely as long as the result is identical — it will push a
WHEREpredicate down into an index scan, or stop early on aLIMIT. Do not reason about performance from this table.
Reading what it actually did
EXPLAIN ANALYZE
SELECT c.name FROM orders2 o JOIN customer c ON c.id = o.customer_id
WHERE o.status = 'pending';Nested Loop (rows=1) (actual rows=49975.00 loops=1)
-> Seq Scan on orders2 o (rows=1) (actual rows=49975.00 loops=1)
-> Index Scan using customer_pkey on customer c
(actual rows=1.00 loops=49975)The number that matters is rows=1 estimated against rows=49975 actual.
Believing one order would match, the planner chose a nested loop that probes
the customer index once per order, the right plan for one row and the wrong
one for fifty thousand: 150,466 buffers instead of 1,082. The statistics had
been taken when the table held 50 rows.
The fix is usually ANALYZE orders2; rather than rewriting the query — stale
statistics are the most common cause of a bad plan. See
Reading a query plan and
VACUUM, autovacuum, table bloat, and transaction wraparound.
Related
Interview angle 6
- “What happens between submitting SQL and getting rows?” - parse into a syntax tree, bind and validate against the catalogue, rewrite (views, rules), plan and optimise by estimated cost, then execute. The optimiser stage is where performance is decided.
- “What is the logical order of evaluation?” -
FROM,WHERE,GROUP BY,HAVING,SELECT,ORDER BY,LIMIT- not the written order. That is why aSELECTalias cannot be used inWHEREbut can inORDER BY. - “
WHEREorHAVING?” -WHEREfilters rows before grouping,HAVINGfilters groups after. A non-aggregate condition inHAVINGis moved into the scan by PostgreSQL, so it is a readability issue, not a cost. - “Why does the planner pick a bad plan?” - stale or insufficient statistics, so its row estimates are wrong. A sequential scan where you expected an index usually means it estimated the result set as large. Refresh statistics before rewriting the query.
- “How do you read a plan?” -
EXPLAIN ANALYZEand compare estimated against actual rows. A large discrepancy points at statistics; the most expensive node and the join method tell you where the time went. - “Does the logical order tell you how it executes?” - no. It defines the semantics; the planner reorders freely as long as the result is identical, pushing predicates into scans and stopping early on a
LIMIT.