skip to content

You own a database serving both interactive OLTP traffic and heavy batch reporting on the same instance. How do you decide the per-operation memory budget for sorts and hash aggregates (PostgreSQL's work_mem, SQL Server's memory grants, Oracle's PGA target), and what policy keeps one workload from starving the other?

level: principalimportance: nice to knowfreq 32%

answer

  1. RAM = OS + buffer pool + connections + working memory
  2. Exposure = concurrency × operators × workers × budget
  3. Low default for OLTP, elevated per batch role/session
  4. Bound concurrency — the pool is the real control
  5. Protect latency, let throughput degrade

basics

~20 s

Budget from the top down: total RAM minus OS and buffer pool leaves a working-memory pool; divide by realistic peak concurrency times operators-per-plan to get a conservative default. Then differentiate by workload — small default for OLTP, elevated per-role or per-session for batch — and treat spilling as acceptable degradation, not failure.

solid answer

~60 s

Start from a **memory budget, not a per-query wish**. Total RAM = OS + filesystem cache + buffer pool + connection overhead + working memory. Whatever is left is the working pool. The exposure is `concurrent_queries × stateful_operators_per_plan × budget`, so a value that looks harmless is multiplied twice before it hits the host. Then apply a policy rather than one number: - **Low default** sized for the OLTP path, where most plans are index-driven and hold little state. Spilling here should be rare because the operators are small. - **Elevated per role/session/job** for batch and reporting, which run at low concurrency and genuinely need large sorts. - **Admission control** — a queue or connection pool cap for heavy work — so the multiplication factor is bounded rather than hoped-for. Engines with explicit grants (SQL Server) queue on memory; engines without it (PostgreSQL) need the pool to do that job. - **Isolation** where the stakes justify it: separate replicas or instances for reporting. And accept that **spilling is the correct behaviour at the tail**. The goal is graceful degradation for the batch tail while OLTP latency stays predictable — not zero spills.

go deeper

for a junior

Know that this memory is configurable, that it is granted per operation, and that setting it too high can exhaust the server's memory.

for a middle

Give the budget equation and the multiplication by operators and concurrency, and note that batch and OLTP want different values.

for a senior

Present the differentiated policy — low default, elevated per batch role, bounded concurrency — plus the instrumentation that shows whether it is working.

for a principal

Own the trade-off explicitly: protect OLTP latency, let batch throughput degrade gracefully, prefer structural fixes and workload isolation over redistributing a fixed pool, and state the failure mode you are engineering against.

## Frame it as an allocation problem The question is not "what number should this setting be" — it is "how do I divide a fixed amount of RAM among competing consumers with different latency requirements". A machine's memory goes to: the OS and filesystem cache; the database buffer pool (cached data/index pages); per-connection overhead; and per-operation working memory for sorts, hash joins, hash aggregates, and distinct. Growing any one shrinks another. Handing every query enough memory to never spill would starve the buffer pool, and a cold buffer pool hurts *every* query, including the ones you were trying to protect. ## The multiplication that catches people The per-operation budget is charged **per operator instance**, not per query. A reporting plan with two sorts and two hash operators claims four budgets simultaneously. Add parallelism and each worker may hold its own. Now multiply by concurrent sessions. The honest exposure is: `peak_working_memory ≈ concurrent_heavy_queries × operators_per_plan × parallel_workers × budget` A "safe-looking" 64 MB with 20 concurrent reports, 3 stateful operators each, and 4 workers is 15 GB. This is why the naive fix — raise the global setting until the slow report stops spilling — is the standard route to an out-of-memory kill or heavy swapping, which converts a slow query into an outage. ## Sizing method 1. **Reserve from the top.** Leave the OS its share, then size the buffer pool for the hot working set (guided by cache hit ratio and physical read rates). What remains is the working pool. 2. **Estimate the multiplier.** Measure, don't guess: how many sessions actually run stateful plans at peak, and how many such operators a typical heavy plan carries. 3. **Divide conservatively** to get the default, accepting that the default will make large sorts spill. That is intentional. 4. **Differentiate.** Give batch roles or specific jobs a much higher session-level value, justified by their low concurrency. A nightly job running alone can safely claim a large multiple of the OLTP default. 5. **Bound concurrency.** The variable you control most reliably is not the per-operator size but how many heavy operators exist at once. A connection pool or job scheduler that admits N heavy jobs turns an unbounded product into a computed one. Engines with explicit memory grants make waiters queue for memory; where the engine will not do that, the pool must. ## Policy levers beyond the number - **Workload isolation.** Reporting on a read replica removes the contention entirely and is often cheaper than tuning. The cost is replication lag semantics and an extra instance. - **Resource governors / resource manager.** Where the engine supports classifying sessions into pools with caps (SQL Server Resource Governor, Oracle Resource Manager), express the policy declaratively instead of relying on discipline. - **Plan-shaping over memory-buying.** Every index that supplies an interesting order converts a memory-hungry hash aggregate into a streaming grouped aggregate. Every predicate pushed down shrinks the operator's input. Structural fixes free memory permanently; setting changes only redistribute it. - **Pre-aggregation.** If the same heavy grouping runs nightly, a maintained summary removes the operator from the system rather than funding it. - **Container limits.** In a cgroup/container, exceeding the limit is an OOM kill, not swapping. Budgets must be set against the *container's* limit with real headroom, because the failure is abrupt. ## Deciding what "good" looks like Define the objective explicitly, because OLTP and batch want different things: - **OLTP:** predictable p99 latency. Sorts should be small or absent; a spill on this path is a design smell to fix with an index, not with memory. - **Batch:** throughput within a window. Spilling is acceptable; what matters is that it completes and does not damage the OLTP path while it runs. That asymmetry is the whole policy: **protect latency, let throughput degrade gracefully.** A batch job that runs 30% slower because it spilled is a success; an OLTP p99 that doubled because a batch job took the memory is a failure. ## Instrumentation that makes the policy real Track, at minimum: temp/spill bytes per workload class; count of queries spilling per hour; memory-grant wait time (where the engine exposes it); host and container memory headroom; and buffer cache hit ratio, so you notice when raising working memory quietly starved the pool. The leading indicator is spill *volume* by class, which moves weeks before anyone files a ticket about duration. ## The trap answer Being asked for "the right value" invites a number. There isn't one — it depends on RAM, workload mix, plan shapes, parallelism, and concurrency. The credible answer states the budget equation, names the multiplication factors, differentiates by workload class, bounds concurrency as the real control, prefers structural fixes that remove memory demand, and explicitly accepts spilling as designed degradation for the batch tail.

  • Would you rather buy more RAM for the buffer pool or for working memory?
    Usually the buffer pool first, because it benefits every query on the instance and reduces physical I/O across the board, while working memory only helps the specific operators that were spilling. The exception is when spill volume is clearly the dominant cost — measured in temp bytes and temp-device I/O — and the buffer pool already covers the hot working set. Measure both before deciding: cache hit ratio and physical reads on one side, spill bytes and temp I/O on the other.
  • Your engine has no explicit memory-grant queue. How do you still bound total working memory?
    Bound the number of concurrent heavy sessions outside the engine — a connection pool with a small dedicated pool for reporting, or a job scheduler that admits a fixed number of batch jobs. That converts an unbounded product into a computed worst case. Pair it with a low global default and elevated session-level settings applied only inside the admitted jobs, so an unclassified session can never claim the large budget.

Airline seat allocation: you do not size the cabin so everyone can lie flat; you set a small default, upgrade the few flights that need it, and cap how many premium passengers board at once.

saying these in an interview costs you the question

  • Quoting a single "correct" value for the setting without reference to RAM, concurrency, or plan shape
  • Forgetting that the budget is per operator instance and per parallel worker, not per query
  • Sizing so that nothing ever spills — starving the buffer pool and slowing every query
  • Ignoring container/cgroup limits, where overcommitting produces an abrupt OOM kill rather than degradation
  • Treating tuning as purely a settings exercise and skipping structural fixes (indexes that supply order, pre-aggregation) that remove the demand

context