Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

MySQL Performance Tuning Guide

By Kokil Thapa | Last reviewed: September 2026

A slow database kills every layer above it. No amount of front-end polish fixes a 3-second query on checkout or a booking form that locks under load. This MySQL Performance Tuning Guide walks through the workflow I use on production Laravel, WooCommerce, and custom PHP apps backed by MySQL performance tuning for web applications. You will measure first, fix queries and indexes second, and only then touch server variables. That order prevents the classic mistake of raising max_connections while a full table scan still runs on every page view.

What Is the First Step in a MySQL Performance Tuning Guide?

Start with a baseline. Capture what is slow, how often it runs, and under what load. Without numbers, you cannot prove a fix worked or spot regressions after the next deploy.

On Ubuntu servers I maintain, I pull four signals before changing anything:

  1. Application timing — Laravel Debugbar in staging, or APM in production
  2. MySQL slow query log — queries above a sensible threshold
  3. SHOW GLOBAL STATUS — buffer pool hit rate, temp tables, sort merges
  4. OS metrics — CPU, disk I/O wait, swap usage

Enable the slow query log in /etc/mysql/mysql.conf.d/mysqld.cnf (path varies by distro):

[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1

Restart or reload MySQL, reproduce traffic, then inspect with mysqldumpslow or MySQL Performance Schema. A threshold of 1 second works for most web apps. APIs may need 0.5 seconds. Batch jobs can use 5 seconds to reduce noise.

MySQL Performance Tuning WorkflowBaselineLogs + STATUSDiagnoseEXPLAIN + ProfileFix QueryIndex + RewriteVerifySame workloadCommon Mistakes to SkipRaising max_connections before fixing scansAdding indexes without measuring selectivityTuning prod without staging replayIgnoring ORM N+1 query patterns
MySQL Performance Tuning Guide — measure, diagnose, fix queries, then verify under real load

Store baseline numbers in a spreadsheet or ticket. Page load, p95 query time, and buffer pool hit ratio are enough for most teams. If you cannot replay traffic, schedule tuning during a low-traffic window and watch error rates closely.

How Do You Find Slow MySQL Queries in Production?

The slow query log tells you what hurt users. Performance Schema tells you why. Use both when possible.

Reading EXPLAIN output

Every tuning session eventually lands on EXPLAIN. Run it on the exact SQL your app sends — not a simplified version:

EXPLAIN ANALYZE
SELECT o.id, o.total, c.name
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.status = 'pending'
  AND o.created_at >= '2026-01-01'
ORDER BY o.created_at DESC
LIMIT 50;

Watch for these red flags:

  • type = ALL — full table scan on a large table
  • rows in the millions for a paginated list
  • Using filesort or Using temporary on high-traffic paths
  • key = NULL when a filter column clearly needs an index

For deeper analysis, enable the performance schema and query events_statements_summary_by_digest. That surfaces queries by total time, not just individual slow executions. On a legal-tech portal I built, one digest accounted for 40% of database time. It was a report query missing a composite index — not the checkout path we assumed was the problem.

pt-query-digest and Laravel Telescope

Percona Toolkit's pt-query-digest groups slow log entries by fingerprint. In Laravel apps, pair it with Laravel performance optimization techniques and watch for N+1 patterns in Eloquent. A single controller action firing 80 queries often beats one slow query for total damage.

Slow Query Diagnosis FlowSlow Page or APISlow Query LogEXPLAIN ANALYZEAdd IndexComposite + coverRewrite SQLJoin + limit fixCache LayerRedis 8.10
Diagnosis path in any MySQL Performance Tuning Guide — from symptom to index, rewrite, or cache

See also MySQL query optimization for slow queries and optimizing MySQL queries for high-traffic applications for worked examples on pagination and JOIN order.

How Should You Design Indexes for MySQL Performance?

Indexes are the highest-ROI fix in most tuning jobs. They are also the easiest way to slow writes and waste disk if applied blindly.

Composite index column order

MySQL uses a composite index left-to-right. For a query filtering by status and sorting by created_at, this index usually wins:

ALTER TABLE orders
ADD INDEX idx_orders_status_created (status, created_at);

Put equality columns first. Range columns last. If you filter status = 'pending' and created_at >= '2026-01-01', the same index still works — but only if status is the leading column.

Covering indexes and selective filters

A covering index includes all columns the query reads. MySQL can satisfy the query from the index alone:

ALTER TABLE orders
ADD INDEX idx_orders_list_covering (status, created_at, id, total);

Do not index every column. Low-cardinality fields like boolean flags rarely help alone. Combine them with selective columns or skip them. Read MySQL index design deep dive and database indexing for performance before adding five indexes to one table.

On the Quick And Easy Nepalese Grocery Laravel store, delivery-zone lookups needed a composite index on (zone_id, is_active). Without it, zone checks scanned the full product table on every cart update.

Tuning approachBest forRiskTypical effort
Query rewriteBad JOINs, SELECT *, OR conditionsLow if tested1–4 hours
Composite indexFiltered lists, dashboards, APIsWrite overhead30 min + monitor
InnoDB buffer poolRead-heavy apps, warm cachesRAM misallocationConfig + restart
Redis query cacheExpensive read-mostly aggregatesStale dataApp code change
Table partitioningArchival logs, time-series rowsQuery planner surprisesHours to days
Index Decision TreeQuery on hot path?NoDefer — fix bigger winsYesEXPLAIN shows scan?Composite indexequality then rangeStill slow?Try covering indexAvoid Index Anti-PatternsIndexing low-cardinality aloneDuplicate indexes on prefixesFunctions on indexed columns
Index decision tree — core logic in every MySQL Performance Tuning Guide for web workloads

Which MySQL Server Variables Matter Most for Tuning?

Server tuning comes after query fixes. MySQL 9.7 and the 8.4 LTS line share the same InnoDB core. Most shared hosts lock these variables — self-managed Ubuntu boxes do not.

InnoDB buffer pool

The buffer pool caches data and index pages in RAM. It is the single most important memory setting:

-- Check current size (bytes)
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';

-- Target 60-70% of dedicated DB RAM on a DB-only server
SET GLOBAL innodb_buffer_pool_size = 4294967296;  -- 4 GB

Check hit rate after a warm-up period:

SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';

Calculate: 1 - (Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests). Aim above 99% on steady-state production. Below 95% usually means the pool is too small or the working set grew.

Connections, temp tables, and I/O

Match max_connections to your PHP-FPM or app pool size plus admin headroom. Setting 500 connections on a 2 GB RAM box creates thrashing, not capacity. Tune PHP-FPM pools in parallel — see PHP-FPM tuning for high-traffic websites.

Other variables worth reviewing:

  • innodb_log_file_size — larger logs reduce flush pressure on write-heavy tables
  • tmp_table_size / max_heap_table_size — raise together if EXPLAIN shows memory temp tables spilling to disk
  • innodb_flush_log_at_trx_commit — value 2 trades durability for speed on non-financial logs (know the risk)
  • table_open_cache — increase when you see "Opening tables" waits on schema-heavy apps

For large historical tables, consider MySQL partitioning for large tables before throwing RAM at full scans. Partition pruning helps time-range reports on audit and order history.

Fresh installs on Ubuntu should follow install MySQL on Ubuntu and LEMP stack setup before custom tuning. Default configs assume a small dev machine, not a production web app.

How Do You Tune MySQL for Laravel and PHP Web Applications?

Application-layer fixes often beat server knobs. On Laravel 12 or 13.x with PHP 8.3+, these patterns recur across client projects.

Eliminate N+1 and lazy loading

// Bad — N+1 on order lines
$orders = Order::where('status', 'pending')->get();
foreach ($orders as $order) {
    echo $order->customer->name;
}

// Good — eager load
$orders = Order::with('customer')
    ->where('status', 'pending')
    ->latest()
    ->limit(50)
    ->get();

Use ->select() to fetch only needed columns on large tables. Avoid SELECT * on tables with JSON blobs or long text fields.

Caching with Redis

Redis 8.10 sitting in front of MySQL handles session data, rate limits, and expensive aggregates. Cache invalidation must be explicit — stale inventory counts cause real eCommerce losses. Read improving web performance with caching strategies for TTL and tag patterns that work with Laravel's cache driver.

Do not cache everything. User-specific dashboards and legal document lists often need fresh reads. Product category trees and CMS menus are safe cache candidates.

Schema and migration hygiene

Use appropriate types. BIGINT for high-volume IDs. DECIMAL(12,2) for money — never float. Add foreign keys where referential integrity matters; InnoDB indexes FK columns automatically on the child side.

Run ANALYZE TABLE orders; after large imports so the optimizer picks correct indexes. On Deployer 7 releases I maintain, post-deploy migrations on big tables run through pt-online-schema-change or native ALGORITHM=INPLACE to avoid full locks.

Before vs After TuningBeforeAfterp95 query: 2400 msBuffer hit: 91%Queries per request: 4785 msBuffer hit: 99.4%3Fixes AppliedComposite index + eager loading + buffer pool 4 GBRedis cache on category tree — TTL 3600 s
Typical MySQL Performance Tuning Guide outcome — index and ORM fixes beat raw server tweaks alone

Managed RDS versus self-hosted EC2 is a cost and ops trade-off, not a performance silver bullet. See RDS vs self-managed MySQL on EC2 for when each makes sense. For teams comparing engines, PostgreSQL vs MySQL for production and MariaDB vs MySQL in 2026 cover migration triggers — not every slowdown means switch databases.

Replication and binary logs matter for read scaling and backups. Plan those after single-server tuning stabilizes — MySQL binary logs for replication and backup covers retention without filling disk.

Professional audits combine database work with server review. Our testing and optimization service and Linux system administration cover the full stack. Ongoing fixes land under support and maintenance when clients need monthly monitoring.

Use the JSON formatter tool when inspecting API payloads that trigger heavy JSON column queries. MySQL JSON functions are convenient but easy to abuse — functional indexes help, but simpler schemas often win.

Key Takeaways

  • Enable the slow query log and capture a baseline before changing indexes or config.
  • Run EXPLAIN ANALYZE on exact production SQL — fix scans and filesorts first.
  • Build composite indexes with equality columns leading, then range columns.
  • Set innodb_buffer_pool_size to 60–70% of dedicated DB RAM after query fixes.
  • Eliminate Laravel N+1 queries and cache read-mostly data in Redis 8.10 with clear TTLs.
  • Re-measure under the same workload — tuning without verification is guesswork.

People Also Ask

What is the most common cause of slow MySQL performance?

Missing or wrong indexes on hot queries cause most web app slowdowns. Full table scans on growing tables, N+1 ORM patterns, and SELECT * on wide rows follow close behind. Server misconfiguration ranks lower until query paths are clean.

How big should the InnoDB buffer pool be?

On a dedicated database server, allocate 60–70% of RAM to innodb_buffer_pool_size. On shared app+DB boxes, split carefully — starving PHP-FPM or the OS page cache creates new bottlenecks. Monitor hit rate and adjust.

Is MySQL 9.7 better for performance than MySQL 8.4 LTS?

MySQL 9.7 adds optimizer improvements and newer features. MySQL 8.4 LTS remains the safer choice on managed hosting with long support windows. Performance gains come mainly from indexes, query design, and hardware — not the minor version alone.

When should you partition a MySQL table?

Partition when a table exceeds tens of millions of rows and queries consistently filter by a partition key like created_at. Partitioning without matching query patterns adds complexity without speed gains. Archive old data when legal retention allows.

Ship Faster Queries With a Structured Tuning Plan

A repeatable MySQL Performance Tuning Guide beats one-off heroics every release cycle. Measure slow queries, fix indexes and application patterns, tune InnoDB memory, then verify under real traffic. That sequence has cleared production bottlenecks on eCommerce, booking, and legal-tech apps I have shipped since 2010 — without unnecessary database migrations or hardware spend.

If your app still stalls under load after internal fixes, an external review often finds the one digest eating half your database time. Contact us for a performance audit, or browse the portfolio for Laravel and MySQL projects already running in production. For related reading, start with MySQL optimization for SaaS and the master-slave replication setup guide when you outgrow a single server.

Frequently Asked Questions

Start with a baseline before changing indexes or config. Capture what is slow, how often it runs, and under what load. On Ubuntu servers I maintain, I pull four signals first: application timing from Laravel Debugbar in staging or APM in production, the MySQL slow query log, SHOW GLOBAL STATUS for buffer pool hit rate and temp tables, and OS metrics for CPU, disk I/O wait, and swap. Store page load, p95 query time, and buffer pool hit ratio in a spreadsheet or ticket. Without numbers you cannot prove a fix worked or spot regressions after the next deploy.

Missing or wrong indexes on hot queries cause most web app slowdowns. Full table scans on growing tables, N+1 ORM patterns, and SELECT on wide rows follow close behind. Server misconfiguration ranks lower until query paths are clean.

Edit /etc/mysql/mysql.conf.d/mysqld.cnf under the [mysqld] section—path varies by distro. Set slow_query_log = 1, slow_query_log_file = /var/log/mysql/slow.log, long_query_time = 1, and log_queries_not_using_indexes = 1. Restart or reload MySQL, reproduce traffic, then inspect entries with mysqldumpslow or MySQL Performance Schema. A threshold of one second works for most web apps. APIs may need 0.5 seconds. Batch jobs can use five seconds to reduce noise. Fresh installs on Ubuntu should follow a proper LEMP stack setup before custom tuning, since default configs assume a small dev machine.

The slow query log tells you what hurt users. Performance Schema tells you why. Use both when possible. Run EXPLAIN ANALYZE on the exact SQL your app sends—not a simplified version. Enable events_statements_summary_by_digest to surface queries ranked by total database time, not just individual slow executions. Percona Toolkit pt-query-digest groups slow log entries by fingerprint. In Laravel apps, pair it with Laravel Telescope and watch for N+1 patterns in Eloquent. A single controller action firing 80 queries often beats one slow query for total damage.

Watch for type = ALL, which means a full table scan on a large table. Rows in the millions for a paginated list is another warning sign. Using filesort or Using temporary on high-traffic paths adds avoidable overhead. key = NULL when a filter column clearly needs an index points to a missing or unused index. Every tuning session eventually lands on EXPLAIN. Fix scans and filesorts before raising max_connections or other server variables. Deeper analysis through Performance Schema often reveals one query digest accounting for a large share of total database time.

Indexes are the highest-ROI fix in most tuning jobs, but they also slow writes and waste disk if applied blindly. MySQL uses a composite index left-to-right. Put equality columns first and range columns last. For a query filtering by status and sorting by created_at, index (status, created_at). If you filter status = pending and created_at >= a date, the same index still works only when status is the leading column. Do not index every column. Low-cardinality fields like boolean flags rarely help alone—combine them with selective columns or skip them.

A covering index includes all columns the query reads, so MySQL can satisfy the query from the index alone without touching the table row. Example: idx_orders_list_covering on (status, created_at, id, total) for a filtered order list. Covering indexes pay off on read-heavy dashboards and APIs where the same columns appear repeatedly. They add write overhead on every insert and update to that table, so apply them to proven hot paths identified in the slow log—not speculatively across five indexes on one table.

On a dedicated database server, allocate 60–70% of RAM to innodb_buffer_pool_size. On shared app-plus-DB boxes, split carefully—starving PHP-FPM or the OS page cache creates new bottlenecks.

Server tuning comes after query fixes. MySQL 9.7 and the 8.4 LTS line share the same InnoDB core. innodb_buffer_pool_size is the single most important memory setting—aim for a hit rate above 99% using Innodb_buffer_pool_reads versus read_requests; below 95% usually means the pool is too small or the working set grew. Match max_connections to your PHP-FPM or app pool size plus admin headroom. Also review innodb_log_file_size, tmp_table_size and max_heap_table_size together, innodb_flush_log_at_trx_commit for durability trade-offs on non-financial logs, and table_open_cache when schema-heavy apps show Opening tables waits.

MySQL 9.7 adds optimizer improvements and newer features. MySQL 8.4 LTS remains the safer choice on managed hosting with long support windows. Performance gains come mainly from indexes, query design, and hardware—not the minor version alone.

Partition when a table exceeds tens of millions of rows and queries consistently filter by a partition key like created_at. Partition pruning helps time-range reports on audit and order history. Partitioning without matching query patterns adds complexity without speed gains—hours to days of effort with query planner surprises. Archive old data when legal retention allows instead of scanning full historical tables. Consider partitioning before throwing RAM at full scans on large historical tables.

Application-layer fixes often beat server knobs. On Laravel 12 or 13.x with PHP 8.3+, eliminate N+1 queries with eager loading via with() instead of lazy-loading relations in loops. Use select() to fetch only needed columns and avoid SELECT on tables with JSON blobs or long text. Use BIGINT for high-volume IDs and DECIMAL(12,2) for money—never float. Run ANALYZE TABLE after large imports so the optimizer picks correct indexes. On Deployer 7 releases, post-deploy migrations on big tables should run through pt-online-schema-change or native ALGORITHM=INPLACE to avoid full locks.

Measure first, fix queries and indexes second, and only then touch server variables. That order prevents the classic mistake of raising max_connections while a full table scan still runs on every page view. Enable the slow query log and capture a baseline, run EXPLAIN ANALYZE on exact production SQL, build composite indexes with equality columns leading, set innodb_buffer_pool_size after query fixes, and eliminate Laravel N+1 patterns. Re-measure under the same workload after each change. Tuning without verification is guesswork.

Redis 8.10 sitting in front of MySQL handles session data, rate limits, and expensive aggregates, reducing read pressure on InnoDB. Cache invalidation must be explicit—stale inventory counts cause real eCommerce losses. Do not cache everything. User-specific dashboards and legal document lists often need fresh reads. Product category trees and CMS menus are safe cache candidates with clear TTL patterns. Caching complements index fixes on read-mostly aggregates; it does not replace fixing a query that scans millions of rows on every request.

Managed RDS versus self-hosted EC2 is a cost and operations trade-off, not a performance silver bullet. Either environment can run MySQL 8.4 LTS or 9.7 with the same InnoDB core. Most production slowdowns trace to missing indexes, N+1 queries, and undersized buffer pools—not the hosting model alone. Single-server tuning should stabilize before adding replication or read scaling. Plan replication and binary log retention after query paths and indexes are clean, not as a first response to slow pages.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: