How Postgres executes a query
A query passes through five stages — parse, analyze, rewrite, plan, execute — before a single row comes back. Knowing the pipeline tells you where time goes and which problems live at which stage.
You type SELECT * FROM orders WHERE id = 42; and a row comes back in a millisecond. It feels atomic — like the database just looked it up. It isn’t atomic at all: your text went through five distinct stages, and each one is where a different class of bug or slowness lives. When a query is slow, the first senior question is always “slow at which stage?”
The five stages
Your SQL string is not run directly. The Postgres backend that owns your connection pushes it through a pipeline:
- Parse — the raw text is checked for syntax and turned into a parse tree. A missing comma or
SELCTdies here. No tables are touched yet; the parser doesn’t even know ifordersexists. - Analyze (semantic) — names are resolved against the catalog. Does
ordersexist? Does it have a columnid? What types are involved? A “column does not exist” error comes from here, not the parser. - Rewrite — rule-based transformations apply. The big one: a query against a view is rewritten to hit the underlying tables; row-level security policies and rules are injected here too.
- Plan / optimize — the planner takes the analyzed query tree and produces a physical plan: which scan (sequential? index?), which join algorithm (nested loop? hash? merge?), in which order. It uses table statistics to estimate row counts and picks the cheapest plan by an internal cost model.
- Execute — the executor walks the chosen plan tree, pulling rows through it node by node, and streams the result back to you.
Together these five stages mean that by the time a row reaches you, it has been validated, resolved, transformed, optimized, and fetched — and each of those steps can fail independently. Skipping any one of them in your mental model leaves you guessing when something goes wrong.
Why the stages matter for debugging
When a query misbehaves, most engineers rewrite it first — and often waste an hour changing text that was never the problem. The pipeline gives you a faster path: match the symptom to the stage, fix at that stage, done.
The pipeline is a diagnostic map. Match the symptom to the stage:
- Syntax error → parse stage. Pure text problem; the schema is irrelevant.
- “relation/column does not exist” → analyze stage. The text is valid SQL but doesn’t match the catalog.
- Query suddenly slow after a data load → plan stage. The planner’s row estimates went stale, so it picked a bad plan. The fix is usually
ANALYZEto refresh statistics — not rewriting the query. - Query always slow, plan looks right → execute stage. The plan is fine but it’s genuinely reading too many pages; the fix is an index or less data, which changes what plans are available.
This is why the senior reflex is EXPLAIN. EXPLAIN <query> shows you the plan the planner chose without running it; EXPLAIN ANALYZE <query> runs it and shows estimated vs actual rows at each node. The gap between estimate and actual is the single most useful number in Postgres performance work — it tells you whether the planner was lied to by stale statistics.
-- See the plan only (fast, no execution):
EXPLAIN SELECT * FROM orders WHERE id = 42;
-- Run it and compare estimate vs reality:
EXPLAIN ANALYZE SELECT * FROM orders WHERE id = 42; ↻ latency You won’t fully read a plan yet — that’s an entire unit later (08-internals-and-tuning) and the databases track covers the planner and execution-plan internals in depth. For now, internalize only the shape: text in, five stages, rows out, and EXPLAIN is your window into stage four.
▸Why this works
Why separate planning from execution at all? Because planning is expensive — for a query joining six tables, the number of possible join orders explodes, and the planner does real combinatorial work to find a cheap one. Postgres pays that cost once and (with prepared statements) can reuse the plan across many executions. It also means the same parsed query can be re-planned later under better statistics. Bundling plan and execute together would forfeit both wins. The databases track’s execution-plans chapter goes deep on the cost model that drives stage four.
You run a query and get 'column "emial" does not exist'. Which stage produced this error?
A query was fast yesterday; today, after a big bulk INSERT, it's 50× slower with the same text. Best first move?
Fill in the blank: EXPLAIN shows you the planner's chosen plan _______ running the query, while EXPLAIN ANALYZE runs it and reports actual timings and row counts.
- 01List the five stages a query passes through, in order, with one word on what each does.
- 02A query is suddenly slow after a bulk load even though its text is unchanged. Which stage is the culprit and what's the fix?
- 03What is the difference between EXPLAIN and EXPLAIN ANALYZE, and why does the estimate-vs-actual gap matter?
A query is not looked up atomically — it crosses a five-stage pipeline: parse (syntax to tree), analyze (resolve names against the catalog), rewrite (expand views, inject row-level security), plan (the cost-based optimizer turns the query tree into a physical plan of scans and joins), and execute (the executor walks that plan and streams rows). The value of knowing the pipeline is diagnostic: a syntax error is parse, a missing-column error is analyze, a sudden post-load slowdown is the plan stage starved of fresh statistics (fix with ANALYZE), and a chronically slow-but-correct plan is the execute stage doing too much I/O (fix with an index). EXPLAIN shows the chosen plan; EXPLAIN ANALYZE runs it and exposes the estimate-vs-actual gap. Now when you see a sudden regression after a bulk load, you won’t rewrite SQL — you’ll reach for ANALYZE, check the estimate-vs-actual gap in EXPLAIN ANALYZE, and fix at the right stage.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.