Get a list of WooCommerce sale products

WooCommerce tips and tricks

You can get lists of all simple products, variable products, or featured products easily with WooCommerce. It’s also easy to get things like “in-stock” products or “out of stock” products. However, getting a list of WooCommerce sale products can be a bit tougher because sale products use different meta keys to determine sale status depending on the product type.

Let’s take a look at a couple of ways you can get sale products.

Check sale status

First of all, this post will tell you how to get all sale products. If you’re only interested in checking for sale status with one product, there’s a very simple way to do that:

global $product;
if ( $product->is_on_sale() ) {
    do_something();
}

We’re talking about querying all sale products instead.

Display all sale products

First of all, many developers want to query sale products so they can display them in a custom template or archive page.

If you want to display all sale products in a “deals” page or something and you want to use the WooCommerce loop to display them, you can easily do so via the sale products shortcode. No complex queries or additional code necessary 🙂 :

[[sale_products columns="3" per_page="12"]]

This lets WooCommerce do the heavy lifting for you, so use this if you’re just looking to output an archive of sale products:

On Sale Page

Querying WooCommerce sale products

You can use WP_Query to get your sale products, but how you do this matters. The default recommendation is typically to use the _sale_price meta to key to determine which products are on sale.

The issue with this is that variable products don’t use this key, so it would only return simple products. No problem, right? Let’s just add the meta key that variable products use as well: _min_variation_sale_price. We can then query both of these keys with an OR relationship to get products with either meta key.

$args = array(
    'post_type'      => 'product',
    'posts_per_page' => 8,
    'meta_query'     => array(
        'relation' => 'OR',
        array( // Simple products type
            'key'           => '_sale_price',
            'value'         => 0,
            'compare'       => '>',
            'type'          => 'numeric'
        ),
        array( // Variable products type
            'key'           => '_min_variation_sale_price',
            'value'         => 0,
            'compare'       => '>',
            'type'          => 'numeric'
        )
    )
);

$loop = new WP_Query( $args );

This totally works. You can now do whatever you’re trying to do with sale products that’s not simply outputting them onto the page. That doesn’t mean it’s the best way to get your sale products.

Let’s return to that shortcode we mentioned before to see how WooCommerce gets sale products.

Whoa, they’re doing something different. This uses the wc_get_product_ids_on_sale() function, which returns an array containing the IDs of the products that are on sale. This lets them use a simple query to get the sale products:

$query_args = array(
    'posts_per_page'    => 8,
    'no_found_rows'     => 1,
    'post_status'       => 'publish',
    'post_type'         => 'product',
    'meta_query'        => WC()->query->get_meta_query(),
    'post__in'          => array_merge( array( 0 ), wc_get_product_ids_on_sale() )
);
$products = new WP_Query( $query_args );

This is a more efficient way to query WooCommerce sale products.

So why is this better?

  • it’s easier for SQL to run the single post__in query compared to the more complicated query for meta keys
  • this method uses a separate query to get the product IDs on sale, and that can be parsed and optimized (related to next point)
  • querying for post__in when we already have the IDs is much, much faster because the wc_get_product_ids_on_sale function uses a transient, meaning this query is skipped if the transient exists

The moral of the story is that WooCommerce (and WordPress) optimize a lot for you, so before building your own query, take a look at how WooCommerce core handles this to see if there are similar use cases, or if there’s a function that can do part of the query for you.