Chapter 17 · Memory, I/O, Temporary Work, and Server Performance Engineering

Per-Connection Buffers, Sort/Join Buffers, Memory Multipliers, and OOM Risk

Budget MySQL per-session and per-operation memory from realistic concurrency, diagnose sort/join memory demand, and avoid globally oversized buffers that amplify OOM risk.

Advanced170–230 minper-session memory-budget labMySQL Community Server 8.4.10 LTSsort/join buffers + P_S memoryLast reviewed: August 2026

Learning outcomes

After sizing the global cache, ServiceHub still suffers memory spikes when many reporting sessions run at once. A common reaction is to make sort_buffer_size and join_buffer_size huge globally because one query got faster in a test session. This lesson shows why configured per-session maxima become dangerous when multiplied by concurrency—and why MySQL does not necessarily allocate every configured byte to every idle connection.

01

Identify important session/per-operation memory knobs and distinguish configured limits from actual allocation.

02

Use Performance Schema memory summaries to observe current allocations by thread/event rather than guessing from max_connections.

03

Build a realistic concurrent-memory budget using active workload classes and safety headroom.

04

Diagnose sorts and unindexed/hash joins with plans and status counters before increasing memory.

05

Prefer indexing/query changes or narrowly scoped session/SET_VAR experiments over broad global buffer increases.

Continuity

Run Lesson 1 first so servicehub_perf_lab exists. All changes in this lesson are session-scoped unless a code block is explicitly labeled as an anti-pattern.

Memory multiplication: configured maximum is not resident memory

MySQL sessions allocate memory as work requires it. For example, a session that performs a sort allocates sort workspace up to the configured sort_buffer_size; the optimizer can allocate less and grow toward the limit. A join that cannot use an index can need join buffers, and a complex join can require more than one. Therefore, multiplying every buffer maximum by max_connections usually overstates steady memory—but ignoring multiplication entirely understates peak risk.

Setting / memory classScope and allocation ideaEngineering question
sort_buffer_sizeglobal default + session; used by sessions that sorthow many concurrent sorts, how many merge passes, can index/order design avoid work?
join_buffer_sizeglobal default + session; per full join pair / hash-join memory influenceis the join unindexed because design is missing an access path?
read_buffer_size / read_rnd_buffer_sizesession defaults used for certain scan/read patternsis scan/sort shape expected and measured?
TempTable thread-local/global resourcesinternal temporary work; separate from sort/join buffershow many concurrent temp-heavy statements and how much spill?
network/result/thread stateconnection-related memory grows with activity/resultsare idle connections being confused with active peak work?

Inspect variables and actual memory instrumentation

sql · session defaults and global sources
SELECT @@GLOBAL.sort_buffer_size AS global_sort,       @@SESSION.sort_buffer_size AS session_sort,       @@GLOBAL.join_buffer_size AS global_join,       @@SESSION.join_buffer_size AS session_join,       @@SESSION.read_buffer_size AS read_buffer,       @@SESSION.read_rnd_buffer_size AS read_rnd_buffer;SELECT VARIABLE_NAME, VARIABLE_VALUE, VARIABLE_SOURCEFROM performance_schema.variables_infoWHERE VARIABLE_NAME IN ('sort_buffer_size','join_buffer_size',                        'read_buffer_size','read_rnd_buffer_size')ORDER BY VARIABLE_NAME;
sql · largest current memory consumers by thread
SELECT THREAD_ID,       EVENT_NAME,       CURRENT_NUMBER_OF_BYTES_USED,       HIGH_NUMBER_OF_BYTES_USEDFROM performance_schema.memory_summary_by_thread_by_event_nameWHERE CURRENT_NUMBER_OF_BYTES_USED > 0ORDER BY CURRENT_NUMBER_OF_BYTES_USED DESCLIMIT 30;

Memory event names vary by subsystem and version, so diagnose from the rows your server actually exposes instead of hard-coding one instrument name into monitoring. The summary reports observed allocations; it does not predict a future concurrency spike.

Create a sort-plus-join workload and read the plan first

sql · plan a query with sorting and region join work
EXPLAIN ANALYZESELECT w.work_order_id,       w.region,       w.parts_cost * r.multiplier AS adjusted_partsFROM servicehub_perf_lab.work_orders AS wJOIN servicehub_perf_lab.region_rules AS r  ON r.region_name = w.regionWHERE w.status='open'ORDER BY adjusted_parts DESCLIMIT 5000;

The exact algorithm is cost-based; current MySQL can choose a hash join when applicable, and an expression-based order still needs sort work. The evidence to capture is the chosen operators, estimated versus actual rows, execution time, and session sort counters—not an assumption that a specific buffer must be the bottleneck.

sql · capture per-session sort evidence
SHOW SESSION STATUS WHERE Variable_name IN ( 'Sort_rows','Sort_scan','Sort_range','Sort_merge_passes');SELECT w.work_order_id,       w.region,       w.parts_cost * r.multiplier AS adjusted_partsFROM servicehub_perf_lab.work_orders AS wJOIN servicehub_perf_lab.region_rules AS r  ON r.region_name=w.regionWHERE w.status='open'ORDER BY adjusted_parts DESCLIMIT 5000;SHOW SESSION STATUS WHERE Variable_name IN ( 'Sort_rows','Sort_scan','Sort_range','Sort_merge_passes');

A budget model that respects concurrency

Do not create a “worst case” by multiplying every maximum by max_connections and call that a forecast. Build workload classes from observed active sessions. For example, classify ordinary API sessions, reporting sessions, ETL sessions, and administrative work; record realistic concurrent peaks and the memory-heavy operations each class can perform.

Budget termIllustrative worksheet—not a recommendationHow to obtain it
global resident targetbuffer pool + observed mysqld global structuresconfiguration + process RSS/P_S memory
API active sessionspeak simultaneously running/queued API work × observed session memoryPerformance Schema threads/memory + load test
reporting sessionssmall concurrency × observed sort/temp/join high-waterrepresentative report run
maintenance/backuptool process + server-side work during overlapmaintenance-window measurement
OS/platform reservekernel + agents + filesystem + safety marginhost monitoring under peak

Keep an explicit uncertainty margin. Memory high-water values from one run are not proof of the future maximum, but they are much stronger evidence than assuming every connection allocates every configured buffer.

Wrong approach: make all buffers huge globally

sql · ANTI-PATTERN — shown for diagnosis, do not run on a shared server
-- A tempting but dangerous reaction to one slow report:-- SET GLOBAL sort_buffer_size = 64 * 1024 * 1024;-- SET GLOBAL join_buffer_size = 64 * 1024 * 1024;-- Global changes become defaults for relevant sessions/new connections;-- concurrent operations can multiply memory demand dramatically.
Why this can fail

A 64 MiB setting is not “just 64 MiB for the server.” Many concurrent sessions can allocate sort or join work, and a complex join can need multiple join buffers. Large global sort buffers can also make many ordinary sorts slower because allocation/management overhead grows.

Repair: fix access paths, then scope memory experiments

For the ServiceHub join, the missing access path on work_orders.region is more fundamental than a global join buffer. Add an index only after verifying the workload benefits and write cost are acceptable.

sql · add the workload-driven access path and compare
CREATE INDEX idx_region_statusON servicehub_perf_lab.work_orders(region,status);ANALYZE TABLE servicehub_perf_lab.work_orders;EXPLAIN ANALYZESELECT w.work_order_id,       w.region,       w.parts_cost * r.multiplier AS adjusted_partsFROM servicehub_perf_lab.work_orders AS wJOIN servicehub_perf_lab.region_rules AS r  ON r.region_name=w.regionWHERE w.status='open'ORDER BY adjusted_parts DESCLIMIT 5000;

The sort may remain because the ordering is on a computed value, but the join/filter access path can improve. If a large unavoidable sort still shows meaningful merge-pass pressure, test a slightly larger buffer only in the target session or statement.

sql · narrowly scoped memory test with SET_VAR
SELECT /*+ SET_VAR(sort_buffer_size=1048576) */       w.work_order_id,       w.region,       w.parts_cost * r.multiplier AS adjusted_partsFROM servicehub_perf_lab.work_orders AS wJOIN servicehub_perf_lab.region_rules AS r  ON r.region_name=w.regionWHERE w.status='open'ORDER BY adjusted_parts DESCLIMIT 5000;

Compare the same dataset, same concurrency, same cache state, plan, merge-pass delta, latency distribution, and host memory. A faster single run is not sufficient evidence for a production default.

Production judgment

Per-session buffers are economic multipliers: their cost depends on the number of simultaneous operations that need them. Start with query shape and indexing. Change session-scoped values only after the plan and counters show the relevant operation remains expensive. Global changes require a concurrency budget and a staged load test that watches both mysqld resident memory and host paging.

Lesson 3 moves to another concurrency multiplier: internal temporary tables, which draw from TempTable resources and may spill to disk when memory limits are reached.

Knowledge check

  1. Why is multiplying every configured buffer by max_connections usually too pessimistic?
  2. Why can ignoring per-operation multiplication still be dangerous?
  3. What should you check before increasing join_buffer_size?
  4. Why prefer SET_VAR or session scope for experiments?
  5. What evidence should accompany a memory budget?
Reveal answers
  1. Because many buffers allocate only when a session performs the associated work, and idle sessions do not allocate every possible maximum.
  2. Concurrent sorts/joins and multiple join buffers within complex statements can make peak memory much larger than one-session tests suggest.
  3. Whether an appropriate index/access path can avoid the full/unindexed join work in the first place.
  4. It limits blast radius and lets you compare one known workload before changing defaults for unrelated sessions.
  5. Observed active concurrency by workload class, P_S memory high-water/current data, mysqld/OS memory, and explicit uncertainty/headroom.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.