Uncached functions

WordPress core has a number of functions that, for various reasons, are uncached, which means that calling them will always result in an SQL query. Below, we outline some of these functions:

  • get_posts()
    • Unlike WP_Query, the results of get_posts() are not cached via Advanced Post Cache.
    • Use WP_Query instead, or set 'suppress_filters' => false.
      $args = array(
      	'post_type'        => 'post',
      	'posts_per_page'   => 3,
      	'suppress_filters' => false,
      );
      $query = get_posts( $args );
      
    • When using WP_Query instead of get_posts don’t forget about setting ignore_sticky_posts and no_found_rows params appropriately (both are hardcoded inside a get_posts function with value of true )
  • wp_get_recent_posts()
    • See get_posts()
  • get_children()
    • Similar to get_posts(), but also performs a no-LIMIT query among other bad things by default. Alias of break_my_site_now_please(). Do not use. Instead do a regular WP_Query and make sure that the post_parent you are looking for is not 0 or a falsey value. Also make sure to set a reasonable posts_per_page, get_children will do a -1 query by default, a maximum of 100 should be used (but a smaller value could increase performance)
  • term_exists()
    • Use wpcom_vip_term_exists() instead
  • get_page_by_title()
  • get_page_by_path()
    • Use wpcom_vip_get_page_by_path() instead
  • url_to_postid()
    • Use wpcom_vip_url_to_postid() instead
  • count_user_posts()
    • Use wpcom_vip_count_user_posts() instead.
  • wp_old_slug_redirect()
    • Use wpcom_vip_old_slug_redirect() instead.
  • get_adjacent_post()get_previous_post()get_next_post(), previous_post_link(), next_post_link()
    • Use  wpcom_vip_get_adjacent_post() instead.
  • attachment_url_to_postid()
    • Use  wpcom_vip_attachment_url_to_postid() instead.
  • wp_oembed_get()
    • Use wpcom_vip_wp_oembed_get() instead.

Querying on meta_value

There’s a quick rule of thumb to know if querying on meta value will be a problem. Ask yourself:

“Will I be querying for this meta_value using WP_Query?”

If the answer is no, then you’ve got a perfect use case for postmeta values.

If the answer is yes, then the query is likely to have issues with performance and scalability. This is because the WordPress postmeta table has an index on meta_key, but not meta_value.

However, many use cases can be modified to avoid performance problems:

  • Taxonomy Terms – Some meta_value queries can be transformed into taxonomy queries. For example, instead of using a meta_value to filter if a post should be shown to visitors with membership level “Premium”, use a custom taxonomy and a term for each of the membership levels in order to leverage the indexes.
  • Binary Situations – When meta_value is set as a binary value (ex. “hide_on_homepage” = “true”), MySQL will look at every single row that has the meta_key “hide_on_homepage” in order to check for the meta_value “true”. The solution is to change this so that the mere presence of the meta_key means that the post should be hidden on the homepage. If a post shouldn’t be hidden on the homepage, simply delete the “hide_on_homepage” meta_key. This will leverage the meta_key index and can result in large performance gains.
  • Non-binary Situations – Instead of setting meta_key equal to “primary_category” and meta_value equal to “sports”, you can set meta_key to “primary_category_sports”. This enables you to query by primary_category. However, instead of doing get_post_meta( $id, ‘primary_category’), you would need to iterate over possible values of primary_category with get_post_meta( $id, ‘primary_category_sports’). If you need to do both, you could use a ‘primary_category’ and a ‘primary_category_sports’ meta_key that both update when the primary category changes. Another, better, solution for this particular use case would be to use a hidden taxonomy named primary_category and have the categories be the terms.
  • Elasticsearch – If it’s not possible to avoid performing a meta_value query, consider using Elasticsearch instead of MySQL.

One caveat to note is that if you are using Elasticsearch on your site (regardless of if it’s for a particular query or just in general) having multiple distinct meta_keys such as the example of Non-binary situations could potentially cause severe performance problems for Elasticsearch. This is based on the way Elasticsearch will store the data and not how it queries the data and therefore it doesn’t matter if you are using elasticsearch for that particular query or not.

Term queries should consider include_children =>false

Previously 'include_children' => false was added to almost every taxonomy query on WordPress.com. When a term query is processed, WordPress first looks to see if the term is shared. When global terms were enabled on WordPress.com (before the 4.4 merge) this would always return true.

If the term is shared it will not include the children term. If the term is not shared, it will include the children.

As of 4.4 Terms have all been split on WordPress.com which means that this now adds 'include_children' => true to almost all taxonomy queries. This can have a very significant performance impact on code that was performant previously. In some cases this change means that queries that used to return almost instantly will time out. We therefore now recommend 'include_children' => false to be added to all taxonomy queries when possible.

In many instances where the wanted behaviour is to grab all posts that are either the parent or a child term this can be replaced by only querying for the parent term and enforcing inside a save_post() hook that if a child term is added that it’s parent term is also added. A one time WP CLI command might be needed to ensure previous data integrity.

Performance improvements by removing usage of post__not_in

Using post__not_in means that the query cache hitrate will often be a lot lower. It can also make the query slower if the exclusion list is large. In almost all cases you can gain great speed improvements by requesting more posts and skipping the posts in PHP.

example, instead of:

$other_posts_in_tag = get_posts( array(
	'tag_id' =>  $tag_id,
	'posts_per_page' => $limit,
	'post__not_in'	=> $array_of_post_ids_to_skip,
	'suppress_filters' => false,
));

foreach ( $other_posts_in_tag as $post ){
	//logic goes here;
}

do this:

$other_posts_in_tag = get_posts( array(
	'tag_id' =>  $tag_id,
	'posts_per_page' => $limit + count( $array_of_post_ids_to_skip ),
	'suppress_filters' => false,
));

foreach ( $other_posts_in_tag as $post ){
	if ( in_array( $post, $array_of_post_ids_to_skip ) ){
		continue;
	}
	//logic goes here;
}

Ready to get started?

Drop us a note.

No matter where you are in the planning process, we’re happy to help, and we’re actual humans here on the other side of the form. 👋 We’re here to discuss your challenges and plans, evaluate your existing resources or a potential partner, or even make some initial recommendations. And, of course, we’re here to help any time you’re in the market for some robust WordPress awesomeness.