Skip to content
About
Main content

What spilling actually costs

Why does spilling 5% of the data make a query several times slower?

There are four components. They are ordered by how often people forget them.

1. The write. N bytes, sequential, predictable. At about 1 GB/s of local NVMe, 100 GB takes 100 seconds. This is the only term most people budget.

2. The read-back. Another N bytes, read later, competing with whatever else is spilling by then. The cost is 2N, not N.

3. The pipeline break. A pipeline is a chain of operators where each row flows to the next operator immediately, so the operators work at the same time. A spilling operator breaks this. The partitioning pass must complete before any output is produced. So the operator now blocks where it may not have blocked before, and its full duration lands on the critical path (the chain of steps that determines total time) instead of overlapping with downstream work.

This has a consequence that is not obvious and that closes a loop back to Rationing. The operator's residence time W (how long a query stays in the system) jumps. By (average number in the system = arrival rate × average time in the system), with arrival rate λ unchanged, the number of queries in flight L = λW rises in the same proportion. Every one of those queries holds a memory grant.

The relief mechanism feeds the condition it relieves. Whole-system overload behaviour is a topic of its own, but the mechanism belongs here, and the important fact is that it is structural, not a tuning error.

4. The resource shift. This is where the cliff comes from. A spilling operator stops being memory-bandwidth-bound and becomes I/O-bound. These two resources differ by roughly 50× per node: of the order of 100+ GB/s of memory bandwidth against 1–2 GB/s of local NVMe. So the slowdown is not proportional to the fraction spilled. It is governed by the applied to that fraction.

Let f be the fraction, and r = B_mem / B_disk the bandwidth ratio. Counting the read-back:

the share of the data that goes to disk
memory bandwidth ÷ disk bandwidth (about 50 per node)
local NVMe (r = 50)network-attached disk (r = 500)
Data table
Slowdown by spill fraction
Spill fraction fseriesSlowdown (× in-memory)
0local NVMe (r = 50)1×
0.013local NVMe (r = 50)2.24×
0.025local NVMe (r = 50)3.48×
0.038local NVMe (r = 50)4.71×
0.05local NVMe (r = 50)5.95×
0.063local NVMe (r = 50)7.19×
0.075local NVMe (r = 50)8.43×
0.088local NVMe (r = 50)9.66×
0.1local NVMe (r = 50)10.9×
0.113local NVMe (r = 50)12.1×
0.125local NVMe (r = 50)13.4×
0.138local NVMe (r = 50)14.6×
0.15local NVMe (r = 50)15.8×
0.163local NVMe (r = 50)17.1×
0.175local NVMe (r = 50)18.3×
0.188local NVMe (r = 50)19.6×
0.2local NVMe (r = 50)20.8×
0.212local NVMe (r = 50)22×
0.225local NVMe (r = 50)23.3×
0.237local NVMe (r = 50)24.5×
0.25local NVMe (r = 50)25.8×
0.263local NVMe (r = 50)27×
0.275local NVMe (r = 50)28.2×
0.287local NVMe (r = 50)29.5×
0.3local NVMe (r = 50)30.7×
0.313local NVMe (r = 50)31.9×
0.325local NVMe (r = 50)33.2×
0.338local NVMe (r = 50)34.4×
0.35local NVMe (r = 50)35.6×
0.362local NVMe (r = 50)36.9×
0.375local NVMe (r = 50)38.1×
0.388local NVMe (r = 50)39.4×
0.4local NVMe (r = 50)40.6×
0.412local NVMe (r = 50)41.8×
0.425local NVMe (r = 50)43.1×
0.438local NVMe (r = 50)44.3×
0.45local NVMe (r = 50)45.5×
0.463local NVMe (r = 50)46.8×
0.475local NVMe (r = 50)48×
0.487local NVMe (r = 50)49.3×
0.5local NVMe (r = 50)50.5×
0.512local NVMe (r = 50)51.7×
0.525local NVMe (r = 50)53×
0.537local NVMe (r = 50)54.2×
0.55local NVMe (r = 50)55.5×
0.563local NVMe (r = 50)56.7×
0.575local NVMe (r = 50)57.9×
0.588local NVMe (r = 50)59.2×
0.6local NVMe (r = 50)60.4×
0.613local NVMe (r = 50)61.6×
0.625local NVMe (r = 50)62.9×
0.637local NVMe (r = 50)64.1×
0.65local NVMe (r = 50)65.3×
0.662local NVMe (r = 50)66.6×
0.675local NVMe (r = 50)67.8×
0.688local NVMe (r = 50)69.1×
0.7local NVMe (r = 50)70.3×
0.713local NVMe (r = 50)71.5×
0.725local NVMe (r = 50)72.8×
0.738local NVMe (r = 50)74×
0.75local NVMe (r = 50)75.3×
0.762local NVMe (r = 50)76.5×
0.775local NVMe (r = 50)77.7×
0.787local NVMe (r = 50)79×
0.8local NVMe (r = 50)80.2×
0.813local NVMe (r = 50)81.4×
0.825local NVMe (r = 50)82.7×
0.838local NVMe (r = 50)83.9×
0.85local NVMe (r = 50)85.2×
0.863local NVMe (r = 50)86.4×
0.875local NVMe (r = 50)87.6×
0.887local NVMe (r = 50)88.9×
0.9local NVMe (r = 50)90.1×
0.912local NVMe (r = 50)91.3×
0.925local NVMe (r = 50)92.6×
0.938local NVMe (r = 50)93.8×
0.95local NVMe (r = 50)95×
0.963local NVMe (r = 50)96.3×
0.975local NVMe (r = 50)97.5×
0.988local NVMe (r = 50)98.8×
1local NVMe (r = 50)100×
0network-attached disk (r = 500)1×
0.013network-attached disk (r = 500)13.5×
0.025network-attached disk (r = 500)26×
0.038network-attached disk (r = 500)38.5×
0.05network-attached disk (r = 500)51×
0.063network-attached disk (r = 500)63.4×
0.075network-attached disk (r = 500)75.9×
0.088network-attached disk (r = 500)88.4×
0.1network-attached disk (r = 500)101×
0.113network-attached disk (r = 500)113×
0.125network-attached disk (r = 500)126×
0.138network-attached disk (r = 500)138×
0.15network-attached disk (r = 500)151×
0.163network-attached disk (r = 500)163×
0.175network-attached disk (r = 500)176×
0.188network-attached disk (r = 500)188×
0.2network-attached disk (r = 500)201×
0.212network-attached disk (r = 500)213×
0.225network-attached disk (r = 500)226×
0.237network-attached disk (r = 500)238×
0.25network-attached disk (r = 500)251×
0.263network-attached disk (r = 500)263×
0.275network-attached disk (r = 500)276×
0.287network-attached disk (r = 500)288×
0.3network-attached disk (r = 500)301×
0.313network-attached disk (r = 500)313×
0.325network-attached disk (r = 500)326×
0.338network-attached disk (r = 500)338×
0.35network-attached disk (r = 500)351×
0.362network-attached disk (r = 500)363×
0.375network-attached disk (r = 500)376×
0.388network-attached disk (r = 500)388×
0.4network-attached disk (r = 500)401×
0.412network-attached disk (r = 500)413×
0.425network-attached disk (r = 500)426×
0.438network-attached disk (r = 500)438×
0.45network-attached disk (r = 500)451×
0.463network-attached disk (r = 500)463×
0.475network-attached disk (r = 500)476×
0.487network-attached disk (r = 500)488×
0.5network-attached disk (r = 500)501×
0.512network-attached disk (r = 500)513×
0.525network-attached disk (r = 500)525×
0.537network-attached disk (r = 500)538×
0.55network-attached disk (r = 500)550×
0.563network-attached disk (r = 500)563×
0.575network-attached disk (r = 500)575×
0.588network-attached disk (r = 500)588×
0.6network-attached disk (r = 500)600×
0.613network-attached disk (r = 500)613×
0.625network-attached disk (r = 500)625×
0.637network-attached disk (r = 500)638×
0.65network-attached disk (r = 500)650×
0.662network-attached disk (r = 500)663×
0.675network-attached disk (r = 500)675×
0.688network-attached disk (r = 500)688×
0.7network-attached disk (r = 500)700×
0.713network-attached disk (r = 500)713×
0.725network-attached disk (r = 500)725×
0.738network-attached disk (r = 500)738×
0.75network-attached disk (r = 500)750×
0.762network-attached disk (r = 500)763×
0.775network-attached disk (r = 500)775×
0.787network-attached disk (r = 500)788×
0.8network-attached disk (r = 500)800×
0.813network-attached disk (r = 500)813×
0.825network-attached disk (r = 500)825×
0.838network-attached disk (r = 500)838×
0.85network-attached disk (r = 500)850×
0.863network-attached disk (r = 500)863×
0.875network-attached disk (r = 500)875×
0.887network-attached disk (r = 500)888×
0.9network-attached disk (r = 500)900×
0.912network-attached disk (r = 500)913×
0.925network-attached disk (r = 500)925×
0.938network-attached disk (r = 500)938×
0.95network-attached disk (r = 500)950×
0.963network-attached disk (r = 500)963×
0.975network-attached disk (r = 500)975×
0.988network-attached disk (r = 500)988×
1network-attached disk (r = 500)1,000×

(The (1 − f) part is the data that stays in memory. The 2·f·r part is the data that goes to disk and back, each byte costing r times more, twice.)

At r = 50:

5% spilled is not 5% slowerr = 50

This one calculation explains every "it was fine and then it fell off a cliff" report. Nobody spills 100% and is surprised. People spill 5% and are astonished.

Honest caveat. Real engines overlap I/O with computation, so this formula is an upper bound, and well-tuned systems do better. But the shape is right, and the shape is what matters: the penalty is set by the ratio of two bandwidths, not by the fraction of the data that crossed between them.

This is not a contradiction of hybrid hash's "ramp, not a cliff". Hybrid hash makes the extra I/O grow in proportion to the spilled fraction, a ramp when measured against the cost of a fully spilled join; measured against the in-memory join, each spilled byte costs r times more, which is the cliff.

One-line compression: the slowdown from spilling is governed by the memory-to-disk bandwidth ratio, not the spill fraction, which is why 5% spilled feels like a cliff.