• Indonesian
  • English
  • MySQL Slow Query Optimization: Complete Troubleshooting

    Kecepatan:
    ⏱ 11 min read

    Ever had one of those days where your server specs look totally fine — CPU barely breaking a sweat, RAM sitting pretty with plenty of headroom — yet your website feels like it’s running through molasses? I was scratching my head over exactly this situation last week. MySQL was the culprit, and it took me a solid afternoon of digging to figure out why a handful of queries were absolutely tanking the whole user experience. So here’s the deal: MySQL slow queries are one of those problems that can hide in plain sight. Your monitoring dashboards show green across the board, but underneath, there’s a query or two absolutely demolishing your response times. And the worst part? Most of the time, it’s not a hardware issue at all — it’s a query that was never properly indexed, or an ORM generating garbage SQL that nobody bothered to check. Alright, let’s dive in. I’m going to walk you through exactly how I approach slow query optimization on production MySQL servers, step by step. No fluff, just practical stuff that actually works. I’ve done this on everything from shared hosting boxes to bare-metal dedicated servers running millions of queries per hour, and the pattern is almost always the same.

    The thing about slow queries is they don’t just slow down one page — they compound. One slow query running 500 times per hour? That’s 500 times your server is doing unnecessary work. Times 24 hours. Times 30 days. The math gets ugly fast. And if you’re on a VPS with limited resources, those wasted cycles eat into CPU time that other processes need. Before you know it, load average spikes, connections pile up, and suddenly your “healthy” server is throwing 502 errors. I’ve seen it happen more times than I can count. The good news? Most slow query issues are completely fixable without spending a dime on hardware upgrades. You just need to know where to look and what to do about it.

    Let me be real with you — this isn’t glamorous work. Nobody’s going to give you a standing ovation for tuning a MySQL query. But when your boss asks why the site is slow and you can point to the exact query causing it and show the fix, that feels pretty good. Trust me, I’ve been there. And the fix is usually simpler than you’d expect. Most of the time, it’s either a missing index, a poorly written query, or a misconfigured MySQL instance. Sometimes it’s all three. Let’s break it down.

    Difficulty: Intermediate
    Last Updated: August 2026
    Tested On: MySQL 8.0.36 / MariaDB 10.11.6 / Ubuntu 22.04 / AlmaLinux 9

    MySQL slow query optimization with EXPLAIN analysis

    Why Are Queries Running Slow in the First Place?

    Before we start fixing things, let’s understand what’s actually happening when a query runs slow. Think of it like driving without GPS in an unfamiliar city. If you know the route, you get there fast. If you don’t, you’re stopping at every intersection, checking your phone, probably taking a few wrong turns. MySQL does the same thing when it doesn’t have the right indexes — it scans through data row by row until it finds what it needs. That’s called a full table scan, and on a table with millions of rows, it’s devastating.

    The first thing you absolutely need is visibility into what’s happening. MySQL has a built-in feature called the slow query log that records any query taking longer than a specified threshold. Shockingly, most production servers have this disabled. Either nobody thought to enable it, or someone disabled it because they were worried about disk space. Either way, flying blind on production is not a great strategy. Here’s how to enable it:

    SHOW VARIABLES LIKE 'slow_query%';
    SHOW VARIABLES LIKE 'long_query_time';

    Check if it’s OFF — and if it is, turn it on immediately. Seriously, don’t wait. Every minute it’s off, you’re missing data that could help you fix performance issues faster.

    SET GLOBAL slow_query_log = 'ON';
    SET GLOBAL slow_query_log_file = '/var/log/mysql/slow-query.log';
    SET GLOBAL long_query_time = 1;

    I usually start with long_query_time set to 1 second for production. That captures queries that are noticeably slow without flooding the log with borderline cases. For active troubleshooting, drop it to 0.5 or even 0.2 seconds. The key is finding that sweet spot between catching problematic queries and not overwhelming yourself with noise. Once you’ve got the log running, give it a day or two to collect data, then dig in.

    Reading EXPLAIN Output Like a Pro

    This is where the real detective work begins. EXPLAIN is MySQL’s way of showing you its execution plan — basically how it intends to find the data you’re asking for. Run EXPLAIN before any query you suspect is slow:

    EXPLAIN SELECT u.name, o.total, o.created_at
    FROM users u
    JOIN orders o ON u.id = o.user_id
    WHERE u.status = 'active'
    AND o.created_at > '2026-01-01'
    ORDER BY o.total DESC
    LIMIT 20;

    Now, here’s what you need to look for. The type column tells you how MySQL is accessing the table — if you see ALL, that’s a full table scan and it’s bad. You want to see ref, eq_ref, or range instead. The rows column shows how many rows MySQL expects to examine. If that number is in the millions but your LIMIT is 20, something’s off. And watch the Extra column carefully — Using temporary and Using filesort are red flags meaning MySQL had to create temporary tables or sort results in memory because there was no suitable index.

    Here’s a quick reference table I keep bookmarked:

    EXPLAIN Extra What It Means What To Do
    Using filesort MySQL must sort results because no index covers the ORDER BY Create a composite index covering the ORDER BY columns
    Using temporary Temporary table created — usually from GROUP BY or DISTINCT Optimize the GROUP BY or add a covering index
    Using where Filtering happens after fetching from table Ensure WHERE columns are indexed
    Using index Covering index — no need to access table data Great! Keep this pattern
    type: ALL Full table scan — scanning every row Add index on filtered/joined columns

    Creating the Right Indexes

    Look, I get it — indexes sound like one of those boring database theory topics. But here’s the reality: the right index can turn a 3-second query into a 3-millisecond query. That’s a 1000x improvement. And it takes about 30 seconds to create. Best ROI you’ll ever get in performance tuning.

    Based on the EXPLAIN output above, here’s what I’d create:

    -- Index for the WHERE filter on users table
    CREATE INDEX idx_users_status ON users(status);
    
    -- Composite index covering the JOIN, WHERE, and ORDER BY
    CREATE INDEX idx_orders_user_created_total 
    ON orders(user_id, created_at, total);

    The composite index on orders is doing a lot of heavy lifting here. It lets MySQL jump directly to the right user_id, then range scan on created_at, and finally it already has total for the ORDER BY — meaning no filesort needed. That’s a covering index pattern, and it’s exactly what you want to aim for.

    But don’t go index-crazy. Every index you add has a cost: more disk space, slower writes (because MySQL has to update the index on every INSERT/UPDATE/DELETE), and longer backup times. Only create indexes for queries that run frequently and have a real impact. If a query runs twice a day with 50 rows, it probably doesn’t need a custom index. But if it runs 500 times per hour on a table with 5 million rows? Absolutely create that index.

    Rewriting Queries for Better Performance

    Sometimes the fix isn’t adding an index — it’s rewriting the query entirely. Here are the most common anti-patterns I see in production code:

    -- BAD: Using function on column (not sargable)
    SELECT * FROM orders WHERE YEAR(created_at) = 2026;
    
    -- GOOD: Sargable range condition
    SELECT * FROM orders
    WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01';

    See the difference? The first query wraps created_at in a function, which means MySQL can't use an index for the lookup — it has to evaluate the function on every single row. The second query compares raw column values directly, which lets MySQL use a range scan on an index. Massive performance difference.

    Another common issue is the N+1 query problem, especially in applications using ORMs:

    -- BAD: N+1 queries (one query per user)
    SELECT * FROM users WHERE status = 'active';
    -- Then for each result:
    SELECT * FROM orders WHERE user_id = ?;
    
    -- GOOD: Single JOIN or eager loading
    SELECT u.*, o.id, o.total
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id
    WHERE u.status = 'active';

    If you have 1000 active users, the N+1 pattern fires 1001 queries. The JOIN pattern fires 1. The difference in production is staggering — we're talking orders of magnitude. If you're using Laravel, use eager loading with with(). If you're using WordPress, check your plugins for query patterns like this, and consider reading our WordPress database optimization guide for CMS-specific tips.

    Tuning MySQL Server Configuration

    Even perfectly optimized queries can run slow if your MySQL configuration is garbage. Here are the settings that matter most:

    # /etc/mysql/mysql.conf.d/mysqld.cnf or /etc/my.cnf
    
    # Buffer pool — set to 50-70% of RAM on dedicated MySQL servers
    innodb_buffer_pool_size = 3G
    
    # Temporary table limits
    max_heap_table_size = 256M
    tmp_table_size = 256M
    
    # Join and sort buffers
    join_buffer_size = 4M
    sort_buffer_size = 4M
    
    # Thread cache
    thread_cache_size = 64
    
    # InnoDB settings
    innodb_flush_log_at_trx_commit = 2
    innodb_log_file_size = 512M

    innodb_buffer_pool_size is the single most important setting. It determines how much data and index information MySQL can keep in RAM. If it's too small, MySQL falls back to disk reads, which are orders of magnitude slower. On a dedicated MySQL server, set this to 50-70% of total RAM. On a VPS with limited memory, you'll need to be more conservative — check out our guide on optimizing MySQL on small RAM VPS for specific numbers.

    Also worth noting: if you're running MySQL 8.0, the query cache has been removed. It was actually counterproductive in high-concurrency environments because it used a global lock. For read-heavy workloads, consider ProxySQL or application-level caching with Redis instead.

    Setting Up Ongoing Monitoring

    Here's a truth bomb: optimization isn't a one-time event. Your database evolves — data grows, traffic patterns shift, new features add new queries. What was perfectly optimized six months ago might be crawling today. You need ongoing monitoring.

    The performance_schema in MySQL 8.0 is your best friend here. It tracks query statistics without any additional setup:

    SELECT
      DIGEST_TEXT AS query_pattern,
      COUNT_STAR AS exec_count,
      ROUND(AVG_TIMER_WAIT/1000000000, 2) AS avg_time_ms,
      SUM_ROWS_EXAMINED AS total_rows_examined
    FROM performance_schema.events_statements_summary_by_digest
    ORDER BY AVG_TIMER_WAIT DESC
    LIMIT 10;

    This gives you the top 10 slowest queries by average execution time. Sort by COUNT_STAR * AVG_TIMER_WAIT to find queries with the highest total impact. Those are your priority targets. Set up a weekly routine: check slow query log, run this query, investigate any new entries. Takes 15 minutes and can save you from a production incident.

    Also run ANALYZE TABLE periodically on heavily modified tables. Index statistics can become stale over time, causing MySQL to choose suboptimal query plans. A fresh ANALYZE refreshes those statistics and often improves query performance immediately with zero code changes.

    Q: How do I know when MySQL needs a new index?

    Check your slow query log for frequently executed slow queries. Run EXPLAIN on them — if you see type: ALL (full table scan), Using filesort, or Using temporary, that's a strong indicator an index is needed. Also use performance_schema to identify queries that examine far more rows than they return.

    Q: Should I index every column that appears in a WHERE clause?

    No. Every index adds overhead to writes and uses disk space. Focus on columns that are frequently used in WHERE, JOIN, and ORDER BY clauses for high-frequency queries. Low-frequency queries rarely justify custom indexes.

    Q: What long_query_time should I use in production?

    1 second is a solid default for production web servers. For active troubleshooting, drop it to 0.5 or 0.2 seconds to catch more borderline queries. Be careful on high-traffic servers — too low a threshold will generate enormous log files.

    Author: Syslog Solutions — NOC & Server Management Team. We handle 500+ servers daily, from shared hosting to enterprise dedicated infrastructure.

    There you have it — a practical, step-by-step approach to tackling MySQL slow queries that actually works in production. Enable the slow query log, read EXPLAIN like a pro, create targeted indexes, rewrite bad query patterns, tune your MySQL config, and set up monitoring. That's it. Six steps, and you'll see measurable improvement in your database performance.

    Start with the easy wins: turn on the slow query log today, give it a couple days, then tackle the worst offenders. Check: 1) slow query log is active, 2) long_query_time is appropriate, 3) EXPLAIN shows no full table scans, 4) indexes cover your WHERE/JOIN/ORDER BY columns, 5) no N+1 query patterns lurking in your application code. Do this consistently and your MySQL performance will be night and day. Here's what worked for me — now go make it work for you.