Technote

Performance optimization Intro

Series Profiling WordPress performance Part 2 of 8

Reading a query profile: slow versus duplicated

They sit side by side in the same panel but they are not the same problem. One needs the query rewritten; the other disappears with a cache. Telling them apart comes first.

With server time identified as the culprit, queries are next. Turn on a profiler such as Query Monitor and you get every query the request made — and judging that list by its total count is usually wrong. A page with 200 queries can easily be slower than one with 300.

There are two things to look for, and they have different causes and different fixes.

They appear in the same panel, but you fix them in different places

Reading the panel in order

Read the profiler top to bottom. In the overview, take total queries, total query time and peak memory. The important part is that total query time is only a slice of response time. If query time is small and the response is still slow, the bottleneck is PHP execution — outbound API calls, image processing, an overloaded hook — not the database.

Then sort the query list by duration and read the top few: those are your slow queries. Move to the duplicates panel and look at how many times the same SQL ran. Scanning the list as a single blob hides both: the one statement that owns the time, and the repetition that owns the count.

The most valuable column in any profiler is the caller. Where a query came from tells you where to fix it far better than the SQL does. Thirty identical statements from one function means a loop; thirty from thirty different places means the cache is simply empty.

When you cannot install a profiler

Query Monitor belongs on development and staging only. It exposes query and hook detail on screen and installs a db.php drop-in that adds overhead to every query — leave it on in production and the diagnostic tool becomes part of the problem.

Where a plugin is not an option, one core constant gives you the same data.

// wp-config.php — staging only
define( 'SAVEQUERIES', true );

With it enabled, $wpdb->queries collects [SQL, duration, call stack] for the request. One sort and one tally at the end of the request reproduce both lists.

add_action( 'shutdown', function () {
    global $wpdb;

    if ( ! defined( 'SAVEQUERIES' ) || ! SAVEQUERIES ) {
        return;
    }

    $rows = $wpdb->queries;

    // 1. slow — sort by duration, descending
    usort( $rows, fn( $a, $b ) => $b[1] <=> $a[1] );
    error_log( 'slowest: ' . round( $rows[0][1] * 1000 ) . 'ms ' . $rows[0][0] );

    // 2. duplicated — how often did the same SQL run?
    $dupes = array_count_values( array_column( $rows, 0 ) );
    arsort( $dupes );
    error_log( 'top repeat: ' . reset( $dupes ) . 'x ' . key( $dupes ) );
} );

Under twenty lines, and those two log entries decide what you work on. One statement standing far above the rest has to be rewritten — a cache in front of it still leaves the first request paying the full cost — while the same SQL appearing dozens of times mostly disappears once the caches are primed.

Traps when reading the numbers

Fewer queries does not automatically mean faster, and fixing one slow statement can transform a page while the count stays identical. Count is a clue, not a target. When you report an improvement, give response time and query count together, and if only one of them moved, say so.

Also, never mix an admin-screen profile with a front-end one. Admin screens legitimately run more queries — list tables, notices, update checks — and using that figure as a baseline for a public page sends you digging in the wrong place.

Articles on query structure and indexing sit in the performance archive, and if you would rather hand diagnosis and remediation over as one job, that is our optimization program.

Next part

If duplication dominates your profile, there is usually a single cause. The next part is N+1 — what get_post_meta() inside a loop actually costs, and the priming functions core already ships.

More on this topic

All technotes

Performance optimization Practical

Who pays for the milliseconds a tag adds?

A tracking script does not stop at downloading. While it is parsed and executed the main thread is busy, and a busy page ignores fingers. That cost is…

Marketers 5 min read

Performance optimization Practical

The object cache: what Redis actually removes

Repeated identical lookups disappear. One slow query does not. Miss that distinction and you will wire up Redis and then ask why nothing got faster.

Developers 7 min read

Performance optimization Practical

This is how a landing page gets heavy

Nobody decides to make a landing page heavy. It accumulates: one addition per campaign, and nothing ever removed when the campaign ends.

Marketers 6 min read

₩270,000 · Join the program