Query Optimization & Join Methods
How one SQL statement becomes one execution plan, and how to price the alternatives. Relation profiles and selectivity, the four join methods and their cost formulas, external merge sort, and the discipline of writing a complete cost formula — the single largest points pool after concurrency.
01 · Motivation
What the optimizer actually does
You wrote what you want. The optimizer decides how — and the same query can be executed in many ways whose costs differ by factors of thousands.
The query optimizer receives a query written in SQL and produces a program in an internal format that uses the data access methods. It runs in five phases:
- Lexical, syntactic and semantic analysis — is the query well formed, and do its tables and columns exist?
- Translation into an internal representation — something close to an algebraic tree.
- Algebraic optimization — rewriting the tree into an equivalent, cheaper one.
- Cost-based optimization — choosing access methods, join order and join algorithms using statistics.
- Code generation.
Phase 3 is the classical part and one example carries it. Given
SELECT LastName, FirstName
FROM Student, Exam
WHERE Student.ID = Exam.SID AND Exam.CourseID = 'DB2'the naive tree computes the Cartesian product of Student and Exam, then filters. With 18 000
students each enrolled in about 10 exams, and only 200 taking DB2:
naive: Student × Exam = 18 000 × 180 000 = 3 240 000 000 rows
then filter to 200
pushed: filter Exam to CourseID='DB2' → 200 rows
then × Student = 18 000 × 200 = 3 600 000 rowsPushing the selection below the product shrinks the intermediate result by three orders of magnitude. The heuristic generalises to apply first the operations that reduce the size of intermediate results — and it is a heuristic, not a guarantee of optimality.
02 · Statistics
Profiles and selectivity
Every cost estimate in this course begins by turning the given statistics into derived ones. Getting this line right is most of the exercise.
The data dictionary stores a relation profile for each table:
- the cardinality — number of tuples ;
- the size in bytes of each attribute ;
- the number of distinct values of each attribute, written ;
- the minimum and maximum value of each attribute.
These are recomputed periodically by an explicit command (update statistics and friends), not
maintained continuously. They enable two things: some queries can be answered from the dictionary
alone (select count(*) from T, or a comparison against the recorded min/max), and — the important
one — the optimizer can estimate the size of intermediate results.
The selectivity of a predicate is the probability that a row satisfies it. Under the uniform distribution assumption — which the course applies whenever no distribution data is given — an equality predicate on an attribute with distinct values has
so the number of tuples surviving it is .
Query plan costing — 8 of 14 sessions, 90 points
Physical-DB exercises appear in 8 of the 14 sessions in the bank, worth 10–12 points each — the largest points pool after concurrency control. Every one of them opens the same way: convert the given statistics into tuples per block, surviving tuples per predicate, and matches per join key. The published solutions always print that derivation as a block before any plan is priced, and the exercise text asks for “all the needed computations”. Do it explicitly; it is worth marks on its own and it is where an arithmetic slip becomes visible.
The derived quantities you will need, from a typical exercise:
Given: Hotel 500 K tuples, 12 K blocks, val(Country)=100, val(Stars)=5
Booking 20 M tuples, 200 K blocks, val(RoomType)=10, val(Channel)=4
3% of bookings are in July 2025
Derive: hotels in Italy with 4 stars = 500 K / (100 × 5) = 1 000
bookings matching all three = 20 M × 3% / (10×4) = 15 000
bookings per hotel = 20 M / 500 K = 40Three lines, and the rest of the exercise is arithmetic on them.
03 · Joins
The four join methods
Joins are the most frequent and most expensive operation a DBMS performs, so they get four distinct algorithms with sharply different cost profiles.
Throughout, and are the block counts of the external (outer) and internal (inner) tables, the tuple count of the external one.
1 · Nested loop
Scan the external table; for each of its blocks, scan the whole internal table. Always available, quadratic, and usually a last resort.
2 · Scan & lookup (indexed nested loop)
Scan the external table; for each tuple, use an index on the internal one to fetch only matching rows. Requires an index on the join attribute of the inner table.
3 · Merge-scan
Both inputs sorted on the join attribute: walk them together, advancing whichever is behind. Linear — but only possible when both are ordered (or a B+ on the join key provides the order).
(plus sort cost if one must be sorted)
4 · Hash join
Both inputs hashed on the join attribute with the same function: matches can only be in corresponding buckets, so compare bucket by bucket.
Two refinements to the nested loop matter in exercises. If one table is small enough to fit in the
buffer, cache it and the cost collapses to — the lectures’ example joins 1.7 K
blocks of STUDENT with a 10-block CITIES table for ≈ 1.7 K I/O rather than 17 000. And note the
asymmetry in scan & lookup: the multiplier is , the number of tuples, not , the
number of blocks. That single distinction is worth a factor of the block factor — often 100×.
Merge-scan and hash join need a precondition, not a preference
Both cost , which makes them look like free wins. They are not always available: merge-scan requires both inputs ordered on the join attribute (a sequentially-ordered primary structure, or a B+ tree on it), and hash join requires both hashed on the join attribute with the same hash function. If the exercise does not give you those structures, the method is off the table — say so rather than quietly using it.
04 · Joins
Picking the driving side
When both tables carry filters, the plan has a choice: which table to scan first and filter, and which to probe. The wrong choice can cost thirty times more.
The lectures set it up with STUDENT ⋈ EXAM filtered by City='Milan' AND Grade='30', given
and :
Option 1 — scan STUDENT, filter Milan, look up EXAM
read 1.7 K blocks; 150 K / 150 = 1 000 Milan students survive
for each, scan EXAM's 5.3 K blocks
C = 1.7 K + 1 000 × 5.3 K ≈ 5.3 M I/O
Option 2 — scan EXAM, filter Grade=30, look up STUDENT
read 5.3 K blocks; 1.8 M / 17 = 106 K exams survive
for each, scan STUDENT's 1.7 K blocks
C = 5.3 K + 106 K × 1.7 K ≈ 180 M I/OSame query, same data, a factor of 34 between them — decided entirely by which filter is more selective. The rule: drive from the side whose predicate leaves fewer rows, because that count is the multiplier.
Now add a secondary hash index on EXAM(SID) and option 1 improves dramatically. Each of the 1 000
Milan students costs one hash access plus the 1.8 M / 150 K = 12 exams to fetch:
C = 1.7 K + 1 000 × (1 + 12) = 14.7 K I/Oand if the query only needs to know that a match exists rather than reading the exam rows, the 12 disappears and it falls to 2.7 K.
Exercises hand you scenarios precisely to force this comparison
The physical-DB question is nearly always staged: “scenario 1, no auxiliary structures; scenario 2, add this index; scenario 3, add that one” — and it frequently asks for two plans within one scenario (“one starting from the City table and one starting from the Event table”, 2025-09-05). The marks are for the comparison, not for finding the single best plan. Price both directions and say which wins and why.
The September 2025 paper adds a twist worth knowing, because it is the only place in the bank where a join can be removed rather than reordered:
Deep dive Join elimination via a foreign key
The query joins City and Event on (CountryName, CityName) and filters both sides. Its final
sub-question asks: does anything change if SELECT * becomes SELECT Event.*?
It does — but only under a condition. If Event declares a foreign key on
(CountryName, CityName) referencing City, then every event’s pair is guaranteed to exist in
City. The join can therefore never filter any event out; and if the projection no longer needs any
City column, the join contributes nothing at all. A plan starting from Event may then terminate
without ever touching City: the cost drops from 5 016 to 5 000 I/O in the no-index scenario, and
from 10 030 to 10 014 with the date index.
Without the constraint, the optimizer cannot make that inference and all three plans must perform the join to verify matches. The lesson generalises: integrity constraints are optimizer inputs, not just correctness rules.
05 · Operators
External merge sort
Sorting data too large for memory — needed for ORDER BY, for GROUP BY,
and to make merge-scan joins possible.
To sort blocks using buffer pages:
- Pass 1 reads blocks at a time, sorts them in memory, and writes them back as a sorted chunk. That produces chunks.
- Every later pass uses pages for input chunks and 1 for output, merging chunks into one. Each pass reduces the chunk count by a factor of and lengthens chunks by the same factor.
Repeat until one chunk remains:
The is because every pass reads and writes every block. With and : pass 1 makes 8 chunks of 5 pages, pass 2 merges 4 at a time into 2 chunks of 20, pass 3 merges those into one — passes, I/O.
The formula degrades gently: sorting a billion blocks with 129 buffers takes 5 passes.
06 · Optimization
Cost-based plan choice
With profiles and formulas in hand, the optimizer searches. Three decisions, combinatorially many combinations.
The decisions are: which data access operations to execute (scan versus index access), in what order (crucially, the join order), and with which implementation option for each operation (which join method). The search is organised as a decision tree — each node a choice, each leaf a complete execution plan — and each leaf is priced by
though in this course is all that is counted. The tree is explored with operations-research techniques (branch and bound), and — the honest part — optimizers aim for a good solution in a very short time, not the optimal one. Exhaustive search over join orders is factorial; spending longer optimizing than executing would be self-defeating.
Two execution strategies follow:
- Compile and store — the query is compiled once and executed many times (a prepared statement). The internal code is stored alongside the catalog versions it depends on; a relevant catalog change invalidates it and forces recompilation.
- Compile and go — immediate execution, no storage, though the code may survive briefly in memory for reuse.
PREPARE my_query1 FROM 'SELECT * FROM students WHERE id = ?';
EXECUTE my_query1 USING 54;
EXECUTE my_query1 USING 2;
DEALLOCATE PREPARE my_query1; 07 · Practice
Buffer pool, EXPLAIN, and a real plan
A short look at the live system, both to make the model concrete and to see where it is idealised.
When a query executes, the DBMS loads the pages it needs into the buffer pool. A page already there is a cache hit; otherwise a cache miss forces a disk read. The pool holds data pages, index pages, and in some systems execution plans. When it fills, pages are evicted by LRU or a variant — least recently used first.
That is precisely the mechanism our cost model ignores when it assumes no caching. The assumption is deliberate and conservative: it gives an upper bound and keeps the arithmetic checkable. Exercises that want the other number ask for it explicitly.
Every major system exposes the chosen plan through EXPLAIN / EXPLAIN PLAN. Here is a genuine one,
from Politecnico’s own student-records database — a five-way join returning seven rows:
| Id | Operation | Name | Cost | A-Rows |
| 0 | SELECT STATEMENT | | 19 | 7 |
| 1 | NESTED LOOPS | | 19 | 7 |
| 2 | NESTED LOOPS | | 19 | 7 |
| 3 | NESTED LOOPS | | 10 | 7 |
| 6 | TABLE ACCESS BY INDEX ROWID | AUNICA | 3 | 1 |
|* 7 | INDEX UNIQUE SCAN | UK_AUNICA | 2 | 1 |
| 8 | TABLE ACCESS BY INDEX ROWID | CARRIERA | 4 | 1 |
|* 9 | INDEX RANGE SCAN | NU_CARR_ID_STUDENTE | 2 | 1 |Read it inside-out and the vocabulary is exactly this chapter’s: nested loops, index unique scan for the equality on a key, index range scan for the non-unique foreign key, table access by rowid to fetch the tuple once the index gave its address. The optimizer drives from the most selective predicate — the single person id — and the whole query costs 19 units and about a millisecond.
08 · Exam
Writing a full cost formula
The exercise does not ask for a number. It asks for a plan description, the block counts with their derivations, and the complete formula — cached and uncached where both apply.
Here is the drill on a real paper, February 2026, worked end to end.
The setup
Athlete — 40 K tuples, primary hash on AthleteId, 2 K blocks + 0.4 K overflow (lookup cost
1.2). Result — 120 K tuples, 6 K blocks, sequentially ordered by (Discipline, Score), with a
B+ on that pair (3 levels, 600 leaves). val(CountryCode)=100, val(Discipline)=20, 70 % of
scores ≥ 90. Query: join on AthleteId, filter Discipline='ski jumping' AND Score>=90 AND CountryCode='ITA'.
Derive the statistics first
Result blocks per discipline = 6 K / 20 = 300, of which 70 % qualify → 210 blocks. Qualifying tuples = 120 K / 20 × 0.70 = 4 200. Italian athletes = 40 K / 100 = 400. Results per athlete = 120 K / 40 K = 3.
Scenario 1 — start from the B+
Search the B+ for ('ski jumping', 90): 2 intermediate + 1 leaf. Because the primary structure is
sorted on that pair, the qualifying tuples are contiguous — read 210 consecutive blocks, not
4 200 random ones. Then hash-probe Athlete per tuple at 1.2.
I/O.
Scenario 2 — start from the Athlete hash
Scan the hash table (2.4 K), filter to 400 Italians. For each, find their ski-jumping results: results are not organised by athlete, so each lookup pays the B+ descent plus the same 210-block discipline range.
I/O — sixteen times worse.
Scenario 3 — hash join
Add a hash index on Result(AthleteId) with the same hash organisation. Read both structures
bucket by bucket (2.4 K + 2 K); the athlete tuple is already in hand so the ITA test is free; only
the 400 Italians chase their 3 result pointers each.
I/O.
The verdict
Scenario 1 wins at 5 253, narrowly ahead of the hash join at 5 600. The decisive asset is not an
index at all — it is that Result is physically clustered on the predicate, turning 4 200
scattered accesses into a 210-block sequential read.
Three ways to lose marks on a plan you got right
Ignoring clustering. When the primary structure is sorted on the predicate’s attributes, matching tuples are consecutive — counting one I/O per tuple inflates the answer tenfold.
Multiplying by blocks instead of tuples. In scan & lookup the multiplier is ; using understates the cost by the block factor.
Giving only the number. The rubric asks for the formula and the derivation of every term. A bare total earns a fraction of the marks even when it is correct.
A plan scans a 5 K-block table, filters it to 1 200 tuples, and for each uses a secondary B+ index (4 levels) on a second table to fetch about 8 matching rows. What is the cost?
Load-bearing ideas
- Five phases, of which two matter here: algebraic optimization (push selections down, shrink intermediate results) and cost-based optimization (access paths, join order, join methods).
- Start every answer with the derived statistics — tuples per block, tuples surviving each predicate ( under uniformity), matches per join key. The marks are partly there.
- Four join formulas. Nested loop ; scan & lookup ; merge-scan and hash join both — but only when their preconditions (sorted / same hash) actually hold.
- The driving side decides the order of magnitude. Drive from the more selective filter; its surviving count is the multiplier.
- External sort: passes, per pass.
- Physical clustering beats indexing. A primary structure sorted on the predicate turns per-tuple pointer-chasing into a contiguous block read.
- More indexes ≠ faster. When the pointers to follow outnumber the table’s blocks, the scan wins; several exam scenarios exist purely to make you notice this.
- Exam radar. Re-derive before the exam: the four join cost formulas with their preconditions; the both-directions comparison for a filtered join; and the full worked shape of a three-scenario costing, including the cached variant when the exercise asks for it.