advanced24 min studyLesson 289 in Engineering

Spill to disk and memory budgets

Budget simultaneously live operators and explain external work without treating a per-operation setting as a server-wide memory reservation.

On this page

What you’ll understand

  • Estimate operator state from row count, width, and overhead.
  • Distinguish a PostgreSQL work_mem limit from a centralized query memory grant.
  • Calculate concurrent operator envelopes and a bounded spill-transfer cost.

A plan that is efficient in memory can behave differently once its retained state exceeds an available budget. Sorting can create temporary runs and a hash operation can partition work into batches. The memory story includes both the size of one operator and which operators or workers remain live together. A single configuration number is therefore not a complete bound on a database server's physical usage.

Estimate state rather than only input payload

For a hash build, row payload is only a starting point. Bucket arrays, entry headers, alignment, pointers or offsets, load-factor choices, and allocator metadata add space. A sort similarly retains keys, references, or tuples depending on implementation. An estimated row width can omit representation-specific overhead, and a cardinality underestimate can make the expected fit wrong before the first allocation. Distinguish logical input bytes from retained algorithm state. The exact accounting belongs to the operator implementation, so a payload formula should be labeled as an estimate or lower category, not a guaranteed allocation size.

Some database systems negotiate centralized memory grants for a query. PostgreSQL's work_mem has a different contract: it is a base per-operation allowance used by relevant sorts and other operations, not one reservation for the entire query or connection. Hash-based operations can use the documented hash_mem_multiplier with that base. Multiple plan nodes, sessions, and parallel execution contexts can use memory concurrently. Shared versus private parallel state also matters, so multiplying a worker count blindly may overcount shared state or miss per-worker copies. Inspect the actual plan and deployment version.

Mstate≈N(w+h)+MbucketsM_{\rm state}\approx N(w+h)+M_{\rm buckets}
N is retained rows, w is payload bytes per row, h is assumed per-row state overhead in bytes, and M_buckets is additional bytes for a bucket array. This illustrative hash-state estimate excludes allocator rounding, skew handling, other live operators, and implementation-specific sharing.↔ Scroll wide equations horizontally.

Spilling changes the work, not the answer

An external sort can form sorted runs that fit its memory budget and then merge them. A partitioned hash operation writes and later processes groups that cannot remain together in memory. Skew can leave one partition too large, requiring more work than an even-distribution estimate suggests. Temporary data adds storage traffic and competes with other queries, but a correct spill path preserves the requested result. A spill is therefore neither automatically a bug nor automatically harmless; judge its effect on latency, throughput, temporary capacity, and resource contention.

Count the actual phases in an I/O estimate. Writing D bytes once and reading that temporary representation once contributes 2D transferred bytes before accounting for initial input and final output. Multiple merge passes, different representations, compression, read-ahead, and a final pass piped directly to a consumer can change the total. Do not infer a universal number of passes from the word external. Observe sort methods, hash batches, temporary reads or writes, and available timing information, then compare those measurements with the phase model you proposed.

Dtemp=2D,Ttemp≥2DBD_{\rm temp}=2D,\qquad T_{\rm temp}\ge\frac{2D}{B}
The model assumes exactly one materialized temporary write of D bytes and one read of the same D bytes, with sustained shared transfer bandwidth B bytes per second. T is seconds; it excludes original input, final output, extra passes, metadata, computation, and interference beyond that bandwidth.↔ Scroll wide equations horizontally.

Concurrency determines the operational envelope

Draw a timeline of retained states rather than adding every node regardless of lifetime or assuming only one node matters. A hash build can remain live while its probe side invokes another memory-using operation. A blocking sort may retain state while a consumer reads it. Admission control, lower concurrency, narrower intermediates, better cardinality estimates, or a selective earlier filter can help without raising every operator's limit. Increasing a global default may reduce one spill while creating a much larger concurrent memory peak elsewhere.

Use separate queue-lab runs to model one admission-pressure mechanism. Reset, set both λ=4 and μ=3, predict growth from the initial four jobs at one per second, then Play or Step. Separately Reset, set both λ=2 and μ=3, predict drainage at t=4, then Play or Step. Jobs are equal normalized retained tasks, not bytes or operators; the instrument contains no memory grants, spill files, or scheduler. Its M/M/1 mean needs separate stochastic assumptions. State the measured per-query overlap and rejection policy required before applying this analogy to a real service.

A per-operator budget multiplies with live work

A stipulated execution envelope has four concurrent sessions. Each session simultaneously retains two private 32 MiB sorts and one private 64 MiB hash state. These are modeled maxima for those states, with no sharing or parallel workers.

  1. One session can retain 2×32+64=128 MiB across the three stipulated live states. The number is not merely its 32 MiB sort allowance.

  2. Four such sessions have a modeled 4×128=512 MiB envelope for these states. Add other query state, connection memory, shared buffers, maintenance work, and operating-system needs separately.

  3. If one spill writes and rereads 256 MiB once through an assumed sustained 128 MiB/s bottleneck, that extra 512 MiB of temporary traffic has a four-second transfer lower bound. It does not predict the complete query duration or another implementation's spill passes.

The result

Concurrency and lifetime turn per-operation allowances into a larger resource envelope. A measured spill and a measured host peak must be explained with their own scopes.

Explore the model

Interactive lab

Interactive lab · queue

Admission influences retained work

Reset, set both λ=4 and μ=3, predict one-job-per-second growth from four, then Play or Step. Separately Reset, set both λ=2 and μ=3, predict drainage from four at t=4, then Play or Step. This fluid model has no bytes, operator grants, or spills; its M/M/1 mean has separate assumptions.

Admission influences retained workFluid backlog starts at 4 and follows max(0, 4 + (λ − μ)t). The separate M/M/1 residence-time formula assumes Poisson arrivals, exponential service and λ < μ. Current readings: Fluid backlog: 4; Net rate · λ − μ: −1 /s; M/M/1 mean residence: 1 s. All parameters are adjustable with the labeled controls below.02.557.51001.534.56Elapsed time · sFluid backlogλ < μ
Fluid backlogCurrent time
Fluid backlog
4
Net rate · λ − μ
−1 /s
M/M/1 mean residence
1 s
t = 0 s

Fluid backlog starts at 4 and follows max(0, 4 + (λ − μ)t). The separate M/M/1 residence-time formula assumes Poisson arrivals, exponential service and λ < μ. Parameter changes pause playback and recompute the model at the current elapsed time. Reset restores both parameters and returns time to zero. Playback starts paused. Adjust either parameter, then choose Play or Step.

Look forBounding concurrent retained work can complement operator tuning, but the real memory envelope needs operator lifetimes and byte measurements.

Check your understanding

Why does setting PostgreSQL work_mem to 32 MiB not cap an entire server at 32 MiB of query memory?

Try it yourself

A teaching hash-state estimate has 100,000 retained rows, 24 payload bytes and sixteen overhead bytes per row, plus a 1 MiB bucket array. Estimate total state and name one omitted factor.

Take a moment to make a prediction or work it out on paper. Open a hint when you need one.

Give me a hint

Multiply rows by payload plus overhead, then add 1,048,576 bytes.

Compare with the solution

The estimate is 100,000×40+1,048,576=5,048,576 bytes, approximately 4.815 MiB. Allocator rounding, additional hash metadata, skew structures, variable-length values, parallel copies, or another live operator can add memory. The formula does not define a universal PostgreSQL hash-entry layout or guarantee a fit under a similarly named limit.

Take it with you

  • Operator state includes representation overhead beyond row payload.
  • Per-operation allowances and centralized query grants are different contracts.
  • Budget concurrent lifetimes and count actual temporary-I/O phases.

Read further

Original sources and deeper explanations to follow at your own pace.

Optional progress, saved in this browser when available. Revisit any lesson at any time.

Lesson not marked complete.