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;That is also the whole WHERE versus HAVING distinction: WHERE filters
rows before grouping, HAVING filters groups after. Putting a condition in
HAVING that could have gone in WHERE means the engine grouped rows it was
about to discard.
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, BUFFERS)
SELECT * FROM books WHERE author = 'Le Guin';Seq Scan on books (cost=0.00..2891.00 rows=1 width=68)
(actual time=0.02..18.4 rows=214 loops=1)
Filter: (author = 'Le Guin'::text)
Rows Removed by Filter: 99786The number that matters is rows=1 estimated against rows=214 actual. The
planner believed one row would match, so a sequential scan looked cheap. Being
wrong by two orders of magnitude is how a query that was fast in staging
becomes a sequential scan in production.
The fix is usually ANALYZE books; rather than rewriting the query — stale
statistics are the most common cause of a bad plan. See
EXPLAIN and EXPLAIN ANALYZE 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 condition inHAVINGthat belongs inWHEREmakes the engine group rows it is about to throw away. - “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.