Before adding a caching plugin to hold one expensive query, WordPress already has the API: set_transient() and get_transient(), with a time to live and no configuration. It uses the object cache when one is installed and falls back to the options table when one is not, so the calling code is the same on a shared host and on a box with Memcached.
function td_top_sellers() {
global $wpdb;
$ids = get_transient( 'td_top_sellers' );
if ( false === $ids ) {
$ids = $wpdb->get_col(
"SELECT p.ID FROM {$wpdb->posts} p
JOIN {$wpdb->postmeta} m ON m.post_id = p.ID AND m.meta_key = 'total_sales'
WHERE p.post_type = 'product' AND p.post_status = 'publish'
ORDER BY m.meta_value + 0 DESC
LIMIT 8"
);
set_transient( 'td_top_sellers', $ids, 6 * HOUR_IN_SECONDS );
}
return $ids;
}
A transient with an expiry is stored as two non-autoloaded rows; a transient with an expiry of 0 is stored autoloaded and never garbage collected, which is how an options table ends up with thousands of rows nobody remembers writing. The strict false === check matters because a cached empty array and a cache miss are otherwise indistinguishable, and the query then runs on every request while appearing to be cached. The larger caveat is that with a persistent object cache installed the value never reaches the database at all and the expiry becomes advisory — the backend may evict it a minute after it was written, so the miss branch has to be correct rather than merely present.