Module 16 · Query Optimization Home · Engineering Guide · Cheatsheet · Troubleshooting
Lesson 01 of 12 · Next: EXPLAIN → · SQL Lab
Before you can optimize a query, you need a mental model of what actually happens to it after you hit “run.” Most SQL users think of a query as a single step: text in, rows out. In reality, a query passes through five distinct stages, and almost every performance problem traces back to one specific stage.
FROM → WHERE → GROUP BY → ...) and the physical execution
order the optimizer actually choosesYou cannot read an execution plan, reason about an index, or explain why a rewritten query is faster if you don’t know what stage of the lifecycle you’re influencing. “Add an index” changes stage 3. “Rewrite the subquery as a JOIN” changes stage 2. Skipping this lesson means every later optimization technique feels like a memorized trick instead of a logical consequence.
Imagine a payments team query used inside a fraud-review dashboard:
SELECT e.emp_name, d.dept_name, COUNT(*) AS flagged_transactions
FROM employes e
JOIN departments d ON e.dept_id = d.dept_id
WHERE d.dept_name = 'Fraud Review'
GROUP BY e.emp_name, d.dept_name
ORDER BY flagged_transactions DESC;
At a startup with 200 employee rows, this runs instantly regardless of how it’s written. At a bank with 40 million transaction-linked rows behind a similar query, the exact same SQL can take 30 seconds or 300 milliseconds depending entirely on what happens during the five lifecycle stages below.
┌───────────┐ ┌───────────┐ ┌────────────┐ ┌───────────┐ ┌─────────┐
│ Parsing │ → │ Binding │ → │Optimization│ → │ Execution │ → │ Fetch │
└───────────┘ └───────────┘ └────────────┘ └───────────┘ └─────────┘
flowchart LR
A[SQL Text] --> B[Parsing]
B --> C[Binding]
C --> D[Optimization]
D --> E[Execution]
E --> F[Result Set]
D -.-> G[Statistics & Catalog]
G -.-> D
The engine checks your SQL for syntactic correctness — matching keywords,
valid clause order, balanced parentheses. It has no idea yet whether
departments exists or whether dept_name is spelled correctly. This
stage produces a parse tree, a structural representation of your query.
The engine resolves every name in the parse tree against the actual schema:
does employes exist, does it have a dept_id column, is d.dept_name a
valid reference given the alias d. This is also where implicit type
conversions get decided — a common, invisible source of the non-SARGable
predicates covered in Lesson 03.
This is the stage that matters most for this module. The query optimizer takes the bound query and considers multiple possible execution plans — different orders to access tables, different join algorithms, different index choices — and picks the one it estimates will be cheapest, using table statistics (row counts, value distributions) it has cached about your data. This is called cost-based optimization: the optimizer isn’t finding the correct plan, it’s finding the plan with the lowest estimated cost, and it can be wrong if its statistics are stale.
The chosen plan is carried out: index lookups happen, tables are scanned, joins are performed, sorts and aggregations run — all according to the plan selected in stage 3, not according to the order you wrote the SQL clauses.
Result rows are streamed back to the client. For large result sets, this stage can itself become the bottleneck — a perfectly optimized query can still feel slow if the client is fetching a million rows over a slow network link.
You already know the logical processing order from earlier modules:
FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT
This defines what the result must be equivalent to — not what the engine
literally does step by step. During stage 3 (Optimization), the engine is
free to reorder joins, push a WHERE predicate down so it’s applied before
a join instead of after, or compute LIMIT early to avoid processing rows
it will discard — as long as the final result matches what the logical
order would have produced.
This is the single most important idea in query optimization: you write logical order, the optimizer chooses physical order, and almost every optimization technique in this module is really a way of giving the optimizer more freedom (or fewer excuses) to choose a cheap physical order.
WHERE emp_id = ?) often
outperform queries with literal values baked in on every call.EXPLAIN and
EXPLAIN ANALYZE (Lesson 02), and its optimizer statistics are visible in
pg_stats.ANALYZE /
statistics refresh, a data volume change, or a plan cache eviction.At high query-per-second workloads, stages 1–3 (Parsing, Binding, Optimization) become a real, measurable cost on their own — not just stage 4 (Execution). This is why connection poolers and prepared-statement reuse matter operationally: skipping re-parsing and re-optimizing identical query shapes on every call is a genuine throughput win at scale, independent of any single query’s execution cost.
CURSOR_SHARING controlling how aggressively literal
values are treated as reusable bind parameters.A query is not a single operation — it’s a pipeline of five stages, and the Optimization stage is where your SQL becomes a plan, chosen based on statistics and cost, not on the order you happened to type the clauses. Every later lesson in this module is about understanding and influencing that one stage.
ANALYZE runs (see Production Incident #5).These scenarios are illustrative case studies inspired by common production failure modes; specific figures are illustrative unless an external source is cited in PRODUCTION_INCIDENTS.md. See its disclaimer for the full explanation.
EXPLAIN and compare the current plan to a saved baseline — plan cache invalidation and stale statistics both manifest here.WHERE emp_id = $1) reuse stages 1–3 across calls; literal-heavy SQL forces re-optimization every time. At high QPS, this alone can add measurable latency.INSERT/UPDATE/DELETE, schedule ANALYZE (PostgreSQL) or UPDATE STATISTICS (SQL Server) before the next reporting window — the optimizer’s stage-3 decisions depend on fresh row counts and histograms.Interviewers use lifecycle questions to test whether you debug systematically or guess. When asked “why did this query get slower?”, walk through stages in order: code change (unlikely if none), statistics drift (stage 3), plan cache eviction (stages 1–3), or data growth (stage 4). Mentioning “logical vs. physical order” signals you understand that the optimizer reorders work — a strong differentiator from candidates who only know SQL syntax.
WHERE emp_id = 42) vs. a prepared statement and compare planning time in EXPLAIN ANALYZE — note the difference in stages 1–3.employes, skip ANALYZE, and compare EXPLAIN estimated rows before and after — observe how stage 3’s cost model changes.02_EXPLAIN_AND_EXECUTION_PLANS · 03_SARGability · 07_Query Tuning Workflow