Back to the map

Module 19 · Storage, indexing and recovery · Lesson 3 of 4

Query processing and join algorithms

What happens between SQL text and results: parsing, logical and physical plans, the iterator execution model, scans, external sorting, nested-loop, hash and sort-merge joins with their costs.

DeepShould-knowSenior 14 minVideo Updated Nov 2026
Video walkthrough · 2:26 · narrated, with captions

Why it matters

Two logically identical queries can differ in runtime by a factor of a thousand depending on the plan the database chooses. Understanding how plans are built and executed lets you read EXPLAIN output, predict when a hash join beats a nested loop, and see why bad row estimates produce disastrous plans. The System Design track's query optimization lesson covers the practical method; this lesson covers the machinery it relies on.

The idea

A query goes through a pipeline before any data moves.

  1. 1

    Parse and bind

    Check syntax, resolve tables and columns against the catalogue, check permissions.
  2. 2

    Rewrite

    Apply rules that are always beneficial: expand views, flatten subqueries into joins, push selections and projections down, remove redundant conditions.
  3. 3

    Optimize

    Enumerate alternative physical plans (join orders, join algorithms, access paths) and estimate each one's cost from table statistics; keep the cheapest.
  4. 4

    Execute

    Run the chosen plan, a tree of physical operators.

The iterator model

Most engines execute plans as a tree of operators that each implement open, next and close (the Volcano or iterator model). The root asks its child for the next row, which asks its children, and rows flow up one at a time. Operators that can produce rows as they consume them (scans, filters, nested-loop joins) pipeline; operators that must see all input first (sort, the build side of a hash join, aggregation) are blocking. Analytical engines process batches of rows per call (vectorised execution) or compile plans into machine code to cut per-row overhead.

Access paths

Access pathReadsBest when
Sequential (full) scanEvery page of the table, in orderA large fraction of rows qualifies, or there is no useful index
Index scanIndex pages plus a table page per matching row (random I/O)Few rows qualify (high selectivity)
Index-only scanIndex pages onlyThe index covers every needed column
Bitmap index scanCollect matching row ids, sort by page, then read pages in orderModerate selectivity, or combining several indexes

Sorting data larger than memory

ORDER BY, DISTINCT, GROUP BY and sort-merge joins may need to sort more data than fits in memory. External merge sort reads memory-sized chunks, sorts each and writes them as sorted runs, then merges runs in passes. With B pages of memory and N pages of data, there are about N/B initial runs, and each merge pass combines B − 1 runs, so a few passes suffice even for huge inputs. This is why raising sort memory (work_mem, sort_buffer_size) can turn a multi-pass disk sort into an in-memory one.

Join algorithms

Let R be the outer table with M pages and S the inner with N pages.

AlgorithmHow it worksApproximate cost (page I/Os)Best when
Simple nested loopFor each row of R, scan all of SM + rows(R) × NNever, except tiny tables
Block nested loopFor each memory-sized block of R, scan S onceM + ⌈M / (B − 2)⌉ × NNo index and no equality condition
Index nested loopFor each row of R, look up matches in an index on SM + rows(R) × (index lookup cost)Small outer input and an index on the inner join key
Sort-merge joinSort both on the join key, then merge in one passSort costs + M + NInputs already sorted, or output must be sorted; equality or range joins
Hash joinBuild a hash table on the smaller input, probe it with the largerAbout 3 × (M + N) with partitioning if the build side spills, M + N if it fitsLarge equality joins with no useful order
Departmentsmall: build sidebuildHash tabledept_id → rowEmployeelarge: probe sideprobeJoined rowsif the build side does not fit, both inputs are partitioned to disk by hash first (grace hash join)
A hash join: build an in-memory hash table from the smaller input on the join key, then stream the larger input through it. Each probe row finds its matches in one lookup.

Choosing a plan

The optimizer estimates cardinalities (how many rows each operator produces) from statistics: row counts, distinct values, histograms and most-common values per column. From cardinalities it estimates I/O and CPU cost. For joins it must pick an order: with n tables there are factorially many orders, so optimizers use dynamic programming over subsets (System R style) for moderate n, usually restricted to left-deep trees, and heuristics or genetic search beyond that. Estimation errors compound across joins: assuming two filters are independent when they are correlated can underestimate rows by orders of magnitude, leading to a nested loop where a hash join was needed. Fresh statistics, extended statistics for correlated columns, and checking estimated versus actual rows in EXPLAIN ANALYZE are the practical defences.

Trade-offs

Nested loops start returning rows immediately and excel with small outer inputs and indexes; hash joins dominate large equality joins but need memory and block on the build side; sort-merge joins win when inputs are already ordered. Cost-based optimization usually finds good plans quickly, at the risk of terrible plans when statistics mislead it. Pipelined execution saves memory; blocking operators are sometimes unavoidable.

In an interview

Explain a plan choice with numbers: "Joining a 50-row filtered department set to a 10-million-row employee table with an index on dept_id: an index nested loop does 50 index lookups, effectively instant. Joining two unfiltered 10-million-row tables on a non-indexed key: a hash join building on the smaller one, a few full scans. If the optimizer picked a nested loop for the second, I would check its row estimates, because it probably thought one side was tiny."

Check yourself

A query joins two large tables on an equality condition, neither input is sorted, and there is no index on either join column. Which join algorithm will a good optimizer usually choose?

Query processing and join algorithms · Backplane