How to Optimize MySQL and MariaDB Performance
Tune MySQL and MariaDB for your VPS RAM, fix slow queries, configure buffer pools, and monitor database health.
A default MySQL or MariaDB install is configured for a machine with minimal resources. On a VPS running WordPress, WooCommerce, or any database-heavy app, the defaults waste RAM and leave performance on the table. A 15-minute tuning session can make your queries run 2–10x faster.
Check What You’re Running
mysql --version
# Or from inside MySQL:
mysql -u root -p -e "SELECT VERSION();"
The config file location depends on your distro:
| Distro | Config File |
|---|---|
| Ubuntu / Debian | /etc/mysql/mysql.conf.d/mysqld.cnf or /etc/mysql/mariadb.conf.d/50-server.cnf |
| Rocky / AlmaLinux | /etc/my.cnf or /etc/my.cnf.d/server.cnf |
The Big Three: Buffer Pool, Log File, Connections
These three settings have the most impact. Get these right and you’ve done 80% of the work.
InnoDB Buffer Pool Size
This is where InnoDB caches table data and indexes in memory. Bigger = fewer disk reads = faster queries.
Rule of thumb: Set it to 50–70% of your total RAM if MySQL is the primary service. If you’re also running Nginx + PHP-FPM + other services, keep it at 25–40% of RAM.
| VPS RAM | Buffer Pool (dedicated DB) | Buffer Pool (web + DB on same server) |
|---|---|---|
| 1 GB | 512 MB | 256 MB |
| 2 GB | 1 GB | 512 MB |
| 4 GB | 2.5 GB | 1 GB |
| 8 GB | 5 GB | 2–3 GB |
| 16 GB | 10 GB | 4–6 GB |
[mysqld]
innodb_buffer_pool_size = 1G
InnoDB Log File Size
Larger log files improve write performance but increase crash recovery time. For most hosting workloads:
[mysqld]
innodb_log_file_size = 256M
Changing innodb_log_file_size on MySQL 5.7 or older requires a clean shutdown. Stop MySQL, delete the old log files (ib_logfile0, ib_logfile1 in the data directory), then start MySQL. It creates new ones automatically. MySQL 8.0+ and MariaDB 10.6+ handle this automatically.
Max Connections
Each connection uses memory. Too many idle connections waste RAM. Too few and your app gets “Too many connections” errors.
[mysqld]
max_connections = 150
Most WordPress sites work fine with 50–150 connections. If you’re hitting the limit, the problem is usually connection pooling or slow queries, not the setting itself.
Check current usage:
SHOW STATUS LIKE 'Max_used_connections';
SHOW STATUS LIKE 'Threads_connected';
Recommended Configuration
Here’s a tuned config for a 4 GB VPS running a web stack (Nginx + PHP-FPM + MySQL):
sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf # Ubuntu/Debian
sudo nano /etc/my.cnf.d/server.cnf # Rocky/Alma
[mysqld]
# === InnoDB Settings ===
innodb_buffer_pool_size = 1G
innodb_log_file_size = 256M
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
innodb_file_per_table = 1
innodb_buffer_pool_instances = 1
# === Connection Settings ===
max_connections = 150
wait_timeout = 300
interactive_timeout = 300
max_allowed_packet = 64M
# === Query Cache (MariaDB only - removed in MySQL 8.0) ===
# query_cache_type = 1
# query_cache_size = 64M
# query_cache_limit = 2M
# === Temp Tables ===
tmp_table_size = 64M
max_heap_table_size = 64M
# === Table Cache ===
table_open_cache = 2000
table_definition_cache = 1000
# === Thread Settings ===
thread_cache_size = 16
# === Logging ===
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
log_queries_not_using_indexes = 1
Restart MySQL:
sudo systemctl restart mysql # or mariadb
What Each Setting Does
| Setting | What It Does |
|---|---|
innodb_flush_log_at_trx_commit = 2 | Flushes logs every second instead of every transaction. Slight durability tradeoff for big write speed gains. |
innodb_flush_method = O_DIRECT | Bypasses the OS file cache (InnoDB has its own). Avoids double-caching. |
innodb_file_per_table = 1 | Each table gets its own file. Makes it easier to reclaim disk space after deleting data. |
wait_timeout = 300 | Disconnect idle connections after 5 minutes. Frees up connection slots. |
max_allowed_packet = 64M | Maximum size of a single query or data row. Needed for large imports and BLOB data. |
tmp_table_size / max_heap_table_size | Max size for in-memory temp tables. Queries that exceed this spill to disk (much slower). |
Find and Fix Slow Queries
Enable the Slow Query Log
If it’s not already in your config:
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 2;
SET GLOBAL log_queries_not_using_indexes = 1;
Analyze Slow Queries
# Summary of slowest queries
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
# Sorted by count (most frequent slow queries)
mysqldumpslow -s c -t 10 /var/log/mysql/slow.log
Use EXPLAIN
When you find a slow query, run it with EXPLAIN to see what MySQL is doing:
EXPLAIN SELECT * FROM wp_posts WHERE post_status = 'publish' ORDER BY post_date DESC LIMIT 10;
Look for:
| Red Flag | What It Means |
|---|---|
type: ALL | Full table scan - no index is being used |
rows: 500000 | Scanning way too many rows |
Extra: Using filesort | Sorting on disk instead of using an index |
Extra: Using temporary | Creating a temp table - needs optimization |
Add Missing Indexes
If EXPLAIN shows full table scans:
-- Example: WordPress posts table slow on post_status queries
CREATE INDEX idx_post_status_date ON wp_posts (post_status, post_date);
-- Check existing indexes
SHOW INDEX FROM wp_posts;
Don’t add indexes blindly. Each index speeds up reads but slows down writes. Only index columns that appear in WHERE, JOIN, and ORDER BY clauses of slow queries.
Use MySQLTuner
MySQLTuner analyzes your running MySQL instance and recommends specific settings:
# Download and run
wget https://raw.githubusercontent.com/major/MySQLTuner-perl/master/mysqltuner.pl
perl mysqltuner.pl --user root --pass yourpassword
It outputs recommendations like:
[!!] InnoDB buffer pool size is too small (128M) - should be at least 1G
[!!] Slow query log is not enabled
[OK] Thread cache hit rate: 99%
Don’t blindly apply every suggestion - read each one and decide if it fits your workload. Run MySQLTuner again after a few days of uptime for more accurate results.
Optimize Tables
Over time, tables accumulate fragmentation from updates and deletes. Optimize them periodically:
-- Single table
OPTIMIZE TABLE wp_posts;
-- Check fragmentation
SELECT TABLE_NAME, DATA_LENGTH, DATA_FREE,
ROUND(DATA_FREE / DATA_LENGTH * 100, 1) AS fragmentation_pct
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_database'
AND DATA_FREE > 0
ORDER BY DATA_FREE DESC;
For WordPress specifically, WP-CLI makes this easy:
wp db optimize --path=/var/www/yourdomain.com
OPTIMIZE TABLE locks InnoDB tables during optimization. On large tables this can take minutes. Run during low-traffic hours. For very large tables, consider using pt-online-schema-change from Percona Toolkit.
Monitor Database Health
Quick Status Check
-- Current connections and thread usage
SHOW STATUS LIKE 'Threads%';
-- Buffer pool efficiency (hit rate should be > 99%)
SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
-- Calculate hit rate
SELECT
(1 - (
(SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads') /
(SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests')
)) * 100 AS buffer_pool_hit_rate;
A buffer pool hit rate below 99% means you need more innodb_buffer_pool_size.
Key Metrics to Watch
-- Queries per second
SHOW STATUS LIKE 'Questions';
-- Slow queries
SHOW STATUS LIKE 'Slow_queries';
-- Table locks
SHOW STATUS LIKE 'Table_locks_waited';
-- Temporary tables on disk (should be low)
SHOW STATUS LIKE 'Created_tmp_disk_tables';
SHOW STATUS LIKE 'Created_tmp_tables';
If Created_tmp_disk_tables is high relative to Created_tmp_tables, increase tmp_table_size and max_heap_table_size.
WordPress-Specific Optimizations
WordPress is one of the heaviest MySQL users. Some targeted fixes:
-- Delete old post revisions (WordPress keeps every revision forever by default)
DELETE FROM wp_posts WHERE post_type = 'revision';
-- Delete orphaned post metadata
DELETE pm FROM wp_postmeta pm
LEFT JOIN wp_posts p ON pm.post_id = p.ID
WHERE p.ID IS NULL;
-- Delete transients (cached data that may be stale)
DELETE FROM wp_options WHERE option_name LIKE '%_transient_%';
-- Optimize after cleanup
OPTIMIZE TABLE wp_posts, wp_postmeta, wp_options;
Limit revisions going forward - add to wp-config.php:
define('WP_POST_REVISIONS', 5);
Troubleshooting
| Problem | Fix |
|---|---|
| “Too many connections” | Increase max_connections or fix connection leaks. Check: SHOW PROCESSLIST; - kill idle connections with KILL <id>;. |
| MySQL uses too much RAM | Lower innodb_buffer_pool_size. Each connection also uses sort_buffer_size + join_buffer_size of RAM. |
| Queries suddenly slow | Check SHOW PROCESSLIST; for locked queries. A long-running query or table lock can stall everything behind it. |
| High I/O wait | Buffer pool is too small - data is being read from disk. Increase innodb_buffer_pool_size. |
| Can’t restart MySQL after config change | Syntax error in config. Check: mysqld --validate-config (MySQL 8) or start without the new setting and test incrementally. |
| Database file keeps growing after deletes | InnoDB doesn’t release disk space back to the OS by default. Run OPTIMIZE TABLE or use innodb_file_per_table = 1 going forward. |
| “The table is full” | Hit the filesystem limit, InnoDB tablespace limit, or disk full. Check df -h and SHOW TABLE STATUS. |
Related Articles
Last updated 21 Apr 2026, 08:08 +0300.