MySQL Performance Tuning Guide

Learn how to diagnose slow MySQL, choose query and configuration changes, use tuning tools, and verify results with a step-by-step performance tuning workflow.

Written By ROMAN AGABEKOV | JUL 05, 2023

Last updated | OCT 11, 2026

MySQL performance tuning starts with the operation you want to make faster. Find the queries and resources behind it, choose a change that addresses the bottleneck, then measure the result under comparable load.
A useful tuning process covers both SQL and server configuration. An inefficient query can overload a well-configured server; a poorly sized buffer pool or excessive connection churn can slow down otherwise reasonable queries. This guide connects those problems to the settings, tools, and checks that help you resolve them.
The main workflow covers Oracle MySQL 8.0 and 8.4 with InnoDB. Version-specific differences are noted where they affect the next step, with a separate section for MySQL 5.7 and MyISAM. Run diagnostic queries selectively with an authorized account. Configuration changes require administrative access and an agreed test and recovery plan.

MySQL Performance Tuning Step by Step

1. Establish a Workload Baseline

Start with a specific problem: checkout latency during a traffic peak, a report that takes too long, rejected connections, or slower writes during a batch job. Record when it happens and what the application is doing.
From your existing monitoring, collect a baseline for that window:

Measure

What it tells you

Application response time, including p95 where available

Whether users are experiencing slow requests

Requests, transactions, or queries per second

How much work the system is handling

Errors and connection failures

Whether the slowdown is accompanied by failed work

CPU, available memory, and swap activity

Whether host resources are constrained

Storage latency and read/write activity

Whether I/O is part of the bottleneck

Replication lag, where applicable

Whether a change affects replicas as well as the primary

Confirm the database version, observation time, and uptime:
SELECT VERSION(), CURRENT_TIMESTAMP(), @@session.time_zone;
SHOW GLOBAL STATUS LIKE 'Uptime';
Uptime is the number of seconds since the server started. It helps interpret accumulated counters; it is not a requirement to wait 24 hours before investigating an incident. Record the time zone used by your charts and SQL timestamps.
Choose a comparison window that represents the problem. A quiet overnight period is a poor baseline for a busy afternoon. Note deployments, restarts, batch jobs, and traffic changes so that you can distinguish a tuning effect from a different workload.

2. Find High-Impact Queries and Separate Other Bottlenecks


Rank Queries by Their Contribution to Work

When statement digest collection is already available, Performance Schema can show which query patterns account for accumulated execution time:
SELECT SCHEMA_NAME,
       DIGEST,
       DIGEST_TEXT,
       COUNT_STAR,
       ROUND(SUM_TIMER_WAIT / 1000000000000, 2) AS total_seconds,
       ROUND(AVG_TIMER_WAIT / 1000000000, 2) AS average_ms,
       SUM_ROWS_EXAMINED,
       SUM_ROWS_SENT,
       SUM_CREATED_TMP_DISK_TABLES
FROM performance_schema.events_statements_summary_by_digest
WHERE DIGEST IS NOT NULL
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
COUNT_STAR counts executions. The timing fields are measured in picoseconds; the query converts them to seconds and milliseconds. They describe instrumented statement time, not CPU consumption. MySQL documents these fields in its statement summary tables reference.
A frequent query can matter more than a rare, very slow query. A query executed 100,000 times at an average of 10 milliseconds contributes 1,000 seconds of accumulated statement time. A two-second query executed 50 times contributes 100 seconds. These totals can overlap across concurrent executions, so they are not elapsed wall-clock time.
The digest totals cover the collection period, not automatically your incident window. Use time-windowed query monitoring or comparable snapshots to connect them to the slow operation. Missing rows or zero timing can reflect collection settings rather than cheap queries. Keep query structures and identifiers private when sharing results.

Review the Plan Before Changing Settings

For a high-impact query, review an available execution plan: access method, selected indexes, join order, estimated rows, and filtering. A large difference between examined and returned rows is a reason to inspect the query, not an instruction to create an index immediately.
Ordinary EXPLAIN describes the optimizer's plan. EXPLAIN ANALYZE, available from MySQL 8.0.18, executes the statement and reports observed execution information; use it only where that execution is acceptable. The MySQL EXPLAIN reference explains the distinction.
For the full query and index workflow, see MySQL query optimization best practices and the guide to using EXPLAIN. Keep query correctness and write costs in the decision, not only read latency.

Choose the Right Branch

Finding

Investigate first

One query pattern dominates statement time

Its plan, access pattern, and indexes

Requests wait on blocked transactions

Lock contention, transaction duration, and application behavior

Connection failures or repeated connection creation

Application pools, concurrency, and connection settings

Broad read latency with physical reads

Buffer pool, working set, and storage

Write latency during heavy updates

Write workload, checkpoint activity, log buffering, and storage

Expensive sorting or temporary work

Query shape, intermediate results, and engine-specific limits

For blocked transactions, the MySQL deadlock detection guide distinguishes current waits from the latest recorded deadlock. Increasing connection or memory limits will not resolve every lock problem.

3. Choose a Tuning Tool for the Job

MySQL tuning tools serve different purposes. Choose one based on the decision you need to make.

Tool or workflow

Best suited to

How to use the result

MySQLTuner report

An initial review of server configuration and status

Turn recommendations into questions about your workload, memory, and current settings

pt-query-digest

Analyzing query workload from available logs

Prioritize query patterns for plan and index review

Releem

Ongoing monitoring, configuration recommendations, and query analysis

Connect observed workload to changes and keep checking performance afterward

A MySQL tuning advisor provides recommendations; it does not remove the need to understand the workload. MySQLTuner's uptime guidance matters to the report it generates, but an active incident still needs immediate investigation. Use a trusted installation and inspect its recommendations rather than applying a downloaded configuration blindly.
For query analysis, use existing logs or monitoring where possible. Enabling additional logging in production changes collection overhead and data handling; plan that separately when the required evidence is missing.
The MySQL tools comparison covers a wider selection. For a closer look at one-off configuration checks versus an ongoing workflow, see Releem and MySQLTuner.

4. Tune InnoDB Memory and Write Activity


Choose a Buffer Pool Size That Fits the Workload

The InnoDB buffer pool caches data and index pages. Start with the configured size and read activity:
SHOW GLOBAL VARIABLES LIKE 'innodb_buffer_pool_size';
SHOW GLOBAL STATUS
WHERE Variable_name IN (
  'Innodb_buffer_pool_read_requests',
  'Innodb_buffer_pool_reads'
);
Compare the change in logical read requests and reads requiring disk access over a representative window, alongside storage latency and host memory. A large accumulated read count alone does not show that the buffer pool is too small.
If frequently accessed pages repeatedly require physical reads and the host has room for more database memory, testing a larger buffer pool may be worthwhile. If storage latency is high even with little read activity, or a query scans unnecessary data, investigate those problems first.
Budget for the whole server: buffer pool, connection and query memory, other MySQL allocations, the operating system, and any colocated processes. On containers, use the container's memory limit rather than assuming all host RAM is available. A fixed percentage copied from another server is not a memory plan.
MySQL supports buffer pool resizing during operation. Dynamic does not mean free of operational impact: choose an appropriate window, watch the resize, and verify latency and memory afterward.
Separate Redo Capacity From Log Buffering

These settings address different parts of the write path:

Setting

Purpose

Evidence to connect to it

innodb_buffer_pool_size

InnoDB data and index page cache

Working set, read activity, memory availability, and I/O

innodb_redo_log_capacity

Disk space available to redo logs

Write workload and checkpoint/flush behavior

innodb_log_buffer_size

Memory used to buffer redo before it is written

Log-buffer waits in the relevant workload window

innodb_redo_log_capacity is available from MySQL 8.0.30. Earlier 8.0 releases use a different redo configuration procedure, so check the version-specific redo log documentation.
Innodb_log_waits records waits for the log buffer to be flushed because it was too small. It is not a general measurement of redo-capacity pressure. Correlate it with write activity and storage latency before deciding what to change.
Avoid tuning write performance by casually weakening durability. Settings such as innodb_flush_log_at_trx_commit and sync_binlog affect persistence guarantees and recovery behavior. Treat those as reliability decisions with the database owner, not routine speed adjustments.

5. Tune Connections, Thread Reuse, and Table Caching


Match Connection Limits to Useful Concurrency

Inspect the configured connection limit and relevant status:
SHOW GLOBAL VARIABLES LIKE 'max_connections';
SHOW GLOBAL STATUS
WHERE Variable_name IN (
  'Threads_connected',
  'Threads_running',
  'Max_used_connections',
  'Connection_errors_max_connections'
);
Threads_connected counts open connections. Threads_running counts non-sleeping threads, including threads that are waiting; it is not a CPU utilization metric. Max_used_connections is a historical peak, while Connection_errors_max_connections records refusals caused by the connection limit.
Before increasing max_connections, review application pool sizes, idle connections, leaks, transaction duration, and peak concurrent work. More permitted connections do not create more CPU, memory, or storage capacity. If the server is already overloaded, a higher limit can allow more work to queue and increase resource pressure.

Reduce Unnecessary Thread Creation

With MySQL's one-thread-per-connection model, thread_cache_size controls how many reusable connection threads can be cached. Review:
SHOW GLOBAL VARIABLES
WHERE Variable_name IN ('thread_handling', 'thread_cache_size');

SHOW GLOBAL STATUS
WHERE Variable_name IN (
  'Connections',
  'Threads_created',
  'Threads_cached'
);
Compare connection attempts and newly created threads over the same window. Repeated connection churn may point to an application pooling problem; increasing the thread cache can help reuse threads, but it does not replace connection pooling. A thread-pool implementation has a different operating model. See the MySQL connection management reference.
Section 7 follows this setting from an observed problem through a trial, persistence, and restoration.

Check Table-Opening Activity

table_open_cache controls a separate cache for open tables. Repeated table opening can warrant investigation, particularly in workloads touching many tables.
SHOW GLOBAL VARIABLES LIKE 'table_open_cache';
SHOW GLOBAL STATUS
WHERE Variable_name IN (
  'Open_tables',
  'Opened_tables',
  'Table_open_cache_hits',
  'Table_open_cache_misses',
  'Table_open_cache_overflows'
);
Relate changes in these counters to workload and latency. Account for memory and file-descriptor limits before increasing the cache. A cache miss is a starting point for investigation, not an automatic instruction to allocate more memory.
Use the MySQL performance parameters reference for individual setting descriptions after identifying the relevant problem.

6. Investigate Temporary Tables, Sorts, and Storage Delays

Sorting, grouping, and joins can create substantial intermediate work. Start with the queries responsible, then review temporary and sort activity:
SHOW GLOBAL STATUS
WHERE Variable_name IN (
  'Created_tmp_tables',
  'Created_tmp_disk_tables',
  'Sort_merge_passes',
  'Innodb_log_waits'
);
These are accumulated counters. Compare changes within a known interval and account for restarts or resets. Created_tmp_disk_tables excludes temporary tables using memory-mapped files, so it does not represent every form of temporary disk activity.
Use query-level evidence to decide whether an index, a narrower intermediate result, or a different query structure can reduce the work. A temporary table on disk is not automatically a configuration error.
Temporary-table settings depend on the storage engine and version:
  • With TempTable in MySQL 8.4 and MySQL 8.0.28 onward, tmp_table_size limits an individual in-memory internal temporary table. Earlier 8.0 releases differ.
  • temptable_max_ram and temptable_max_mmap (available from MySQL 8.0.23) govern shared TempTable resource limits; they are not interchangeable with the per-table limit.
  • For internal temporary tables using MEMORY, the smaller of tmp_table_size and max_heap_table_size limits the in-memory table.
The MySQL 8.0 temporary-table documentation explains these boundaries. Check the actual server version and engine before changing limits.
Similarly, increasing sort_buffer_size or join_buffer_size globally can increase memory demand under concurrency. First identify the affected operation and check the plan. A large join buffer is not a substitute for an appropriate access path.
If read or write latency remains high, compare storage behavior with workload timing, background jobs, and host or service limits. Configuration cannot make an overloaded storage system deliver unlimited throughput.

7. Apply and Persist the Selected Change

A tuning recommendation becomes useful when you can apply it, verify that it took effect, and recover if it makes performance worse.
Before changing production configuration:
  1. Record the effective value, its configuration source, and any persisted override.
  2. Choose one target value for the measured workload and available resources.
  3. Decide how to test it and which symptoms require restoration.
  4. Confirm whether the setting is dynamic or requires a restart, and how your deployment stores configuration.
  5. Have the database owner approve the change and recovery procedure.
Keep a known-good configuration backup. Use the existing deployment process instead of assuming every server has the same my.cnf path or service command. For managed databases, use the provider's parameter workflow and check when changes become effective.

Runtime and Persistence Are Different

For applicable Oracle MySQL system variables:

Method

What it changes

SET SESSION

The current connection's session value, where supported

SET GLOBAL

The global runtime value; it does not save that change for restart

SET PERSIST

The global runtime value and a persisted setting in mysqld-auto.cnf

SET PERSIST_ONLY

A persisted setting without changing the current runtime value

For variables with both session and global scope, changing the global default generally affects new sessions rather than replacing every existing session's value. Check the specific variable. MySQL documents persisted settings and their privileges; do not edit mysqld-auto.cnf by hand.

Example: Test Thread Reuse Before Saving the Change

Consider a self-managed Oracle MySQL 8.4 server using one-thread-per-connection. Connection bursts cause repeated thread creation, and monitoring suggests that connection setup contributes to latency. After checking application pooling and the memory budget, the database owner chooses a trial increase from 8 to 32 cached threads.
These values belong to this test case, not a recommended preset for every server. Use an administrative account with SYSTEM_VARIABLES_ADMIN and the required access to the diagnostic tables.
First capture the current state:
SELECT @@GLOBAL.thread_cache_size AS current_thread_cache_size;

SELECT VARIABLE_SOURCE, VARIABLE_PATH
FROM performance_schema.variables_info
WHERE VARIABLE_NAME = 'thread_cache_size';

SELECT VARIABLE_NAME, VARIABLE_VALUE
FROM performance_schema.persisted_variables
WHERE VARIABLE_NAME = 'thread_cache_size';
For this example, the runtime value is 8, the setting comes from the existing option file, and there is no persisted override. Save the outputs before making a change. Capture timestamped Connections, Threads_created, and Threads_cached observations from section 5, plus connection latency, errors, and memory.
Apply the runtime trial:
SET GLOBAL thread_cache_size = 32;
SELECT @@GLOBAL.thread_cache_size AS current_thread_cache_size;
Compare an equivalent burst of connections. Look for less thread creation relative to connection activity, better connection latency, and no harmful increase in memory or errors. Less thread creation by itself is not enough if the application is still slow.
If the trial fails, restore the saved runtime value:
SET GLOBAL thread_cache_size = 8;
SELECT @@GLOBAL.thread_cache_size AS current_thread_cache_size;
If the trial improves the measured problem, save the selected value through your deployment's configuration process. Where persisted SQL settings are that process and persisted_globals_load is enabled, use:
SET PERSIST thread_cache_size = 32;
Then verify both the runtime value and the row in performance_schema.persisted_variables using the earlier queries. This dynamic setting does not need a restart to change its runtime value; verify its startup behavior through the normal restart validation process.
If you later undo the persisted change on this example server, restore the runtime value and remove only the override added by this test:
SET GLOBAL thread_cache_size = 8;
RESET PERSIST IF EXISTS thread_cache_size;
Verify the runtime value, confirm that the persisted row is absent, and check that the original option-file setting is still 8. RESET PERSIST removes the saved override; it does not restore runtime values. See the RESET PERSIST reference.
If a persisted override existed before the test, restore its saved value instead of deleting it, then restore the separately recorded runtime value if different. Keep startup and runtime recovery distinct. Apply this same record-test-verify process to other settings, using their own memory implications, version support, and restart requirements.

8. Verify Results and Continue Tuning

Compare the same operation under similar request volume, query mix, and concurrency. For cache-related changes, also consider warm-up state: a cold cache after restart is not directly comparable to a warm cache before it.
Keep the original baseline and the after-change observations. Review:
  • Application response time and completed work.
  • Query execution time and frequency for the affected patterns.
  • Connection errors, database errors, and timeouts.
  • CPU, memory, swap, and read/write latency.
  • Replication health and lag where applicable.
  • The effective setting, including whether a restart remains pending.
Choose the next action based on the outcome:

Result

Next action

The target problem improves without a harmful trade-off

Keep the change, record it, and continue monitoring

The relevant measurements do not improve

Revisit the hypothesis before adding more changes

Errors, instability, or resource pressure increase

Follow the recorded recovery procedure and verify restoration

Configuration settings are not transactional changes that a generic SQL ROLLBACK will reverse. Recover the runtime and startup configuration through the method recorded before the test.
Repeat the process as workload changes. The goal is reliable performance for the application, not a perfect cache ratio or a configuration file with the largest possible values.

MySQL 5.7 and MyISAM: What Is Different?

Server version and storage engine are separate choices. A MySQL 8 server can contain MyISAM tables; MySQL 5.7 can use InnoDB. Establish both before borrowing tuning advice.

Area

Difference to account for

InnoDB and MyISAM caching

InnoDB uses the buffer pool for data and index pages. MyISAM's key_buffer_size controls its index cache, while the operating system caches data files

Redo configuration

MySQL 5.7 and earlier MySQL 8.0 releases do not use the modern innodb_redo_log_capacity workflow

Configuration persistence

The SET PERSIST workflow in section 7 is not a MySQL 5.7 procedure

Execution analysis

EXPLAIN ANALYZE is not available in MySQL 5.7

Query cache

It existed in MySQL 5.7, but was removed in MySQL 8.0; query_cache_size is not a modern MySQL tuning target

For MyISAM, investigate the engine's access pattern, locking, key cache, and OS memory rather than treating it as an InnoDB workload. The MyISAM key cache reference explains the cache's role.
Use the documentation for the actual version when working on a legacy server. Do not copy the commands above into MySQL 5.7 or use historical redo-file procedures as a general recovery method. A legacy tuning task can also include planning an upgrade alongside immediate performance work.
MariaDB has its own optimizer and parameter behavior. Follow the MariaDB performance tuning guide for that engine rather than assuming every Oracle MySQL instruction transfers unchanged.

How Releem Helps With MySQL Performance Tuning

Manual tuning combines monitoring, query investigation, configuration review, and follow-up. Releem brings those activities into an ongoing workflow so you can spend less time assembling evidence and more time improving the database.

Connect Workload to Configuration Recommendations

MySQL Configuration Tuning uses observed server data to propose configuration changes. Instead of starting from a generic my.cnf, you can review settings in the context of the database's workload.
In Releem's Recommended Configuration block, review the proposed settings and choose how to apply them for your server. Apply updates manually or through automation you configure, while keeping control of when production changes happen. Some settings need a restart before they become effective, so confirm the active values after applying them.

Find the Queries Worth Optimizing

Query Analytics helps you examine execution time, frequency, and the contribution of query patterns to workload. That supports the decision in section 2: should you focus on a costly query, connection behavior, or configuration?
Releem also proposes query and index improvements. Review and apply those changes yourself; they are not automatically deployed to your application. You can then use continued observation to compare the query's execution time and impact.

Follow the Result, Not Just the Recommendation

Use MySQL monitoring to relate the changed setting or query to application symptoms and resource behavior. Configuration history and event markers help you connect a change to the period you are investigating. Keep the same workload comparison used in the manual process, and check which settings are effective rather than only which were recommended.
The Releem Recommendations documentation covers configuration and query workflows. For example, a rise in physical reads leads you to inspect workload and memory, review a relevant configuration proposal, apply it through the supported workflow, and then check reads, latency, and memory again. Query-heavy problems follow the query-analysis branch instead.
See how Releem helps tune your MySQL configuration and turn repeated manual checks into a manageable optimization process.

Article by

  • Founder & CEO
    Roman Agabekov has 17 years of experience managing and optimizing MySQL and MariaDB in high-load environments. He founded Releem to automate routine database management tasks like performance monitoring, tuning, and query optimization. His articles share practical insights to help others maintain and improve their databases.
Turn MySQL Pain Into Performance.
Get expert insights, real benchmarks, and practical tuning tips — straight from the team behind Releem.
Monthly newsletter. Zero fluff. All signal.