Skip to content

MySQL Tutorial Query Performance and Optimization

Server Settings That Matter

Lesson 216 of 308 5 min read By Deepak Paswan Updated Oct 5, 2026

Most MySQL slowness comes from queries and indexes, not from server settings. Still, a handful of settings matter, and the defaults are conservative. The query cache no longer exists: it was removed in MySQL 8.0, so advice about query_cache_size is out of date.

1. innodb_buffer_pool_size: the most important setting

The buffer pool is InnoDB’s cache of table and index pages in memory. When a page is in the pool, a read costs microseconds. When it is not, MySQL reads from disk (milliseconds).

query.sql
SQL
SELECT @@innodb_buffer_pool_size / 1024 / 1024 AS buffer_pool_mb;
Output
OUTPUT
+----------------+
| buffer_pool_mb |
+----------------+
|    64.00000000 |
+----------------+
1 row in set

(The MySQL default is 128 MB, which is small for a real server. The value printed here comes from the small test server used for this course. Yours will differ.)

  • On a dedicated database server, set it to about 50 to 70 percent of RAM (leave room for the operating system, per-connection memory and other processes).
  • On a shared development machine, 1 to 2 GB is plenty.
  • You can change it online: SET GLOBAL innodb_buffer_pool_size = 4 * 1024 * 1024 * 1024; (it resizes in chunks). Make it permanent in my.cnf.
  • Check the hit ratio: Innodb_buffer_pool_reads (disk reads) compared with Innodb_buffer_pool_read_requests. A hit ratio above 99 percent is healthy.
query.sql
SQL
SHOW GLOBAL STATUS WHERE Variable_name IN ('Innodb_buffer_pool_reads', 'Innodb_buffer_pool_read_requests');
Output
OUTPUT
+----------------------------------+----------+
| Variable_name                    | Value    |
+----------------------------------+----------+
| Innodb_buffer_pool_read_requests | 20765375 |
| Innodb_buffer_pool_reads         | 5610     |
+----------------------------------+----------+
2 rows in set

(The counters come from the test server and grow while it runs.) Divide the two numbers: Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests. A result near 0.001 or below (a hit ratio of 99.9 percent) is healthy.

2. Redo log size

The redo log records changes so that InnoDB can recover after a crash. A log that is too small forces InnoDB to flush data pages more often, which slows write-heavy workloads.

query.sql
SQL
SELECT @@innodb_redo_log_capacity / 1024 / 1024 AS redo_log_mb;
Output
OUTPUT
+--------------+
| redo_log_mb  |
+--------------+
| 100.00000000 |
+--------------+
1 row in set

In MySQL 8.0.30 and later, you set innodb_redo_log_capacity (default 100 MB), and can change it online. For write-heavy servers, 1 to 8 GB is common. (Older versions used innodb_log_file_size and innodb_log_files_in_group.)

3. Durability: innodb_flush_log_at_trx_commit

ValueBehaviourRisk
1 (default)Flush the redo log to disk at every commitNone: fully durable (ACID)
2Write at commit, flush about once a secondAn OS crash or power loss can lose about 1 second of commits
0Write and flush about once a secondA mysqld crash can lose about 1 second of commits

Keep 1 for anything important (money, orders, accounts). 2 is a reasonable choice for data that can be recreated. This is a trade-off, not a free speed-up, and replicas and backups change the equation.

4. max_connections and memory per connection

query.sql
SQL
SELECT @@max_connections AS max_connections, @@sort_buffer_size / 1024 AS sort_buffer_kb, @@join_buffer_size / 1024 AS join_buffer_kb, @@tmp_table_size / 1024 / 1024 AS tmp_table_mb;
Output
OUTPUT
+-----------------+----------------+----------------+--------------+
| max_connections | sort_buffer_kb | join_buffer_kb | tmp_table_mb |
+-----------------+----------------+----------------+--------------+
|             200 |       256.0000 |       256.0000 |  16.00000000 |
+-----------------+----------------+----------------+--------------+
1 row in set
  • max_connections (default 151): the number of simultaneous clients. Each open connection uses memory (buffers are allocated per connection and per operation), and a very high number leads to out-of-memory problems and context-switch overhead. A web application with 20 PHP workers does not need 1,000 connections. Use connection pooling and keep the number low.
  • sort_buffer_size, join_buffer_size, tmp_table_size are per session, and allocated when needed. Raising them globally to hundreds of MB multiplied by hundreds of connections is a classic way to run out of memory. Increase them for one session or one statement (SET_VAR hint) when a specific query needs it.
query.sql
SQL
SHOW GLOBAL STATUS WHERE Variable_name IN ('Threads_connected', 'Max_used_connections', 'Aborted_connects', 'Connection_errors_max_connections');
Output
OUTPUT
+-----------------------------------+-------+
| Variable_name                     | Value |
+-----------------------------------+-------+
| Aborted_connects                  | 1     |
| Connection_errors_max_connections | 0     |
| Max_used_connections              | 1     |
| Threads_connected                 | 1     |
+-----------------------------------+-------+
4 rows in set

(Numbers from the test server.) Max_used_connections shows the peak since startup: set max_connections somewhat above it.

5. Temporary tables

GROUP BY, DISTINCT and sorts use internal temporary tables. They live in memory (the TempTable engine, limited by temptable_max_ram, 1 GB by default) and then spill to disk. Watch:

query.sql
SQL
SHOW GLOBAL STATUS WHERE Variable_name IN ('Created_tmp_tables', 'Created_tmp_disk_tables');
Output
OUTPUT
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| Created_tmp_disk_tables | 18    |
| Created_tmp_tables      | 2848  |
+-------------------------+-------+
2 rows in set

Many disk temporary tables point to queries that group or sort big result sets. Fix the queries and indexes, not the memory limits.

6. Other useful settings

SettingNotes
innodb_file_per_tableON by default. Each table in its own file. Leave it on
innodb_flush_methodO_DIRECT on Linux avoids double buffering. A common production setting
table_open_cacheHow many tables stay open. Raise it if Opened_tables grows quickly
thread_cache_sizeReuses threads. Helpful when connections are opened and closed often
max_allowed_packetLargest statement or value (64 MB default in 8.0)
skip_name_resolveDo not do DNS lookups for clients. Faster logins, and then use IP addresses in accounts
sync_binlog1 (default) is safest for the binary log
character_set_server, collation_serverutf8mb4 and a modern collation
sql_modeKeep the strict default (SQL Mode)
default_time_zone'+00:00' for UTC

How to change settings

  • Dynamic variables: SET GLOBAL name = value; takes effect immediately for new connections, and is lost at restart.
  • Permanent: edit my.cnf (Linux) or my.ini (Windows), then restart, or use SET PERSIST name = value; (MySQL 8), which writes it to mysqld-auto.cnf and applies it now:
query.sql
SQL
SET PERSIST max_connections = 200;
  • Read-only variables need a restart.
  • See what is set where: SELECT * FROM performance_schema.variables_info WHERE variable_name = 'max_connections';

Do not copy configurations blindly

Tuning guides and “my.cnf generators” give numbers for somebody else’s server. Check each setting against the manual and your workload. Start with the buffer pool and the connections, measure, and change one thing at a time.

Tuning order

  1. Queries and indexes (the biggest effect by far).
  2. Schema (types, keys).
  3. Buffer pool size.
  4. Redo log size for write-heavy workloads.
  5. Connections and pooling.
  6. Everything else, only if you measured a problem.
dangerous
A larger buffer is not always better
Memory taken by the buffer pool is not available to the operating system cache, per-connection buffers or other services. Over-allocating makes the server swap, which is far slower than a smaller cache.