Query Processing
Why can one question be answered by plans whose costs differ by a factor of ten thousand, and what does each operator force the engine to do?
Who this mechanism is for: a developer who writes code and basic SQL (SELECT, JOIN, GROUP BY) but has never looked inside a database engine. You do not need to know how an optimizer, a hash join or an external sort works. Every technical term is explained the first time it appears, and again in the glossary at the end. You will learn what each SQL operator forces the engine to do, why some queries are cheap and others are enormous, and why some wrong answers come with no error at all.
The story we will follow
Tarini is a data engineer at Shopsy, an online marketplace with about 40 million customers. She runs the company's analytics warehouse: a cluster of 200 executors (worker processes), each with 16 GB of memory for query operators, running a distributed SQL engine. Next to her sits Omar, a business analyst. Omar writes most of the SQL. He is good at his job and he trusts SQL to "just work". He is regularly surprised by the engine.
The main tables (all numbers are illustrations):
| Table | Rows | What it holds |
|---|---|---|
orders | 2 billion | order_id, customer_id, product_id, order_ts, amount, status (about 200 bytes per row) |
customers | 40 million | customer_id, city, signup_ts, tier |
products | 5 million | product data |
events | 50 billion | clickstream (every click on the site) |
prices | 300 million | price history: one row each time a product's price changed, with a valid_from time |
Two details will matter later. Customer id 0 means "guest checkout": about 7% of all orders have customer_id = 0. And about 2% of orders have customer_id = NULL, because tracking was broken for a while.
The warehouse reads a fixed snapshot of the data. No one is writing to it while a query runs. That makes every query a pure calculation on frozen input, and this is the fact that makes this whole mechanism possible.
Over the next weeks Omar brings Tarini six problems, one at the start of each article below. Each problem looks like a bug in the engine. Each is actually a consequence of what the SQL operators demand. At the end of each article we go back and solve it.
This is the main idea of this mechanism:
How this mechanism is organized
- 1
Why a query has many plans
Why does a query have a huge space of equal-answer plans, and which few ideas explain the cost of every plan?
CaseSame answer, 270 times the cost
- 2
Relational operators
What does each operator (filter, join, group-by, window, union, pivot, recursion) force the engine to do, and where do silent wrong answers come from?
CaseFour reports, none failed, all wrong
- 3
Join algorithms
How can the engine match rows of two tables without scanning one table once per row of the other, and what breaks when keys are skewed?
Case199 executors idle, one running for two hours
- 4
Aggregation algorithms
When can an engine summarize data *before* it moves, and why does `COUNT(DISTINCT)` destroy clusters?
CaseThe COUNT(DISTINCT) that ran out of memory
- 5
Sorting
Why is sorting the one operator that computes nothing, and how do you avoid doing it?
CaseFive full sorts in one plan
- 6
Set and dedup operations
How do `UNION`, `INTERSECT`, `EXCEPT` and `DISTINCT` follow from what we already know, and which shortcut is dangerous?
CaseThe dedup shortcut that lost real rows
- 7
What query processing forbids
What does this whole layer forbid, and what does each escape cost?
CaseA plan review checklist
After that: a summary, hands-on labs, interview questions and a glossary.
What do I do, and what does each option cost?
Check operators for cost and correctness
Reviewing a query plan: find where the row count peaks, which operators block, what each holds in memory and which orderings it needs; then a separate pass for seven silent wrong answers.
DiagnoseChoose a join algorithm
Two inputs must be joined: a non-equality join, an index lookup, a merge, a local join on existing partitions, a broadcast or a shuffle, and then which hash variant fits memory? Then check key frequency and NULLs, which override it all.
ChooseChoose an aggregation strategy
A query groups and aggregates: does the function allow partial results, is an exact answer needed, and does pre-aggregating before the shuffle pay? Three questions, then sizing, sort reuse and determinism checks.
ChoosePlan a sort
A query needs data in order: check that order (not just grouping) is needed, take it for free if the data already has it, then size the sort: passes, fan-in, key normalization, and sampling for a distributed sort.
DesignChoose a dedup strategy
Duplicates must be removed or counted: a sketch for counting, streaming dedup over sorted input, hash dedup in memory, or sort-based dedup when it will not fit? Plus wide keys and why approximate dedup can lose rows.
Choose
Can I see it myself?
Same answer, different cost
The same report two ways on 20 million orders in DuckDB, then with the filter forced above the join: time and rows out of the join.
MeasurePython with DuckDBBlocking versus streaming
Time to first row and total time for LIMIT 10, a full ORDER BY and ORDER BY … LIMIT 10 over 20 million rows in DuckDB.
MeasurePython with DuckDB, PyArrowThe NULL traps
NOT IN against NOT EXISTS before and after one NULL; a LEFT JOIN filtered in WHERE against in ON; a SUM after a join that fans out.
Break itPython with DuckDBJoin algorithms and the memory threshold
A 20-million-row probe joined to a 5-million-row build side with a payload in DuckDB, under memory limits from 4 GB to 100 MB, and at 1 and 8 threads.
MeasurePython with DuckDBAs-of join versus range join
The price at order time for 1,000 products × 1,000 price changes: a range join with ROW_NUMBER against DuckDB's ASOF JOIN.
MeasurePython with DuckDBWhen does partial aggregation pay?
Local pre-aggregation of 100,000 rows per partition for G from 100 to 4 million groups: measured reduction against N/(P·G), and its cost.
SimulatePythonSketches versus sampling for COUNT DISTINCT
Distinct users in 20 million events: exact, HyperLogLog, and "count distinct in a 1% sample × 100"; then the same sample for a SUM.
MeasurePython with DuckDBFloat summation is not associative
One million doubles of very different magnitudes summed in five random orders with a plain loop, against math.fsum.
MeasurePythonThe sort that is not an I/O problem: count the passes
DuckDB sorts 20 million rows (about 2 GB) under a 500 MB memory limit: temp files, wall and CPU time, random and pre-sorted input, and the one-pass capacity M²/B.
MeasurePython with DuckDBNormalized keys and sorting a pointer array
Sort 300,000 three-column rows with a typed comparison and with normalized byte keys; then sort prefix + index pairs.
MeasurePythonDigest collisions and Bloom-filter data loss
The birthday formula with 32-bit digests; then a Bloom filter used as a dedup over a million distinct keys, and as a negative oracle in front of an exact set.
SimulatePython
Can I reason about it under questioning?
Interview questions
Short questions on how queries are executed, such as plans and enforcers, SQL semantics, join algorithms, aggregation, sorting and deduplication, each with an answer you could give aloud and a link to the full explanation.
Scenarios
Longer problems on query execution, of the kind asked in senior interviews: each one estimated with numbers, diagnosed, and answered with its trade-offs.
The tensions this mechanism takes a position on. Each recurs across mechanisms.
Precompute vs. compute-on-read
For example: materialized views, ELT vs. ETL, schema-on-read.
Fail-fast vs. fail-degraded
Reject or continue when something is wrong.
Space vs. time
For example: compression, indexing, materialization, caching.
Exactness vs. approximation
For example: sketches, sampling, ANN.
Generality vs. specialization
For example: general engines vs. purpose-built structures.
Ideas this mechanism meets that keep coming back, mechanism after mechanism.
Independence is assumed
Independence is assumed everywhere and is true almost nowhere.
The algebra decides
What can be skipped, pushed down, parallelized, merged or precomputed is determined by the algebra of the function, not the layout of the data.
Power laws and skew
Costs are conserved
Costs are conserved, never removed.
Variance costs more than volume
Variance costs more than volume.