Before you use this reference
This page is a lookup, not a tutorial. The explanations live in Query Performance Fundamentals, Reading Query Execution Plans, Index Theory, Join Algorithms, Cost-Based Optimization, Systematic Query Profiling, and Query Performance Anti-Patterns.
Stages#
Parse -> Plan (logical) -> Optimize (physical plan, cost-based) -> ExecuteReading a plan#
Execution order: bottom-up (leaves first, root last) — read inside-out.Estimated vs. actual rows: the biggest gap = the most suspect estimate.Find the single dominant node by actual time before investigating anything else.EXPLAIN ANALYZE SELECT ... -- always ANALYZE, not just EXPLAINIndexes#
| Situation | Index used? |
|---|---|
High selectivity (order_id = 4021) | Usually yes |
Low selectivity (status = 'active', 50% of rows) | Often no — sequential scan wins |
Column wrapped in a function (UPPER(status) = ...) | No — breaks index usage |
| Composite index, filter on a non-leading column | No — leftmost-prefix rule |
Join algorithms#
| Algorithm | Cost shape | Best when |
|---|---|---|
| Nested loop | rows(outer) × rows(inner), or × log with an index | Small outer input, indexed inner |
| Hash join | rows(a) + rows(b) | Large equi-join, no useful index, enough memory |
| Sort-merge | Cheap if pre-sorted, else pays a sort | Both inputs already sorted (e.g. by index) |
Cost-based optimization#
Statistics (row counts, histograms) -> cardinality estimates -> cost per candidate plan -> cheapest estimate wins
Stale statistics = wrong estimates = a plan chosen for datathat no longer matches reality. Re-run ANALYZE after a large bulk load.Profiling method#
- BaselineMeasure the current time under realistic conditions.
- PlanEXPLAIN ANALYZE — find the one dominant node.
- HypothesisOne specific, testable reason for that node's cost.
- One changeExactly one — never several at once.
- Re-measureSame conditions, same cache state as the baseline.
- Keep or revertThen repeat on the next-largest node if still too slow.
Anti-patterns to check first#
N+1 queries: one query per row instead of one batched queryImplicit type conversion: comparing a string column to a numeric literalSELECT *: fetching columns and rows nothing downstream needsUnbounded result sets: aggregating in application code instead of SQLFurther reading#
- Using EXPLAIN — PostgreSQL documentation and EXPLAIN — ClickHouse documentation — the real commands behind every plan referenced above.
- Statistics Used by the Planner — PostgreSQL documentation — the full statistics reference behind the cost-estimation section.
- B-tree — the index structure behind the selectivity table above.
- Query optimization — the general field this whole page summarizes.