Optimizing relational databases on a VPS centers on memory allocation: allocate 60% to 75% of available system RAM to innodb_buffer_pool_size (MySQL) or shared_buffers (PostgreSQL) to keep frequently queried tables and indexes in memory. Additionally, enable the slow query log (long_query_time = 1), run mysqltuner to identify missing indexes, and host database directories on NVMe storage with noatime mount flags to eliminate storage I/O bottlenecks.
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
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.
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;
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;"
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';
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 |
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 the single highest-impact optimization. This keeps frequently used tables and indexes in fast RAM while leaving sufficient headroom for the OS and PHP processes.
Q
Should I use MySQL or PostgreSQL for my WordPress VPS?
+
Use MySQL 8.0 or MariaDB 10.11 for WordPress. WordPress is natively engineered and benchmarked for MySQL, offering complete plugin compatibility and seamless Redis object caching integration.
Q
How do I check if my database queries are using indexes?
+
In MySQL, prefix queries with EXPLAIN and check for type: ALL (unindexed full table scan). In PostgreSQL, run EXPLAIN (ANALYZE, BUFFERS) and watch for Seq Scan on high-row tables.
Q
Does NVMe SSD storage improve database performance vs SATA SSD?
+
Yes, dramatically. NVMe Gen4 delivers 1,000,000+ random read IOPS vs ~100,000 on SATA SSDs, slashing random I/O seek latency. Configure innodb_io_capacity = 2000 in MySQL to utilize NVMe throughput.
Q
What is the recommended innodb_buffer_pool_size on a database VPS?
+
For a dedicated database server, allocate between 60% and 75% of total system RAM to innodb_buffer_pool_size to keep entire active database tables cached in RAM.
Q
How does Redis or query caching reduce database CPU load?
+
Offloading repeated SELECT queries to an in-memory Redis caching tier stops the database from re-parsing SQL and re-reading disk blocks, drastically reducing server CPU utilization.
For developers and organizations scaling web applications or requiring dedicated virtual environments, exploring high-performance Linux VPS hosting delivers guaranteed NVMe storage, KVM hypervisor isolation, and full root access for production workloads.
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.