WordPress Query Optimization

Support » Content Management » WordPress » WordPress Query Optimization

At IQComputing we believe in making websites run as fast as possible, regardless of whether a CDN or caching instrument is employed. Whenever developing a theme or working on a plugin optimization is always in the back of our mind. In this article we’ll explain some of the things we do to speed up our queries and some things to avoid. Before getting into the meat of the topic we must first understand how WordPress queries and loops work.

Skip Introduction – Onto The Main Course!

Quick Introduction to The Loop

Whenever a page or post is requested by the user, WordPress hits the database with the requested slug to get the page data such as title and content. Themes then use “The Loop” to display this data which looks something like this:

while( have_posts() ) {
    if( have_posts() ) {
        the_post();
        the_content();
    }
}

To break this down a bit:

  • have_posts() – Tells The Loop if there are still posts to loop through ( gets a boolean of true or false ).
  • the_post() – Uses setup_postdata() to set up our easy to use functions such as the_content() and the_title(). Also increments our post array if there are any additional posts to loop through.

The Loop is very simple and straight-forward. Now let’s find out the different ways developers can get posts for The Loop.

Query Inquiry

There are technically 3 generic ways for developers to get posts that WordPress can use:

  1. Jump to query_posts()
  2. Jump to get_posts()
  3. Jump to WP_Query()

There are more functions that return posts but are not generic in that the returned posts are altered or you’re limited in what you can do. An example could be get_pages() which only works with hierarchical post types. The word “technically” is used because not all the functions listed above are equal; one stands out above the rest. Which query is the best?

Function: query_posts()

It’s been said at least a million times in a thousand different ways and we probably (hopefully) don’t need to reiterate it but we will. There’s not a single scenario where this function is an appropriate one to use. Even The Codex states:

Note: This function isn’t meant to be used by plugins or themes. […] There are better, more performant options to alter the main query.

The idea for this function is that it will overwrite the current request with something new. For example, if you have a page for Testimonials and would like to replace this page with a list of posts from your custom post type testimonials you would use this function like so:

query_posts( array(
	'post_type'		=> 'testimonials',
	'posts_per_page'	=> 200,
) );

if( have_posts() ) {
	while( have_posts() ) { the_post();
		the_content();
	}
	
	wp_reset_query();
}

The core issue with this function is that it overwrites WordPress’s original query and alters some important globals. This means that instead of page content for the Testimonials page showing up we will instead see a list of testimonials which may sound exactly what we want to do but there’s a couple issues. See, whenever WordPress loads it queries the database using the page slug or post ID to pull the necessary data and dumps it into a global $wp_query object so that we can access and process it in The Loop (as seen above). As WordPress is processing and runs across this query_posts() function it will need to perform an additional database call and uses that data to overwrite the global $wp_query and global $post objects. This results in unnecessary overhead. Finally, we need to run wp_reset_query() to reset our global variables back to their original values.

Why go through all of that when we can use something easier and more efficient?

Action Hook: pre_get_posts

What an absolutely wonderful and useful hook. This mitigate any need to use query_posts() because we can intercept the main query before it’s even requested from the database and alter it right here. This is especially useful if a developer needs to change how many posts show up on an archive page (so not to mess up the default posts per page). Let’s take the above example using query_posts() and turn it into a more efficient query using pre_get_posts:

function theme_pgp( $query ) {
	if( is_page( 'testimonials' ) ) {
		$query->set( 'post_type', 'testimonials' );
		$query->set( 'posts_per_page', 200 );
	}
}
add_action( 'pre_get_posts', 'theme_pgp' );

The above will achieve the exact same thing as the query_posts() example without making an additional database request. Unfortunately, you will still lose the Testimonial pages content which we may want to keep. Let’s take a look at some secondary queries!

Function: get_posts()

Not nearly as inefficient as query_posts, this function will return a standard PHP array of Post Objects. This function generates what’s called a “Secondary Query” since it doesn’t overwrite any global variables. With that in mind, by default, you also do not get access to all those convenient functions such as the_content() and the_title() without use of the setup_postdata() function mentioned in the previous “Introduction to The Loop”. If you use the setup_postdata() function will also need to run wp_reset_postdata() to normalize the global $post object. The below example is the most basic usage of get_posts() which you could place above or below your normal content loop:

if( have_posts() ) {
	while( have_posts() ) {
		the_post();
		the_content(); // Normal Testimonial Page Content
	}
}

$testimonials = get_posts( array(
	'post_type'		=> 'testimonials',
	'posts_per_page'	=> 200
) );

if( ! empty( $testimonials ) ) {
	foreach( $testimonials as $post ) { setup_postdata( $post );
		the_content(); // Each testimonials post type's content
	}
	
	wp_reset_postdata();
}

The above is pretty great in its simplicity but it’s still not the best. Just a wrapper for something even more powerful!

The One Query to Rule Them All… WP_Query

WordPress Query Chart
WordPress Query Chart by Andrey (Rarst) Savchenko

All of the above query functions create an instance of the WP_Query Class. May as well skip the middleman and jump right into using WP_Query. The best part about this class is that, unlike get_posts(), you can write your secondary query using normal WordPress Loop procedures which will give you access to those juicy the_content() functions. Since we are able to use these functions that means that the `global $post` object will be overwritten and we will need to run wp_reset_postdata() to normalize. Let’s use the get_posts() example and convert it into a WP_Query instead:

$query = new WP_Query( array(
	'post_type' 		=> 'testimonials',
	'posts_per_page'	=> 200,
) );

if( have_posts() ) {
	while( have_posts() ) {
		the_post();
		the_content(); 	// Normal Page Content
	}
}

if( $query->have_posts() ) {
	while( $query->have_posts() {
		$query->the_post();
		the_content();	// Testimonial Secondary Query Content
	}
	
	wp_reset_postdata();
}

Clean and simple! On top of that, the WP_Query Object gives us access to some great properties and methods that we can use inside the loop such as:

  • Property: $query->current_post is a counter to let us know how many posts we’ve looped through so far (the index of the current post).
  • Property: $query->post_count lets us know how many posts are in our loop. Nice for modulus calculations.
  • Method: $query->rewind_posts() will set the post array back to 0 which is very useful if we need to loop through the query multiple times.

There are so many different combinations you can use to pull posts. The Codex is practically a book on all the different ways to get and display posts. If you run into issues with WP_Query or have questions about any of the above information there’s a few developer friendly spots to ask development related questions such as The WordPress Stack Exchange and the WordPress Support Forums. If you get really stuck you can definitely contact IQComputing for assistance!


Onto the Optimization!

Now that we’ve convinced you to use WP_Query forever (and ever and ever…) let’s review some of the properties we can use to make our queries perform faster and more efficient. Here’s the scenario:

Your client wants to add a list of testimonials to their website. They happen to hate pagination with the burning passion of 1000 suns so they want to display all testimonials no initial page load.

Great, sounds simple enough, right? Let’s check with The Codex and see what their pagination section states:

posts_per_page (int) – number of post to show per page. Use ‘posts_per_page’=>-1 to show all posts…

Parameter: posts_per_page

Alright, you’ve implemented the above and the client is happy so you’re happy. That is, until a couple months later when your clients business has exploded in popularity and have been adding customer testimonials by the truckload. Maybe they put an open form on the site for users to submit testimonials. Whatever the case is there’s now 1,000+ testimonials trying to display on that single page. Odds are good their server can’t handle that kind of load and PHP may not be able to process through that many posts. The server goes down and you see a 500 Internal Server Error which 10 times out of 10 is not a good sign.

If you haven’t guessed by now settings posts_per_page => -1 is not very scalable and can cause problems later down the line as the business grows. You could upgrade your server but eventually that won’t be enough so a better solution is to set a static, but large, upper-limit. Good upper-limits range from 100-500: posts_per_page => 200 which may not give your client exactly what they want but we feel it is a better tradeoff than showing users the Internal Server Error screen. Possibly the best of both worlds would be to use a lazy load plugin to dynamically pull in the posts as their needed such as Ajax Load More. This is also referred to as “Infinite Scroll”.

Parameter: no_found_rows

We can set this parameter to true: no_found_rows => true to stop any sort of pagination calculations. If we don’t set this parameter MySQL will try to get all posts matching the query no matter what the value of posts_per_page is. Using the same scenario, our client also wants to show 10 testimonials in the sidebar of every page. If we do not set this parameter to true then WordPress will look through all 1000+ to calculate how many pages there will be even though there will be no pagination in this sidebar query. In this case the calculation is unnecessary overhead that can be circumvented.

Parameter: fields

There’s only 2 options to set the fields parameter to:

  • 'fields' => 'ids'
  • 'fields' => 'id=>parent'

This tells our query to only return post ID’s and nothing else. At first glance this may not seem useful so we’ll give you some freebie code to explain. Below is an example to test if a given page has children underneath it. Since we don’t need any other values we only return IDs and ensure that there’s at least 1 post which improves the speed of our query:

function has_children( $post_id, $post_type = 'page' ) {
	$children = new WP_Query( array(
		'post_parent'		=> $post_id,
		'post_type'		=> $post_type,
		'posts_per_page'	=> 1,
		'post_status'		=> array( 'publish' ),
		'fields'		=> 'ids',
	) );
	
	return ( $children->have_posts() );
}

Here’s another example: Say you need to run a WordPress cronjob every month to remove testimonials older than 90 days. The wp_delete_post() function only needs a post ID to run so all we need to query are post IDs which makes this parameter delightfully perfect.

Parameters: update_post_term_cache and update_post_meta_cache

Before we get into these two parameters we need to know how WordPress handles some of it’s caching. Here’s a quick introduction:

Whenever WordPress runs a query of almost any kind it will cache all the data it may need which is commonly referred to as “Object Caching”. Most notably postmeta, terms, and term relations. This is convenient because if we do need things like postmeta, WordPress will reach into the cache instead of running an additional database query. Again, convenient if you need it but if you don’t plan on using any postmeta or checking against terms then there’s no need for WordPress to grab any of this information and is just unnecessary overhead. Luckily WP_Query has you covered with the aforementioned parameters!

Now that you know WordPress grabs everything the parameters may make more sense. If we don’t need or do not plan on using postmeta we can tell our query not to even look at the postmeta table: 'update_post_meta_cache' => false and the same goes for terms: 'update_post_term_cache' => false. You could turn both of these parameters to false by using 'cache_results' => false but if you plan on using a meta or term function even once, these parameters are not worth turning off. WordPress will generate another database call to get the meta or term relations and cache the data anyway. Certainly do not turn these off using pre_get_posts. Should you hand your project off to another developer and cache is turned off in pre_get_posts the new developer may accidently make additional database queries to get postmeta not knowing cache is disabled.

Additional Query Caching

WordPress also has a couple neat functions if you need to cache actual MySQL database results.

The above functions are considered to be “non-persistent caching” which means that they will disappear on page refresh (unlike transients which are presistent). This is useful if we need to make the same MySQL query multiple times through the page. For example, if we need to create a filter of postmeta values we could make a database call getting all the distinct values:

$results = $wpdb->get_col( $wpdb->prepare( "
	SELECT DISTINCT pm.meta_value FROM {$wpdb->postmeta} pm
	LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id
	WHERE pm.meta_key = '%s' 
	AND p.post_status = '%s' 
	AND p.post_type = '%s'
", $key, $status, $type ) );

If we need to display this in both the sidebar and before the main content we wouldn’t want to run this taxing query twice. Instead we can wp_cache_set( 'unique_meta', $results ) to set the cache on the first call and then wp_cache_get( 'unique_meta' ) to get our results without any additional database call. Fun, fast, and easy.


Hopefully the above has shed some light on how WordPress works and some ways to speed up your queries. You may say that caching plugins will take care of all the above and you wouldn’t be wrong . The above should assume that your client may not have a caching plugin, maybe the above queries will be built into a plugin or theme on the WordPress Repository. The above should assume that the client’s server will be of the lowest and cheapest quality (as they always seem to be, am I right?) so any queries need to be as fast as possible. Of course, you can also ignore all the above and let us take care of it for you, contact IQComputing for WordPess development help!

References