Build your own mini columnar query engine · Stage 8 of 8
Spill to disk under a memory budget (optional)
The capstone stage. A real aggregate can't assume all groups fit in memory. Bound the in-memory group count and spill partial state to disk when you exceed it, then merge everything for the final answer. This mirrors Spark's spill under memory pressure — and the key invariant is that spilling never changes the result.
Task
aggregate_with_spill(table, keys, aggs, *, max_groups_in_memory, spill_dir=None)
-> pyarrow.Table
- Fold rows into a hash table as in stage 4. When the table holds
max_groups_in_memorygroups and a new key arrives, flush (spill) the current partial state to a file and start a fresh, empty table. - At the end, reload every spilled partial and
merge_partials()them with the in-memory state (reuse stage 4), then finalize. - Clean up the spill files you created.
The bugs this stage teaches
- Forgetting to merge the spill → spilled groups vanish.
- Not clearing memory after a spill → groups counted twice.
- Reusing one spill filename → earlier spills overwritten.
Checks
- Under a tight budget on many groups, the result equals the in-memory aggregate and DuckDB (G6).
- A roomy budget (no spill) and a tight budget (many spills) give identical results.
Hints
Hint 1
Bound memory by the number of in-memory groups. While folding rows, when the hash table reaches max_groups_in_memory and a NEW key arrives, flush (spill) the current partial state to a file and start a fresh, empty table.
Hint 2
Spilling must not change the answer. At the end, reload every spilled partial and merge_partials() them with the in-memory state, then finalize. This reuses your stage-4 merge logic — spill is just more partials.
Hint 3
After spilling, CLEAR the in-memory state (don't both spill and keep it, or groups get double-counted). Give each spill a unique filename so earlier spills aren't overwritten, and clean up the files when done.
uv run de start SPK-BYO/08-spill
uv run de test