A single slow database query can turn a sub-second WordPress page load into a three-second frustration. The query itself rarely announces itself — it hides inside a plugin loop, a poorly written WP_Query call, or a meta lookup that fires a hundred times per request. This guide covers every practical tool and technique for finding those queries and eliminating them permanently.
Why WordPress Database Queries Get Slow
WordPress stores almost everything in MySQL: posts, meta fields, terms, options, user data. Every page load triggers dozens — sometimes hundreds — of SQL queries. Most are fast, but a few common patterns cause serious slowdowns:
- Unbounded meta lookups — calling
get_post_meta()inside a loop without pre-loading the cache - No indexes on searched columns — querying
wp_postmeta.meta_valuewithout a composite index triggers a full table scan - Missing
no_found_rows— every paginatedWP_Queryruns a secondCOUNT(*)query you may not need - The N+1 problem — one query fetches N records, then N more queries fetch related data one by one
- Stale autoloaded options — the
wp_optionstable grows bloated with autoloaded rows, loaded on every request - No object caching — repeated identical queries run against the database instead of a memory cache
Knowing the cause is only half the fix. You need tools that show you which query is slow, where it originates, and how expensive it actually is.
Step 1 — Enable SAVEQUERIES in wp-config.php
WordPress has a built-in query logger. Set the SAVEQUERIES constant to true in wp-config.php and WordPress stores every query, its execution time, and the PHP call stack that triggered it inside $wpdb->queries.
SAVEQUERIES stores every query in memory. Enable it on a staging server or a local environment — never on production under real traffic.
Once enabled, add a shutdown hook that dumps and sorts the queries by execution time so you see the slowest offenders at the top of the page:
Reload the page. A panel appears at the bottom showing every query sorted from slowest to fastest. Anything above 100ms is a red flag. Anything above 500ms is a fire.
Reading $wpdb->last_query for a Known Slow Operation
If you already know which feature is slow — a specific widget, a shortcode, a custom REST endpoint — use $wpdb->last_query and $wpdb->last_error to inspect exactly what SQL was produced:
This outputs a sticky footer visible only to admins, showing the last query and any database error. It is faster than reading a full query log when you have a specific suspect.
Step 2 — Use Query Monitor for Visual Profiling
Query Monitor is the most practical tool for finding slow queries on a running WordPress install. Install it from the plugin directory, and it adds a persistent admin bar panel that breaks down every request:
- Total query count and cumulative execution time
- Each query’s SQL, time, caller (function, hook, plugin file and line number)
- Duplicate queries — identical SQL run more than once
- Slow queries highlighted above a configurable threshold (default 0.05s)
- Queries grouped by component (which plugin or theme triggered them)
Query Monitor’s “Queries by Component” tab is the fastest way to blame a specific plugin for an excessive query count. If one plugin accounts for 40% of your queries, you have your answer.
Query Monitor requires no code changes. Install, activate, load your page, and open the panel. The “Queries” tab shows every query with its caller. The “Duplicate Queries” tab shows queries run more than once — these are almost always bugs rather than intentional behavior.
Filtering Query Monitor Results
On a busy page with 80+ queries, the list is noisy. Use the built-in filters:
- Filter by component — isolate queries from a single plugin or theme
- Filter by type —
SELECT,INSERT,UPDATE,DELETE - Sort by time — click the Time column header to surface the slowest first
- Search SQL — paste a table name or meta_key to find related queries instantly
Query Monitor also exposes HTTP API calls, REST API requests, hooks fired, and PHP errors in the same interface — making it the single most useful debugging plugin for WordPress developers.
Step 3 — Identify and Fix the N+1 Query Problem
The N+1 problem is the most common cause of runaway query counts in WordPress. The pattern: one query fetches a list of posts, then a loop calls get_post_meta() for each post individually. With 50 posts, that is 51 queries. With 200 posts, it is 201.
The fix is update_post_meta_cache: true on WP_Query. This is already the default, but it gets disabled in poorly written plugins — or developers explicitly set it to false thinking it saves queries, without realising the meta loop downstream.
When to Disable the Meta Cache
Disabling update_post_meta_cache is valid only when your loop never calls get_post_meta(). Common valid cases: building sitemaps (IDs only), export scripts, REST endpoints returning raw post objects, batch processing jobs. If in doubt, leave it enabled.
Step 4 — Control Meta and Term Caches Precisely
WordPress pre-loads both post meta and taxonomy terms for every WP_Query result set by default. Each pre-load runs as one bulk query — far better than N individual lookups. But on queries where you genuinely do not need meta or terms, both pre-loads are wasted overhead.
A few additional WP_Query arguments that reduce unnecessary work:
| Argument | Default | When to set false |
|---|---|---|
update_post_meta_cache | true | Loop never calls get_post_meta() |
update_post_term_cache | true | Loop never reads terms/taxonomies |
no_found_rows | false | You do not need pagination (saves COUNT(*)) |
fields | ‘all’ | Set ‘ids’ when you only need post IDs |
cache_results | true | One-off admin scripts (avoids filling object cache) |
Combining fields: 'ids', no_found_rows: true, and both cache flags set to false is the fastest possible WP_Query — appropriate for batch jobs and background processes that process every post in the database.
Step 5 — Run EXPLAIN on the Slow Query
Once you have the raw SQL from Query Monitor or SAVEQUERIES, run EXPLAIN against it in MySQL. EXPLAIN reveals the execution plan: which indexes MySQL uses, how many rows it scans, and whether it needs a temporary table or filesort.
What Each EXPLAIN Column Means
| Column | Good Value | Bad Value |
|---|---|---|
type | ref, eq_ref, range | ALL (full table scan) |
key | An index name | NULL (no index used) |
rows | Low (single digits to hundreds) | Millions |
Extra | Using index | Using filesort, Using temporary |
The most frequent offender in WordPress is wp_postmeta with type: ALL on the meta_value column. WordPress indexes meta_key but not meta_value. Queries that filter by both need a composite index on (meta_key, meta_value) — included in the SQL file above. For a complete step-by-step walkthrough, see how to add custom database indexes to wp_postmeta.
Adding a composite index on
wp_postmeta (meta_key, meta_value)is one of the highest-impact single changes you can make on a WooCommerce or EDD store with heavy meta filtering.
Step 6 — Cache Expensive Query Results with Transients
Some queries are inherently expensive — large aggregates, complex JOINs, meta-filtered product lists. If the underlying data changes infrequently, cache the result with WordPress transients instead of re-running the query on every page load.
Transients are automatically stored in the wp_options table if no persistent object cache is installed. With Redis or Memcached installed (via the Redis Object Cache plugin), transients move into memory and become even faster. The code above does not need to change — WordPress handles the storage layer transparently.
Transient TTL Guidelines
- Live inventory / stock status — 5 minutes maximum
- Product listings, category pages — 1 hour, invalidate on save_post
- Sales totals, report widgets — 12–24 hours
- Static aggregates (tag clouds, archives) — 7 days, invalidate on publish
Always pair a set_transient() call with a targeted delete_transient() on the relevant save hook. Stale caches that serve outdated data are worse than no cache at all.
Step 7 — Enable the MySQL Slow Query Log via WP-CLI
When you cannot reproduce a slow query locally — it only appears under real traffic or on the production database — enable MySQL’s built-in slow query log via WP-CLI. No SSH access to MySQL’s configuration files required.
The slow query log writes any query taking longer than long_query_time seconds to a file. Set log_queries_not_using_indexes to ON to also capture fast-but-unindexed queries that will become slow as data grows.
Analysing the Log with mysqldumpslow
Once you have reproduced the slow behaviour, the log file contains raw SQL with timing data. Use mysqldumpslow (bundled with MySQL) to aggregate and rank the entries:
mysqldumpslow -s t -t 10 /tmp/mysql-slow.log— sort by total time, show top 10 queries. This collapses queries with different literal values into a single pattern so you see the worst offenders by cumulative impact, not just single-run time.
The wp_options Autoload Problem
WordPress loads all rows from wp_options where autoload = 'yes' on every single request. Plugins that store large serialised arrays with autoload enabled add kilobytes — sometimes megabytes — to every page’s startup cost, before a single query runs.
Audit your autoloaded options with this WP-CLI command:
wp db query "SELECT option_name, LENGTH(option_value) AS size FROM wp_options WHERE autoload='yes' ORDER BY size DESC LIMIT 20;"
Any option over 10KB with autoload enabled is a candidate for investigation. Transient entries with autoload enabled are a specific red flag — they should have autoload = 'no'. Fix them:
wp db query "UPDATE wp_options SET autoload='no' WHERE option_name LIKE '_transient_%';"
After changing autoload values, flush the object cache: wp cache flush.
Persistent Object Caching: The Final Layer
All the fixes above reduce how many queries run and how expensive each query is. Persistent object caching eliminates re-running identical queries across requests entirely.
WordPress’s built-in object cache is per-request only — it resets on every page load. Install Redis or Memcached alongside the Redis Object Cache plugin (by Till Kruss) and the cache persists across requests. WordPress’s caching API — wp_cache_get(), wp_cache_set(), get_transient() — works identically from your code’s perspective. The storage layer switches to memory automatically.
- Redis Object Cache plugin — the standard choice, connects via TCP or Unix socket
- W3 Total Cache — includes object cache support alongside page cache
- LiteSpeed Cache — native LSCache with object caching if on LiteSpeed server
On Cloudways, DigitalOcean App Platform, or WP Engine, Redis is available with one click. On shared hosting, you typically have Memcached. Either works — the difference in WordPress query reduction is negligible between the two.
Quick Diagnostic Checklist
Work through this list in order when a WordPress site has slow database performance:
- Install Query Monitor. Load the slow page. Check total query count — anything above 50 for a simple page is a warning sign.
- Check the “Duplicate Queries” tab. Each duplicate is an immediate win.
- Check the “Queries by Component” tab. Identify which plugin accounts for the most queries.
- Enable SAVEQUERIES on staging. Run the dump snippet. Note every query above 50ms.
- Run EXPLAIN on the top 3 slowest queries. Check for full table scans and missing indexes.
- Audit
WP_Queryloops: areupdate_post_meta_cacheandupdate_post_term_cacheappropriate for what the loop actually does? - Check
wp_optionsautoload bloat. Fix transients with autoload enabled. - Install Redis object caching if not already present.
- Add transient caching around any remaining slow queries that run frequently with stable data.
What to Do When a Plugin Is the Culprit
Query Monitor frequently reveals that a single plugin accounts for 30–60% of all queries on a page. Your options, in order of preference:
- Check for a caching setting in the plugin — many plugins have built-in cache options that are disabled by default.
- Open a support ticket with the query data — include the SQL, the caller, and the execution time. Good plugin authors fix this.
- Wrap the plugin’s output in a transient — if the plugin exposes a function or shortcode, cache its output externally.
- Replace the plugin — a plugin generating 40 queries to display a widget that needs 1 is a code quality problem unlikely to be fixed quickly.
- Use a query intercept filter —
queryfilter on$wpdblets you intercept, cache, or short-circuit specific queries in custom code.
Never disable a plugin’s queries by hacking its core files. That change will be overwritten on the next update and leaves you with a maintenance burden that only compounds over time.
Measuring Improvement
After applying fixes, measure the same page with the same tools before drawing conclusions. Query Monitor shows total query time in the admin bar — compare the before and after numbers directly. For production, use New Relic, Datadog, or your hosting platform’s APM if available. Otherwise, a simple time curl -o /dev/null -s https://yoursite.com/slow-page/ run ten times and averaged gives a reliable baseline.
Target numbers for a well-optimised WordPress page:
| Metric | Acceptable | Optimised |
|---|---|---|
| Total queries | < 50 | < 20 |
| Total DB time | < 200ms | < 50ms |
| Slowest single query | < 100ms | < 20ms |
| Duplicate queries | < 5 | 0 |
Series: Database Optimization for WordPress
This article is part 6 of the TweaksWP Database Optimization series. Other articles in the series cover table structure auditing, autoloaded options cleanup, transient management at scale, and optimizing WooCommerce database tables specifically. Work through the series in order for a systematic approach to WordPress database performance.
Stop Guessing, Start Measuring
Slow WordPress database queries are always fixable — but only after you can see them. Enable Query Monitor today, run through the checklist above, and you will have a clear picture of your site’s database load within 15 minutes. The hard part is not finding the slow queries; it is knowing what to do once you find them. Now you do.
Browse the rest of the TweaksWP database optimization series for deeper dives into specific scenarios — WooCommerce meta tables, term relationship bloat, and optimizing WordPress for high-concurrency environments.
database optimization MySQL performance Query Monitor SAVEQUERIES WordPress Database
Last modified: February 27, 2026