all insights/

Optimizing WordPress Queries for High Traffic

4 Best CMS for Local Newsrooms

Handling database queries in WordPress is crucial for the efficient and secure functioning of your website. In this guide, you will find different ways of optimizing queries that interact with your database.

Optimizing Queries: General Rules

1. Use WordPress API Functions Instead of Raw Queries

Instead of writing direct database queries, you can use WordPress’s built-in functions like WP_Query and get_posts. This improves your code’s performance and even makes it easier to read.

Here’s an example:

Let’s say you want to find posts edited by John Doe. With a direct query, you’d write complex code involving $wpdb, a WordPress database access variable.

Original query:

global $wpdb;

$editor = 'John Doe';

// Find posts where John Doe is the article editor
$query = $wpdb->prepare(
    "SELECT post_id FROM {$wpdb->postmeta}
    WHERE meta_key = 'article_editor' AND meta_value = %s",
    $editor
);

$results = $wpdb->get_col($query);

if ($results) {
    $post_ids = array_map('intval', $results);
    // $post_ids contains all the IDs where John Doe is the article editor
} else {
    $post_ids = array();
    // No posts found where John Doe is the article editor
}

This method gets the job done but misses out on WordPress’s security features and might not work as efficiently as it should.

A better approach is to use the WP_Query class:

  1. Set up your query parameters. For instance, specify that you’re looking for posts where ‘John Doe’ is listed as the article editor.
  2. Create a new WP_Query object with these parameters.
  3. Check if there are any posts that match your criteria.
  4. If you find posts, get their IDs. If not, you’ll have an empty list.

Using WP_Query is smarter because it follows WordPress guidelines. It’s more secure, works well with other parts of WordPress, and takes advantage of built-in performance improvements. This approach not only simplifies your code but also ensures it’s up to WordPress standards.

Here’s how the code looks like using WP_Query class:

$editor = 'John Doe';

$query_args = array(
    'meta_key' => 'article_editor',
    'meta_value' => $editor ,
    'fields' => 'ids',
);

$posts_by_editor = new WP_Query($query_args);

if ($posts_by_editor->have_posts()) {
    $post_ids = $posts_by_editor->posts;
    // $post_ids contains all the IDs where John Doe is the article editor
} else {
    $post_ids = array();
    // No posts found where John Doe is the article editor
}

The original query, while functional, skips the advantages of WordPress’s built-in security checks and optimizations. Conversely, the second query utilizing WP_Query not only adheres to WordPress best practices but also ensures compatibility, security, and performance enhancements by leveraging the platform’s native functionality.

2. Fine-Tuning WordPress Queries On-The-Fly with pre_get_posts

The pre_get_posts filter in WordPress is a handy hook which allows adjustments to queries without directly altering the source code where the original query is written. This hook executes just before the query is executed against the database, enabling modifications to its parameters.

function book_query_archive_page($query) {
    if ( !is_admin() && $query->is_main_query() && $query->is_post_type_archive( 'book' ) ) {
        $query->set( 'posts_per_page', 20 );
    }
}
add_action( 'pre_get_posts', 'book_query_archive_page' );

3. Use Query Filter Hooks Instead of Direct Queries

There are cases where you can’t use WordPress API functions for your queries. That’s when it pays to stick to the WordPress way of doing things!

WordPress provides filters like posts_where, posts_join, and posts_clauses. These filters let you tweak SQL queries before they run.

For example, to change a query with posts_where, you:

  1. You write a function, say, query_current_year_posts.
  2. This function adds a condition to only get posts after January 1, 2023.
  3. Then, you connect this function to the posts_where filter.

Here’s how that looks like in action:

function query_current_year_posts($where) {
    $where .= " AND post_date > '2023-01-01'";
    return $where;
}
add_filter('posts_where', 'query_current_year_posts');

to find more about this type of filter hooks have a look at this code from WordPress’s GitHub repository .

Side Note: always secure SQL queries

Always make sure your SQL queries are safe from attacks, like SQL injection by using $wpdb->prepare along with other escape functions, like esc_sql and esc_like. Now, let’s say you have a custom table named wp_custom_table with columns id, name, and age. Here’s how you can construct a safe SQL query using $wpdb->prepare():

global $wpdb;
$table_name = $wpdb->prefix . 'custom_table';
$name = $_POST['name']; // Assuming this is user input

// Escaping user input using esc_sql()
$safe_name = esc_sql($name);

// Using $wpdb->prepare() to handle placeholders safely
$query = $wpdb->prepare("SELECT * FROM $table_name WHERE name = %s", $safe_name);

// Executing the query
$results = $wpdb->get_results($query);

This way, you keep your queries safe and reliable.

4. Caching query results

Caching query results in WordPress can improve performance by storing the results of frequently executed queries or resource-intensive ones.

function home_cached_best_sellers() {
    $cache_key = 'home_best_sellers_query_results';
    $cached_results = get_transient($cache_key);
    
    if (false === $cached_results) {
        $args = array(
            // Query arguments
        );
        
        $query_results = new WP_Query($args);
        
        // Cache the results for 1 hour
        set_transient($cache_key, $query_results, HOUR_IN_SECONDS);
        
        return $query_results;
    } else {
        return $cached_results;
    }
}

// Use the custom function to get query results
$home_best_sellers = home_cached_best_sellers();
// Use $ home_best_sellers as needed

The home_cached_best_sellers function effectively leverages caching to minimize the impact of resource-intensive queries. By storing the results of the top-selling products query, the function ensures that subsequent requests can be served swiftly without repeatedly executing the demanding query. This approach significantly enhances performance and reduces server load, particularly for frequently visited pages that display the best-selling products. The function’s caching mechanism demonstrates how WordPress websites can optimize resource utilization and maintain responsiveness even amidst demanding workloads.

Optimizing Queries: Meta Queries Performance

Navigating meta queries frequently presents challenges. The upcoming sections will propose proven solutions to enhance the performance of interacting with meta queries.

1. Leveraging Taxonomy Terms instead of Meta keys

One effective way to avoid slow searches in meta values is by substituting them with taxonomy terms.

Consider a scenario where a meta_value query filters posts based on a visitor’s membership level, such as ‘Premium’. Rather than relying solely on meta_value for this purpose, employing a custom taxonomy with individual terms assigned to various membership levels can significantly enhance query efficiency by capitalizing on indexes.

Let’s examine the transformation of a meta query into a taxonomy query for improved performance:

Original query:

$args = array(
    'post_type' => 'post',
    'meta_key' => 'membership_level',
    'meta_value' => 'Premium'
);
$query = new WP_Query( $args );

Transformed taxonomy query:

$args = array(
    'post_type' => 'post',
    'tax_query' => array(
        array(
            'taxonomy' => 'membership_levels',
            'field' => 'slug',
            'terms' => 'premium'
        )
    )
);
$query = new WP_Query( $args );

This approach, adaptable to numerous use cases, holds the promise of sidestepping performance issues commonly associated with meta queries.

2. Binary situations and checking for existence of meta_key

When dealing with binary situations like meta values set as true or false, it’s crucial to optimize performance. Take the scenario where a meta value, like hide_on_homepage acts as a yes or no indicator (‘true’ or ‘false’). MySQL usually scans every row with the hide_on_homepage meta_key to check for the ‘true’ meta value. This exhaustive search slows down your performance considerably.

Original meta query:

$args = array(
    'post_type' => 'post',
    'meta_key' => 'hide_on_homepage',
    'meta_value' => 'true'
);
$query = new WP_Query( $args );

A smarter approach involves redefining the method:

Directly associate the presence or absence of the hide_on_homepage meta key with action you want, like whether a post should be hidden on the homepage. To achieve this, remove the hide_on_homepage meta key for posts that shouldn’t be hidden on the homepage. Using the meta key index in this way can lead to substantial performance improvement.

Query using meta key presence:

$args = array(
    'post_type' => 'post',
    'meta_query' => array(
        'relation' => 'OR',
        array(
            'key' => 'hide_on_homepage',
            'compare' => 'NOT EXISTS' // Find posts not hidden
        ),
        array(
            'key' => 'hide_on_homepage',
            'compare' => 'EXISTS' // Find hidden posts
        )
    )
);
$query = new WP_Query( $args );

Optimizing Queries: Taxonomy Queries Performance

When utilizing tax_query to manage terms in WP_Query, it includes child terms in taxonomy queries by default. This seriously slows down your query speed. To improve performance, you should add 'include_children' => false to most taxonomy queries. That’s especially useful if your queries used to run quickly but now take too long and might time out.

Instead of querying both parent and child terms, it’s more efficient to query only the parent term. Then, using a save_post hook, ensure that when a child term is added, its parent term is also included automatically.

// Hook to handle post saves
function add_parent_term($post_id) {
    $post = get_post($post_id);

    if ($post && $post->post_type === 'book') {
        $terms = wp_get_post_terms($post_id, 'book_genres', array('fields' => 'all'));

        foreach ($terms as $term) {
            if ($term->parent !== 0) {
                $parent_term = get_term_by('id', $term->parent, 'book_genres');
                wp_set_post_terms($post_id, $parent_term->term_id, 'book_genres', true);
            }
        }
    }
}
add_action('save_post', 'add_parent_term');

Optimizing Queries: The Use of post__not_in in WP_Query

WordPress developers often use the post__not_in parameter in the WP_Query class to exclude specific post IDs from query results. However, while this can be a convenient solution, it’s crucial to consider its impact on performance, especially on large or busy sites where it may negatively affect the cache hit rate. Imagine you’re managing an e-commerce website featuring a product recommendation system. To enhance user experience, you want to suggest related products on a product detail page while excluding the currently viewed product from the recommendations. Let’s explore how to achieve this with optimal performance.

Initial Implementation:

function display_related_products() {
    $current_product_id = get_current_product_id();
    $product_categories = get_product_categories($current_product_id);

    if ($product_categories) {
        $category_ids = array();
        foreach ($product_categories as $category) {
            $category_ids[] = $category->term_id;
        }

        // Set up query arguments to get related products
        $args = array(
            'category__in' => $category_ids,
            'posts_per_page' => 5,
            'post__not_in' => array($current_post_id),
        );

        $related_products_query = new WP_Query($args);

        // Display the related products
        while ($related_products_query->have_posts()) {
            // HTML markup for displaying related products goes here
        }

        wp_reset_postdata();
    }
}

The initial implementation presents a notable issue. The post__not_in parameter introduces a distinctive query for each product, as it appends an AND ID not in ('123') clause to the SQL executed against the database. This modification obstructs the built-in query cache mechanism. Consequently, it leads to a cache miss, affecting performance as the database redundantly performs identical work for each product.

Improved query by enhancing cache hit rate:

function display_related_products() {
    $current_product_id = get_current_product_id();
    $product_categories = get_product_categories($current_product_id);

    if ($product_categories) {
        $category_ids = array();
        foreach ($product_categories as $category) {
            $category_ids[] = $category->term_id;
        }

        // Set up query arguments to get related products
        $args = array(
            'category__in' => $category_ids,
            'posts_per_page' => 5,
        );

        $related_products_query = new WP_Query($args);

        // Display the related products
        $products = 0;
        while ($related_products_query->have_posts() && $products < 5) {
            $current = get_current_product_id();
            if (!in_array($current, $exclude)) {
                $products++;
                // HTML markup goes here
            }
        }

        wp_reset_postdata();
    }
}

This revised function requests more posts and skips excluded posts in PHP, ensuring a consistent query across all URLs. By avoiding unnecessary cache variations, this approach significantly improves the cache hit rate, contributing to better site scalability and stability.

What’s Next?

That’s it for our guide on WordPress queries. We’ve covered everything from using WP_Query and get_posts to optimizing your queries for better performance.

In the next installment of this series, we’ll explore another vital part of WordPress performance optimization: Database Indexing, so keep an eye out for our next blog post. We’ll continue exploring ways to enhance your WordPress site. If you still have questions or want to dive deeper, leave a comment or connect with us on LinkedIn! Or, if you’re a developer looking for that perfect job, check out our open positions.

Looking to grow your media business?

Get in touch and tell us about your project!

Get in Touch
LET’S WORK TOGETHER

Sound smarter in meetings.

Weekly media tech news in easy-to-read chunks.