Had a client come to me with a WooCommerce store that was starting to choke. They had about 30,000 products, and their admin dashboard, specifically the Orders screen, was timing out. Their team couldn’t filter orders by product anymore, which was a huge workflow problem. Total mess. The site’s growth had completely broken their ability to manage it, and the culprit was poor WordPress query performance under the hood.
My first thought, the knee-jerk reaction, was to hook into pre_get_posts and try to force a more efficient query. Maybe add some meta keys, optimize the arguments. And yeah, you can sometimes get a little boost that way. But for this scale, it’s like putting a band-aid on a broken leg. You’re still forcing WordPress to do some heavy lifting with complex JOINs on the massive postmeta table. It never really solves the root problem. You just create a brittle, complex piece of code that will break the next time another plugin touches the admin screen.
The Real Fix for WooCommerce Query Performance
The real issue is that WooCommerce isn’t designed out-of-the-box for this kind of specific, high-volume filtering. The data just isn’t structured for it. So, you don’t optimize the query. You change the data structure so the query becomes dead simple. For this, we build a custom lookup table. Trust me on this, it’s the only way to go for serious stores. This reminds me of a post I read about reflecting on growth and hitting a plateau; sometimes the old way just stops working. You can read it at https://carlalexander.ca/2017-in-review/.
The idea is simple: create a super-efficient table with just two columns: order_id and product_id. Then, we populate it. When a new order comes in, we hook into the process and save a row for each product in that order.
/**
* Populate a custom lookup table when an order is processed.
*/
add_action( 'woocommerce_checkout_order_processed', function( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
foreach ( $order->get_items() as $item ) {
$product_id = $item->get_product_id();
// You'd have your own function to insert into your custom table
my_custom_add_order_product_to_lookup_table( $order_id, $product_id );
}
}, 10, 1 );
Here’s the kicker: with this in place, filtering is instantaneous. Instead of a messy WP_Query, you’re just doing a simple SQL query against your indexed lookup table to get the order_ids. The admin screen becomes fast again. Problem solved. For good.
So, What’s the Point?
The lesson here isn’t about a specific code snippet. It’s about recognizing when a system has reached its architectural limit. When a core feature gets slow, don’t just reach for a cache or a quick filter. That’s junior-level thinking. Step back and ask: “Is the data in the right shape for the question I’m asking?” More often than not, at scale, the answer is no. Restructuring the data is the real work of a senior developer.
Look, this stuff gets complicated fast. If you’re tired of debugging someone else’s mess and just want your site to work, drop my team a line. We’ve probably seen it before.
Leave a Reply