When WordPress feels sluggish without an obvious cause, the culprit is often a database slow query — an SQL statement that MySQL takes far too long to process. Every WordPress page view fires dozens of database queries: loading posts, comments, widget data, plugin options, and more. Even one slow query in that chain raises TTFB (Time To First Byte) measurably, particularly under load.

This guide walks you through a systematic approach: enabling the MySQL Slow Query Log, reading the results, analyzing execution plans with EXPLAIN, optimizing tables, and cleaning out database bloat — all in a way that works on both shared hosting and VPS environments.

Why WordPress Database Queries Become Slow

WordPress uses MySQL as its primary data store. Every incoming request follows this pipeline: PHP bootstrap → wp-config.php → MySQL connection → data retrieval (posts, options, meta) → HTML render → response. The database step consumes the most time because PHP must make repeated round trips to MySQL.

The most common root causes of slow queries in WordPress are:

Upgrading hardware or switching hosting plans is rarely the right first step. The goal is to identify which specific queries are slow and why, then fix the root cause.

Enabling the MySQL Slow Query Log

The first diagnostic step is turning on the slow query log to capture statements that exceed a time threshold. There are two approaches depending on your level of server access.

Method 1 — Runtime via MySQL Console (temporary, no restart needed)

Ideal for shared hosting where you can access phpMyAdmin but cannot edit server configuration files:

-- Enable slow query logging
SET GLOBAL slow_query_log = 'ON';

-- Flag queries taking longer than 1 second
SET GLOBAL long_query_time = 1;

-- Set the log file path
SET GLOBAL slow_query_log_file = '/tmp/mysql-slow.log';

-- Also log queries that skip indexes (highly recommended)
SET GLOBAL log_queries_not_using_indexes = 'ON';

These settings revert when MySQL restarts, making them safe for a temporary debugging session.

Method 2 — Edit my.cnf (permanent, for VPS with root access)

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

Apply the changes with a MySQL restart:

sudo systemctl restart mysql
# or on older systems:
sudo service mysqld restart

Reading and Analyzing the Slow Query Log

After the log has been running for a period of real traffic, analyze it with the built-in mysqldumpslow tool:

# Top 10 slowest queries by total execution time
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

# Top 10 most frequently executed slow queries
mysqldumpslow -s c -t 10 /var/log/mysql/slow.log

For richer analysis, install the Percona Toolkit and use pt-query-digest:

# Install on Ubuntu/Debian
sudo apt-get install percona-toolkit

# Analyze the log
pt-query-digest /var/log/mysql/slow.log

The output groups similar queries together, showing total execution time, average time per call, and the number of rows examined. Focus first on queries with the highest total execution time — these have the biggest real-world impact on site speed.

Using EXPLAIN to Understand Query Execution Plans

Once you have identified a slow query, run EXPLAIN in front of it to see how MySQL plans to execute it. This reveals whether indexes are being used:

EXPLAIN SELECT * FROM wp_postmeta
WHERE meta_key = '_thumbnail_id'
AND post_id IN (
  SELECT ID FROM wp_posts WHERE post_status = 'publish'
);

Key columns to examine in the EXPLAIN output:

Column Meaning Good Value Warning Value
type How MySQL accesses the table const, ref, range ALL (full table scan)
rows Estimated rows MySQL must examine Close to result set size Hundreds of thousands
key Index chosen for this step Index name NULL (no index used)
Extra Additional execution details Using index Using filesort, Using temporary

When you see type = ALL combined with key = NULL, MySQL is reading every row in the table to find matches. Adding an appropriate index on the columns used in the WHERE or JOIN clause will typically cut execution time by an order of magnitude.

Using the Query Monitor Plugin

If direct MySQL access is unavailable, the free Query Monitor plugin provides equivalent insight from inside the WordPress dashboard. Install it from Plugins > Add New and activate it.

What Query Monitor shows you:

-- Example of a common duplicate query Query Monitor catches
SELECT option_value FROM wp_options WHERE option_name = 'siteurl'
-- This single query can run 5–10 times per page when plugins fail to cache the result

When you find a slow or duplicated query, Query Monitor provides the filename and line number of the code that called it. This lets you go directly to the source to report the issue to the plugin author or find a replacement.

One important note: Query Monitor adds overhead to every page load. Always deactivate it once your investigation is complete — never leave debug plugins running in production.

Cleaning Up Database Bloat Before Optimizing

Running OPTIMIZE TABLE on a table that still holds gigabytes of stale data yields limited benefit. Clean the data first, then optimize. Here are the most impactful cleanup queries for WordPress sites.

Delete old post revisions

-- Check how many revisions exist
SELECT COUNT(*) FROM wp_posts WHERE post_type = 'revision';

-- Delete all revisions
DELETE FROM wp_posts WHERE post_type = 'revision';

-- Clean up orphaned postmeta rows left behind
DELETE pm FROM wp_postmeta pm
LEFT JOIN wp_posts p ON p.ID = pm.post_id
WHERE p.ID IS NULL;

Limit future revisions in wp-config.php

Add this line to wp-config.php before the That's all, stop editing! comment to prevent unlimited revisions going forward:

// Keep only the 3 most recent revisions per post
define('WP_POST_REVISIONS', 3);

Purge expired transients

-- Delete timed-out transients (values whose timeout has already passed)
DELETE FROM wp_options
WHERE option_name LIKE '_transient_timeout_%'
AND option_value < UNIX_TIMESTAMP();

-- Delete the corresponding transient value rows
DELETE FROM wp_options
WHERE option_name LIKE '_transient_%'
AND option_name NOT LIKE '_transient_timeout_%'
AND REPLACE(option_name, '_transient_', '_transient_timeout_')
    NOT IN (
        SELECT option_name
        FROM (SELECT option_name FROM wp_options) AS tmp
    );

Remove spam comments and orphaned comment meta

-- Count spam comments
SELECT COUNT(*) FROM wp_comments WHERE comment_approved = 'spam';

-- Delete spam
DELETE FROM wp_comments WHERE comment_approved = 'spam';

-- Clean up orphaned commentmeta
DELETE FROM wp_commentmeta
WHERE comment_id NOT IN (SELECT comment_ID FROM wp_comments);

Always back up before running DELETE statements. Export your database from phpMyAdmin or use a plugin like UpdraftPlus before executing any bulk delete operation. Run each query as a SELECT COUNT(*) first to verify the number of rows it will affect, then switch to DELETE only when the count looks correct. This two-step approach has saved many sites from accidental data loss.

Running OPTIMIZE TABLE

After cleanup, run OPTIMIZE TABLE to defragment tables and reclaim the space freed by deleted rows. This is most effective on wp_posts, wp_postmeta, wp_options, and wp_comments.

Via phpMyAdmin

  1. Open phpMyAdmin and select your WordPress database.
  2. Click the Structure tab and check all tables (or select specific ones).
  3. From the "With selected" dropdown at the bottom, choose Optimize table.
  4. Wait for all rows to show Status: OK.

Via MySQL command line

-- Optimize specific high-traffic tables
OPTIMIZE TABLE wp_posts, wp_postmeta, wp_options, wp_comments;

-- Or optimize every table in the database at once
mysqlcheck -u root -p --optimize your_wordpress_db

Adding Indexes to Speed Up Common WordPress Queries

If EXPLAIN shows key = NULL, adding a targeted index is often the single highest-impact fix available. The wp_postmeta table is frequently queried by both meta_key and meta_value together, yet the default WordPress schema only has a single-column index on meta_key.

-- Check existing indexes
SHOW INDEX FROM wp_postmeta;

-- Add a composite index for queries filtering by key + value together
ALTER TABLE wp_postmeta
ADD INDEX idx_key_value (meta_key, meta_value(20));

-- Add an index on autoload column to speed up WordPress startup
ALTER TABLE wp_options ADD INDEX idx_autoload (autoload);

-- Audit the total size of autoloaded options (should stay under ~800KB)
SELECT SUM(LENGTH(option_value)) AS autoload_bytes
FROM wp_options WHERE autoload = 'yes';

If autoload_bytes exceeds 800,000 (roughly 800KB), WordPress is loading too much data on every page request. Use the results to identify which plugin is adding the largest autoloaded options and either configure it to not autoload or find an alternative.

Additional wp-config.php Tuning

Several wp-config.php constants directly control how much data WordPress writes to the database on every save operation:

// Limit post revisions to 3
define('WP_POST_REVISIONS', 3);

// Increase autosave interval from 60s to 300s (reduces write frequency)
define('AUTOSAVE_INTERVAL', 300);

// Enable object caching (required for caching plugins like LiteSpeed Cache)
define('WP_CACHE', true);

// Automatically empty trash after 7 days instead of 30
define('EMPTY_TRASH_DAYS', 7);

// Ensure correct character set for full Unicode support
define('DB_CHARSET', 'utf8mb4');
define('DB_COLLATE', 'utf8mb4_unicode_ci');

Frequently Asked Questions

What is a WordPress slow query and how does it affect my site?

A slow query is a MySQL SQL statement that takes longer than a defined threshold to execute. MySQL's default is 10 seconds, but in practice any query taking over 1–2 seconds is considered slow. Every WordPress page view triggers dozens of database queries. Even a single slow query noticeably increases TTFB, especially under high traffic. Slow queries also spike CPU and memory usage on the database server, degrading performance for every other page on the same host.

How do I enable the MySQL Slow Query Log?

On shared hosting, open phpMyAdmin and run SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1; — these settings are temporary. On a VPS with root access, add slow_query_log=1, long_query_time=1, and slow_query_log_file=/var/log/mysql/slow.log to the [mysqld] section of my.cnf, then restart MySQL for a permanent change. Analyze the resulting log with mysqldumpslow or pt-query-digest.

Which WordPress plugin is best for analyzing database query performance?

Query Monitor (free) is the most capable option available from the WordPress Plugin Directory. It shows every database query per page load with execution time and a PHP stack trace pointing to the originating plugin or theme. It also highlights duplicate queries fired multiple times in a single request. Always deactivate debug plugins in production as they add significant overhead to every request.

How often should I optimize my WordPress database?

For high-activity sites — blogs that publish daily or WooCommerce stores with ongoing orders — optimize at least monthly. Sites with frequent content editing accumulate post revisions and expired transients quickly. Set up wp-cron or a server cron job to automatically clean revisions, spam comments, and expired transients. The most important tables to optimize regularly are wp_posts and wp_postmeta due to their high write-and-delete activity.

Hosting Optimized for WordPress

AsiaGB Hosting supports PHP 8.3, MySQL, LiteSpeed Cache, and WordPress Toolkit — starting at 500 THB/year.

View Hosting Plans