Database Systems Engineering
✓ Tested on MySQL 8.0 + PostgreSQL 16
How to Optimize MySQL & PostgreSQL Database on a VPS Server
Relational databases are the transactional heart of every modern web application, SaaS platform, and e-commerce store. Default MySQL and PostgreSQL configurations are intentionally conservative — built for low-memory VMs, not production hardware. This guide shows you exactly how to optimize MySQL database on VPS and tune PostgreSQL to achieve faster query response times, maximum cache hit rates, and zero I/O thrashing under heavy load.
DB
Reviewed by OnliveServer Technical Team
Specialization: MySQL InnoDB Tuning, PostgreSQL VACUUM Analysis & EXPLAIN Query Optimization
📅 Last Technical Audit: September 2026
Relational databases are the transactional beating heart of almost every modern dynamic web application, SaaS platform, and e-commerce store. Default out-of-the-box configurations for MySQL and PostgreSQL are intentionally conservative — designed to run on low-memory servers rather than maximizing high-performance hardware. Learning how to optimize MySQL database on VPS transforms sluggish database response times into lightning-fast, sub-millisecond query execution.
By hosting your database backend on dedicated budget VPS hosting with high-speed NVMe storage, you can allocate dedicated memory buffers, optimize query caching, and eliminate disk I/O thrashing under heavy transaction loads.
📋 Technical Index & Jump Links
1. MySQL / MariaDB InnoDB Performance Tuning (my.cnf)
The most important configuration setting in MySQL is innodb_buffer_pool_size, which dictates how much database data and index memory is cached in RAM to prevent slow disk reads. For a production VPS, this single parameter can deliver noticeable query performance improvements
/etc/mysql/mysql.conf.d/mysqld.cnf — 8GB RAM VPS
UTF-8
# ─────────────────────────────────────────────────────────────
# Recommended Production Tuning — /etc/mysql/mysql.conf.d/mysqld.cnf
# Tested: MySQL 8.0 / MariaDB 10.11 on 8GB RAM VPS with NVMe SSD
# ─────────────────────────────────────────────────────────────
[mysqld]
# InnoDB Buffer Pool: Set to 60-70% of total available RAM
innodb_buffer_pool_size = 5G
# Multiple buffer pool instances improve parallel throughput
innodb_buffer_pool_instances = 4
# InnoDB Log File Size: Larger = fewer disk flushes, better write speed
innodb_log_file_size = 512M
# Flush method: O_DIRECT bypasses OS cache, prevents double buffering
innodb_flush_method = O_DIRECT
# Set to 2 for maximum write performance (safe on NVMe with UPS/RAID)
# Set to 1 for full ACID compliance (mission-critical transactions)
innodb_flush_log_at_trx_commit = 2
# I/O capacity: Match to your NVMe IOPS (NVMe = 2000+, SATA SSD = 800)
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000
# Connection Management: Avoid "Too many connections" errors
max_connections = 300
max_connect_errors = 1000000
# Thread cache reduces overhead of spawning new threads per connection
thread_cache_size = 50
# Query Cache is deprecated in MySQL 8.0 — use ProxySQL/Redis instead
# query_cache_type = 0 (disabled by default in MySQL 8.0)
# Temp table size — prevents spilling temp tables to disk
tmp_table_size = 128M
max_heap_table_size = 128M
# Slow Query Logging — identify problematic queries
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow-query.log
long_query_time = 1
# Binary Logging for replication and point-in-time recovery
log_bin = /var/log/mysql/mysql-bin.log
expire_logs_days = 7
binlog_format = ROW
After editing mysqld.cnf, apply and validate the configuration:
bash — Apply MySQL Config
# Validate config file for syntax errors before restarting
mysqld --validate-config
# Restart MySQL service to apply new settings
systemctl restart mysql
# Verify InnoDB buffer pool size is correctly applied
mysql -u root -p -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"
# Check InnoDB buffer pool hit ratio (should be > 99%)
mysql -u root -p -e "SHOW STATUS LIKE 'Innodb_buffer_pool_read%';"
💡
Pro Tip: On Onlive Server’s NVMe VPS plans, set innodb_io_capacity = 2000 and innodb_io_capacity_max = 4000. These values match the real IOPS capabilities of PCIe NVMe Gen4 drives, allowing InnoDB’s I/O scheduler to flush dirty pages at maximum throughput.
2. MySQL Slow Query Log & EXPLAIN Analysis
Configuration tuning alone isn’t enough. You must identify and fix the specific queries causing performance degradation using the MySQL Slow Query Log and the EXPLAIN statement:
MySQL — Slow Query & EXPLAIN Analysis
# 1. Parse slow query log to find worst offenders
mysqldumpslow -s t -t 10 /var/log/mysql/slow-query.log
# 2. Run EXPLAIN on a slow query to see its execution plan
EXPLAIN SELECT * FROM orders
WHERE customer_id = 1024
ORDER BY created_at DESC
LIMIT 20;
# 3. Run EXPLAIN ANALYZE for actual runtime stats (MySQL 8.0+)
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 1024;
# 4. Check for queries doing Full Table Scans (type = ALL is bad)
# Look for: "type: ALL" → missing index → add composite index
# 5. Add a composite index to fix the query above
ALTER TABLE orders ADD INDEX idx_customer_created (customer_id, created_at);
# 6. Verify index is being used after creation
EXPLAIN SELECT * FROM orders
WHERE customer_id = 1024
ORDER BY created_at DESC LIMIT 20;
3. PostgreSQL 16 Performance Tuning (postgresql.conf)
PostgreSQL’s performance tuning is controlled via /etc/postgresql/16/main/postgresql.conf. The most critical parameters are shared_buffers, effective_cache_size, and work_mem:
/etc/postgresql/16/main/postgresql.conf — 8GB RAM VPS
# ─────────────────────────────────────────────────────────────
# PostgreSQL 16 Production Tuning — 8GB RAM VPS / NVMe SSD
# ─────────────────────────────────────────────────────────────
# MEMORY
# shared_buffers: Set to 25% of total RAM (PostgreSQL's own page cache)
shared_buffers = 2GB
# effective_cache_size: Estimate of OS + Postgres cache (set to 75% of RAM)
effective_cache_size = 6GB
# work_mem: Memory per sort/hash operation per query (be careful with connections!)
# Formula: (available_ram - shared_buffers) / (max_connections * 3)
work_mem = 32MB
# maintenance_work_mem: Memory for VACUUM, CREATE INDEX, ALTER TABLE
maintenance_work_mem = 512MB
# PARALLELISM
# max_worker_processes: Match to your VPS vCPU count
max_worker_processes = 8
max_parallel_workers_per_gather = 4
max_parallel_workers = 8
# WAL / DISK PERFORMANCE
# wal_buffers: Set to ~3% of shared_buffers or 16MB minimum
wal_buffers = 64MB
# synchronous_commit: Set to off for maximum write speed
# (safe for non-critical writes; always on for financial transactions)
synchronous_commit = off
# checkpoint_completion_target: Spread checkpoint I/O over longer period
checkpoint_completion_target = 0.9
# PLANNER SETTINGS (Tell planner about your NVMe storage cost)
# random_page_cost = 1.1 for NVMe SSD (default 4.0 is for spinning HDD)
random_page_cost = 1.1
effective_io_concurrency = 200
# LOGGING
log_min_duration_statement = 1000 # Log queries taking > 1 second
log_checkpoints = on
log_lock_waits = on
bash — Apply & Validate PostgreSQL Config
# Reload PostgreSQL to apply changes (no full restart needed for most params)
systemctl reload postgresql@16-main
# Verify shared_buffers is applied correctly
psql -U postgres -c "SHOW shared_buffers;"
# Check cache hit ratio — should be > 99% for a well-tuned server
psql -U postgres -c "
SELECT
sum(heap_blks_read) AS heap_read,
sum(heap_blks_hit) AS heap_hit,
ROUND(100.0 * sum(heap_blks_hit) / NULLIF(sum(heap_blks_hit) + sum(heap_blks_read), 0), 2) AS cache_hit_ratio
FROM pg_statio_user_tables;"
4. Index Optimization & PostgreSQL VACUUM Strategy
PostgreSQL uses Multi-Version Concurrency Control (MVCC). It means dead row versions (bloat) accumulate over time. Regular VACUUM ANALYZE is essential to reclaim disk space and keep the query planner’s statistics accurate:
PostgreSQL — VACUUM, Index & Bloat Commands
# 1. Run VACUUM ANALYZE on all tables to update planner statistics
VACUUM ANALYZE;
# 2. Run VACUUM FULL to reclaim bloated disk space (requires table lock)
# Run during maintenance window only
VACUUM FULL orders;
# 3. Check table bloat — find tables wasting the most disk space
SELECT
schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename)) AS total_size,
pg_size_pretty(pg_relation_size(schemaname || '.' || tablename)) AS table_size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname || '.' || tablename) DESC
LIMIT 10;
# 4. Find missing indexes — tables with high sequential scan counts
SELECT relname, seq_scan, seq_tup_read, idx_scan
FROM pg_stat_user_tables
WHERE seq_scan > 0
ORDER BY seq_tup_read DESC
LIMIT 10;
# 5. Create a partial index for high-read filtered columns
-- Example: Index only active users (not all 10M rows)
CREATE INDEX idx_users_active ON users (email) WHERE status = 'active';
5. MySQL vs PostgreSQL — VPS Tuning Benchmark Matrix
| Tuning Parameter |
MySQL 8.0 (8GB VPS) |
PostgreSQL 16 (8GB VPS) |
Impact |
| Primary Buffer Cache |
innodb_buffer_pool_size = 5G |
shared_buffers = 2GB |
🔺 High — Reduces disk I/O 70–90% |
| Sort / Hash Memory |
sort_buffer_size = 4M |
work_mem = 32MB |
🔺 High — Eliminates temp disk sorts |
| Random I/O Cost (NVMe) |
innodb_io_capacity = 2000 |
random_page_cost = 1.1 |
🔺 High — Correct planner decisions |
| Write Durability Mode |
innodb_flush_log_at_trx_commit = 2 |
synchronous_commit = off |
⚡ Medium — 3–5x write throughput gain |
| Bloat / Cleanup |
Automatic (InnoDB purge thread) |
VACUUM ANALYZE — autovacuum |
🔺 High — Prevents table bloat & index rot |
6. Common Performance Errors & Fixes
⚠️ Error 1: MySQL — “InnoDB: Out of memory” after changing buffer pool size
Fix: Do not set innodb_buffer_pool_size above 70% of total RAM. Reserve at least 1–2GB for OS, PHP-FPM, and other processes. Check actual RAM with free -h before adjusting.
⚠️ Error 2: PostgreSQL — “out of memory for query result” with high work_mem
Fix: work_mem is allocated PER sort/hash operation PER connection — not globally. With 100 connections each running 3 sorts, you need 100 × 3 × work_mem RAM available. Lower work_mem to 16MB and increase gradually while monitoring with htop.
⚠️ Error 3: MySQL — “Too many connections” error (max_connections exceeded)
Fix: Increase max_connections = 300 and enable thread_cache_size = 50. For high-traffic WordPress or Laravel sites, install ProxySQL as a connection pooler to reuse idle MySQL connections efficiently.
⚠️ Error 4: PostgreSQL — Autovacuum not running, table bloat grows unbounded
Fix: Ensure autovacuum = on in postgresql.conf (it’s on by default). Check autovacuum is working with: SELECT relname, last_autovacuum FROM pg_stat_user_tables ORDER BY last_autovacuum DESC;
📌 Frequently Asked Questions (FAQ)
Q
What is the single most impactful MySQL optimization for a VPS?
+
For most VPS environments, setting innodb_buffer_pool_size between 60–70% of available RAM is a good starting point. This allows MySQL to keep frequently used data and indexes in memory while leaving enough resources for the operating system and other services.
Q
Should I use MySQL or PostgreSQL for my WordPress VPS?
+
Use MySQL 8.0 or MariaDB 10.11 for WordPress. WordPress is specifically built and optimized for MySQL. While PostgreSQL is technically superior for complex analytical queries, MySQL provides better plugin compatibility and easier caching integration with Redis/Memcached for WordPress workloads.
Q
How do I check if my database queries are using indexes?
+
In MySQL, prefix any slow query with EXPLAIN and look for type: ALL — this means a full table scan with no index. In PostgreSQL, use EXPLAIN (ANALYZE, BUFFERS) and look for Seq Scan on large tables. Both indicate a missing index that should be added.
Q
Does NVMe SSD storage actually improve database performance vs SATA SSD?
+
Yes, significantly. NVMe storage generally provides much faster random read and write performance compared with traditional SATA SSDs. For database workloads with many small random reads (InnoDB B-Tree lookups, PostgreSQL heap fetches), NVMe directly translates to lower query latency. Set innodb_io_capacity = 2000 in MySQL and random_page_cost = 1.1 in PostgreSQL to tell each engine about your NVMe storage capabilities.
8. Conclusion: Your Database is Now Production-Ready
Optimizing MySQL and PostgreSQL on a VPS is not a one-time task — it’s a continuous process of configuration tuning, slow query analysis, and index management. By applying the settings in this guide, your database server will serve queries significantly faster, handle more concurrent connections, and scale gracefully under traffic spikes.
Get started with Onlive Server’s budget VPS hosting — powered by NVMe Gen4 storage, high-memory plans, and root access for full my.cnf and postgresql.conf customization.