Chapter 22 · Production Capstone: Design, Secure, Scale, Tune, and Recover MySQL

Load Test, Capture Plans, Tune Queries/Server, and Establish Performance Baselines

Generate a representative workload, capture plans and percentile latency, fix the highest-impact query/index issue first, and establish a reproducible performance baseline before server tuning.

Advanced capstone180–300 minServiceHub production capstoneMySQL Community Server 8.4.10 LTSInnoDBsingle local server mandatoryMySQL Shell 8.4.10 + Router 8.4.10 optional HA extension3-member single-primary InnoDB Cluster production targetLast reviewed: August 2026

Learning outcomes

A production performance claim is meaningful only when the workload, dataset, cache state, concurrency, duration, client behavior, and server version are recorded. This lesson creates a repeatable ServiceHub load test, captures query plans and server evidence, fixes the highest-impact access-path problem first, and then asks whether any server setting still has evidence behind it. The goal is not to manufacture a dramatic speedup; it is to establish a baseline that another engineer can reproduce.

01

Generate a representative but disposable dataset and preserve its seed/configuration artifacts.

02

Measure throughput, p50/p95/p99 latency, errors, and server-side statement evidence under named concurrency.

03

Capture EXPLAIN/EXPLAIN ANALYZE evidence before and after a query/index change on the identical dataset.

04

Distinguish query/schema bottlenecks from memory, I/O, connection, temp-work, and redo pressure before changing server variables.

05

Publish a baseline record containing residual bottlenecks and uncertainty rather than only the best run.

Define the experiment before running it

text · benchmark manifest — commit this beside result artifacts
server: MySQL Community Server 8.4.10 LTSschema migration version: capture from schema_migrationsclient: Python + MySQL Connector/Python version recorded at runtimedataset: ServiceHub seed script + row counts + distribution queryworkload mix: 70% dispatch reads, 20% event writes, 10% detail/history readsconcurrency levels: 1, 8, 32 (reduce on small machines; keep them recorded)warm-up: 30 seconds, excluded from measured statisticsmeasured duration: 120 seconds per runruns: >= 3 per configuration/cache conditioncache condition: warm; cold-cache experiments labeled separatelyreported: throughput, p50, p95, p99, max, error rateserver evidence: statement digests, connections, temp work, buffer reads, redo, I/O waitschange policy: one hypothesis-driven change at a time

Warm versus cold cache changes what is being tested. Most long-running production workloads are not “cold” after every request. Do not restart MySQL merely to make one configuration appear faster unless cold-start behavior is the requirement.

Create a query that the current portfolio does not serve perfectly

The core index portfolio was derived from known dispatch and history paths. The load test adds an operational query: “find high-priority open work across all sites ordered by schedule.” The current site-leading index may not be ideal because this query does not constrain site_id. That is a realistic way a new workload reveals an index gap after deployment.

sql · capture the untuned plan and execution evidence
USE servicehub_capstone;EXPLAIN FORMAT=TREESELECT work_order_id,site_id,asset_id,priority,scheduled_atFROM work_ordersWHERE status='OPEN' AND priority>=4ORDER BY scheduled_at,work_order_idLIMIT 100;EXPLAIN ANALYZESELECT work_order_id,site_id,asset_id,priority,scheduled_atFROM work_ordersWHERE status='OPEN' AND priority>=4ORDER BY scheduled_at,work_order_idLIMIT 100;SELECT INDEX_NAME,CARDINALITYFROM information_schema.STATISTICSWHERE TABLE_SCHEMA='servicehub_capstone'  AND TABLE_NAME='work_orders'ORDER BY INDEX_NAME,SEQ_IN_INDEX;

Save the plan text as an artifact. The optimizer may choose differently as data and statistics change, so the acceptance test should focus on observed rows, sort/scan work, latency distribution, and resource impact—not on freezing one exact human-readable plan forever.

A deterministic load-test harness

python · capstone_load.py — bound parameters, prompted secret, percentile output
import getpass, json, random, statistics, timefrom collections import defaultdictfrom concurrent.futures import ThreadPoolExecutorimport mysql.connectorHOST = "127.0.0.1"PORT = 3306USER = "sh_cap_app"PASSWORD = getpass.getpass("Disposable capstone password: ")DURATION = 30.0           # Increase for real baseline runs.WARMUP = 5.0              # Keep warm-up outside measurements.CONCURRENCY = 8           # Record every change to this value.CFG = dict(host=HOST, port=PORT, user=USER, password=PASSWORD,           database="servicehub_capstone", ssl_disabled=False,           autocommit=True, connection_timeout=5)QUERIES = {    "dispatch": ("""SELECT work_order_id,site_id,asset_id,priority,scheduled_at                     FROM work_orders                     WHERE status=%s AND priority>=%s                     ORDER BY scheduled_at,work_order_id LIMIT 100""", ("OPEN", 4)),    "history":  ("""SELECT event_id,event_type,actor,occurred_at                     FROM work_order_events                     WHERE work_order_id=%s                     ORDER BY occurred_at,event_id LIMIT 50""", None),}def percentile(values, q):    if not values: return None    s = sorted(values)    i = min(len(s)-1, max(0, round((len(s)-1)*q)))    return s[i]def worker(seed, stop_at, warmup_until):    rng = random.Random(seed)    lat = defaultdict(list); errors = 0; ops = 0    cnx = mysql.connector.connect(**CFG)    cur = cnx.cursor()    try:        while time.perf_counter() < stop_at:            kind = "dispatch" if rng.random() < 0.8 else "history"            sql, params = QUERIES[kind]            if kind == "history": params = (rng.randint(1, 1000),)            t0 = time.perf_counter()            try:                cur.execute(sql, params); cur.fetchall()            except mysql.connector.Error:                errors += 1                continue            ms = (time.perf_counter()-t0)*1000            if time.perf_counter() >= warmup_until:                lat[kind].append(ms); ops += 1    finally:        cur.close(); cnx.close()    return lat, errors, opsstart = time.perf_counter(); warmup_until = start + WARMUP; stop = warmup_until + DURATIONwith ThreadPoolExecutor(max_workers=CONCURRENCY) as ex:    results = [f.result() for f in [ex.submit(worker, i, stop, warmup_until)                                   for i in range(CONCURRENCY)]]all_lat = defaultdict(list); errors = total_ops = 0for lat, err, ops in results:    errors += err; total_ops += ops    for k, vals in lat.items(): all_lat[k].extend(vals)report = {"concurrency": CONCURRENCY, "duration_s": DURATION,          "throughput_ops_s": total_ops/DURATION,          "errors": errors,          "error_rate": errors/max(1, total_ops+errors), "latency_ms": {}}for k, vals in all_lat.items():    report["latency_ms"][k] = {"count": len(vals), "p50": percentile(vals,.50),                               "p95": percentile(vals,.95), "p99": percentile(vals,.99),                               "max": max(vals) if vals else None}print(json.dumps(report, indent=2))

The short default duration keeps the lesson practical. A production baseline uses longer runs and repeats, and records CPU/I/O/network behavior outside MySQL as well. No password is embedded in source code and SQL values are bound parameters.

Correlate client results with MySQL evidence

sql · capture statement, connection, temp-work, cache, and redo evidence
SELECT DIGEST_TEXT,COUNT_STAR,SUM_ROWS_EXAMINED,SUM_ROWS_SENT,       ROUND(SUM_TIMER_WAIT/1000000000000,3) AS total_secondsFROM performance_schema.events_statements_summary_by_digestWHERE SCHEMA_NAME='servicehub_capstone'ORDER BY SUM_TIMER_WAIT DESCLIMIT 10;SHOW GLOBAL STATUS WHERE Variable_name IN('Threads_connected','Threads_running','Connections','Aborted_connects', 'Created_tmp_tables','Created_tmp_disk_tables', 'Innodb_buffer_pool_reads','Innodb_buffer_pool_read_requests', 'Innodb_os_log_written','Innodb_log_waits');SELECT EVENT_NAME,COUNT_STAR,       ROUND(SUM_TIMER_WAIT/1000000000000,3) AS total_secondsFROM performance_schema.file_summary_by_event_nameORDER BY SUM_TIMER_WAIT DESCLIMIT 10;

Cumulative status counters need interval deltas. Capture them before and after each measured run, then divide differences by elapsed time where a rate is meaningful. A high cumulative value since boot is not evidence that the current test caused the problem.

Fix the highest-impact query/index issue first

sql · candidate index for the cross-site high-priority queue
CREATE INDEX idx_wo_status_priority_scheduleON work_orders(status,priority,scheduled_at,work_order_id);ANALYZE TABLE work_orders;EXPLAIN FORMAT=TREESELECT work_order_id,site_id,asset_id,priority,scheduled_atFROM work_ordersWHERE status='OPEN' AND priority>=4ORDER BY scheduled_at,work_order_idLIMIT 100;EXPLAIN ANALYZESELECT work_order_id,site_id,asset_id,priority,scheduled_atFROM work_ordersWHERE status='OPEN' AND priority>=4ORDER BY scheduled_at,work_order_idLIMIT 100;

This composite index has a subtle tradeoff: after equality on status, the range on priority can limit how fully the index satisfies ordering by scheduled_at. That is exactly why evidence matters. If sorting remains significant, test an alternative index/order/query shape on the same data rather than claiming this index must win. Keep only the portfolio justified by the real workload.

Wrong approach: tune server memory before proving a server bottleneck

Raising the buffer pool, sort buffers, or temporary-table limits can hide a poor query while increasing memory risk. The capstone change order is deliberate: correctness → query/schema/index → connection/workload shape → server resources. Only if the repeated test and OS/MySQL evidence show sustained memory/I/O/redo pressure should a server variable become the hypothesis.

SignalPossible interpretationDo not conclude yet
high rows examined per callpoor selectivity/access paththat buffer pool is too small
disk temp-table rate risesquery creates spillable temp workthat temptable_max_ram must be raised
Innodb_log_waits increases during writesredo capacity/write pressure deserves investigationthat durability should be weakened
Threads_running persistently high + CPU saturationconcurrency/CPU bottleneck possiblethat max_connections should be increased
buffer-pool physical reads spike after restartcache warmingthat steady-state cache is undersized

Publish the baseline honestly

text · performance-baseline acceptance record
Run ID / timestamp: ______________________________Server + client versions: _________________________Schema migration/checksum: _______________________Dataset row counts/distribution: __________________Concurrency / duration / warm-up: _________________Cache state: _____________________________________Throughput: ______________________________________Read p50 / p95 / p99: ____________________________Write p50 / p95 / p99: ___________________________Error rate: ______________________________________Top statement digests: ___________________________CPU / storage latency / queueing: _________________Temp-table / redo / buffer deltas: _______________Change tested: ___________________________________Before/after effect: ______________________________Residual bottleneck / uncertainty: _______________Decision: keep / revert / retest __________________

The capstone passes only if another person can explain what was tested. A screenshot of one fast query is not a performance baseline.

Knowledge check

  1. Why exclude warm-up from measured latency?
  2. Why report p95/p99 instead of average alone?
  3. Why are cumulative status counters sampled twice?
  4. What should be tuned first when a statement examines far more rows than it returns?
  5. Why keep residual bottlenecks in the report?
Reveal answers
  1. Warm-up is a different cache/initialization phase; mixing it with steady-state results obscures what was measured.
  2. Tail percentiles expose slow-request behavior hidden by an average.
  3. The interval delta attributes activity to the measured window more accurately than a since-start total.
  4. Its query/schema/index access path before broad server-memory changes.
  5. A defensible baseline records limitations and uncertainty rather than only favorable results.

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.