不要使用TRANSIENT的常规方式来缓存WP_QUERY

本文探讨了在WordPress中缓存复杂查询(尤其是涉及元数据的查询)时可能遇到的问题,并提出了解决方案。介绍了为什么不应直接缓存WP_Query对象,而是推荐缓存HTML输出或仅缓存查询得到的ID列表。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

WP_Query is one of the most complex classes in the WordPress codebase. It’s extremely powerful and flexible, but that flexibility often results in slower database queries, especially when working with metadata. To speed things up, WordPress developers tend to cache the results of such queries, but there are a few pitfalls you should be aware of.

Caching Queries

The Transients API is likely the top choice for caching WP_Query results, but what we often see is developers storing the whole WP_Query object in a transient, and we believe that’s a mistake. Translating that into code, it looks something like this:

$cache_key = 'my-expensive-query';
if ( ! $query = get_transient( $cache_key ) ) {
    $query = new WP_Query( ... ); // some expensive query;
    set_transient( $cache_key, $query, 24 * HOUR_IN_SECONDS );
}

while ( $query->have_posts() ) {
    $query->the_post();
    // ...
}

Here you can see that we’re passing the $query object directly to set_transient(), so whenever we get a cache hit, we’ll have our query object available, along with all the useful WP_Query properties and methods.

This is a bad idea, and while this works (or at least seems to work), you’ll want to know what’s happening behind the scenes when you call set_transient() in this particular case.

Serialize/Unseriazile

By default, transients in WordPress translate into the Options API. If you’re familiar with how options work internally, you’ll know that the values are serialized before hitting the database, and unseriaziled when retrieved. This is also true for most persistent object caching dropins, including Memcached and Redis.

As an example, just look at what happens when we serialize a small object in PHP:

$object = new stdClass;
$object->foo = 1;
$object->bar = 2;

var_dump( maybe_serialize( $object ) );
// string(47) "O:8:"stdClass":2:{s:3:"foo";i:1;s:3:"bar";i:2;}"

This allows us to store the object, along with all its properties, as a string, which works well in a MySQL table, in a Redis database, etc. When deserializing (or unserializing) such a string, the result is an identical copy of the object we previously had. This is great, but let’s consider a more complex object:

class A {}
class B {
    public $baz = 3;
}
class C {
    public $qux = 4;
}

$object = new A;
$object->foo = new B;
$object->bar = new C;

var_dump( maybe_serialize( $object ) );
// string(84) "O:1:"A":2:{s:3:"foo";O:1:"B":1:{s:3:"baz";i:3;}s:3:"bar";O:1:"C":1:{s:3:"qux";i:4;}}"

This illustrates that PHP’s serialize() function will recursively serialize any object referenced by a property of another object.

Serializing WP_Query

Let’s try and put this in a WP_Query context by running a simple query and serializing it:

$query = new WP_Query( array(
    'post_type' => 'post',
    'post_status' => 'publish',
    'posts_per_page' => 10,
) );

var_dump( maybe_serialize( $query ) );
// string(22183) "O:8:"WP_Query":50:{s:5:"query";a:3:{s:9:"post_type";s:4:"post";s:11:"post_status";s:7:"publish";s:14:"posts_per_page";i:10;}s:10:"query_vars";a:65:{s:9:"post_type";s:4:"post";s:11:"post_status";s:7:"publish";s:14:"posts_per_page"; ... (about 22000 more characters)

The first thing you’ll notice is that the output is extremely long. Indeed, we’re serializing every property of our WP_Query object, including all query variables, parsed query variables, the loop status and current position, all conditional states, a bunch of WP_Post objects we retrieved, as well as any additional referenced objects.

Referenced objects? Let’s take a look at the WP_Query constructor:

public function __construct( $query = '' ) {
    $this->db = $GLOBALS['wpdb'];
    // ...

Now let’s take a closer look at our gigantic serialized string:

... s:5:"*db";O:4:"wpdb":62:{s:11:"show_errors";b:1;s:15:"suppress_errors";b:0; ...

Whoops! But that’s not all. That wpdb object we’re storing as a string in our database will contain our database credentials, all other database settings, as well as the full list of SQL queries along with their timing and stacktraces if SAVEQUERIES was turned on.

The same is true for other referenced objects, such as WP_Meta_Query, WP_Tax_Query, WP_Date_Query, etc. Our goal was to speed that query up, and while we did, we introduced a lot of unnecessary overhead serializing and deserializing complex objects, as well as leaked potentially sensitive information.

But the overhead does not stop there.

Metadata, Terms, Posts & the Object Cache

Okay so now we have a huge serialized string containing the posts that we wanted to cache, along with a bunch of unnecessary data. What happens when we deserialize that string back to a WP_Query object? Well, nothing really…

When deserializing strings into objects, PHP does not run the constructor method (thankfully), but instead runs __wakeup() if it exists. It doesn’t exist in WP_Query, so that’s what happens — nothing, except of course populating all our properties with all those values from the serialized string, restoring nested objects, and objects nested inside those objects. It should be pretty fast, hopefully much faster than running our initial SQL query.

And after we’re done deserializing, even though at that point the WP_Query object is a bit crippled (serialize can’t store resource types, such as mysqli objects), we can still use it:

while ( $query->have_posts() ) {
    $query->the_post();
    the_title();
}

Which doesn’t cause any additional queries against the wp_posts table, since we already have all the necessary data in the $query->posts array. Until we do something like this:

while ( $query->have_posts() ) {
    $query->the_post();
    the_title();

    get_post_meta( get_the_ID(), 'key', true );
}

And this is where things go south.

The Object Cache

When running a regular WP_Query, the whole process (by default) takes care of retrieving the metadata and terms data for all the posts that match our query, and storing all that in the object cache for the request. That happens in the get_posts() method of our object (_prime_post_caches()). But when re-creating the WP_Query object from a string, the method never runs, and so our term and meta caches are never primed.

For that reason, when running get_post_meta() inside our loop, we’ll see a separate SQL query to fetch the metadata for that particular post. And this happens for every post. Separately. Which means that for 10 “cached” posts, we’re looking at 10 additional queries. Sure, they’re pretty fast, but still.

Now let’s add something like the_tags() to the same loop, and voila! We have another ten SQL queries to grab the terms now.

And finally… This is the best part. Let’s add something often done by a typical plugin that alters the post content or title in any way:

add_filter( 'the_title', function( $title ) {
    $post = get_post( get_the_ID() );
    // do something with $post->post_title and $title
    return $title;
} );

Now we’ll see an additional ten database queries for the posts. How did that happen? Didn’t we have those posts cached?

Yes we did, but we had them in our $query->posts array, and get_post() doesn’t know or care about any queries, it simply fetches data from the WordPress object cache, and it was WP_Query‘s job to prime those caches with the data, which it failed to do upon deserializing. Tough luck.

So ultimately, by caching our WP_Query object in a transient, we went from four database queries (found rows, posts, metadata and terms) to only two (transient timeout and transient value) and an additional thirty queries (posts_per_page * 3) if we want to use metadata, terms or anything that calls get_post().

To be fair, those thirty queries are likely much faster than our initial posts query because they’re lookups by primary key, but each one is still a round-trip to the (possibly remote) MySQL server. Sure, you can probably hack your way around it with _prime_post_caches(), but we don’t recommend that.

The Alternatives

Now that we have covered why you shouldn’t cache WP_Query objects, let’s look at a couple of better ways to cache those slow lookups.

The first, easiest and probably best method is to cache the complete HTML output, and PHP’s output buffering functions will help us implement that without moving too much code around:

$cache_key = 'my-expensive-query';
if ( ! $html = get_transient( $cache_key ) ) {
    $query = new WP_Query( ... );
    ob_start();
    while ( $query->have_posts() ) {
        $query->the_post();
        // output all the things
    }
    $html = ob_get_clean();
    set_transient( $cache_key, $html, 24 * HOUR_IN_SECONDS );
}

echo $html;

This way we’re only storing the actual output in our transient, no posts, no metadata, no terms, and most importantly no database passwords. Just the HTML.

If your HTML string is very (very!) long, you may also consider compressing it with gzcompress() and storing it as a base64 encoded string in your database, which is especially efficient if you’re working with memory-based storage, such as Redis or Memcached. The compute overhead to compress/uncompress is very close to zero.

The second method is to cache post IDs from the expensive query, and later perform lookups by those cached IDs which will be extremely fast. Here’s a simple snippet to illustrate the point:

$cache_key = 'my-expensive-query';
if ( ! $ids = get_transient( $cache_key ) ) {
    $query = new WP_Query( array(
        'fields' => 'ids',
        // ...
    ) );

    $ids = $query->posts;
    set_transient( $cache_key, $ids, 24 * HOUR_IN_SECONDS );
}

$query = new WP_Query( array(
    'post__in' => $ids,
) );

// while ( $query->have_posts() ) ...

Here we have two queries. The first query is the slow one, where we can fetch posts by meta values, etc. Note that we ask WP_Query to retrieve IDs only for that query, and later do a very fast lookup using the post__in argument. The expensive query runs only if we don’t already have an array of IDs in our transient.

This method is a bit less efficient than caching the entire HTML output, since we’re (probably) still querying the database. But the flexibility is sometimes necessary, especially when you’d like to cache the query for much longer, but have other unrelated things that may impact your output, such as a shortcode inside the post content.

Profile

Caching is a great way to speed things up, but you have to know exactly what you’re caching, when, where and how, otherwise you risk facing unexpected consequences. If you’re uncertain whether something is working as intended, always turn to profiling — look at each query against the database, look at all PHP function calls, watch for timing and memory usage.

<?php /* Plugin Name: 多功能 WordPress 插件 Plugin URI: https://yourwebsite.com/plugins/multifunctional Description: 包含置顶、网页宠物、哀悼模式、禁止复制、弹幕等 20+ 功能的综合插件 Version: 1.0.0 Author: Your Name Author URI: https://yourwebsite.com License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html Text Domain: multifunctional-plugin Domain Path: /languages */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('MULTI_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('MULTI_PLUGIN_URL', plugin_dir_url(__FILE__)); define('MULTI_PLUGIN_VERSION', '1.0.0'); // 全局设置存储键 $multi_plugin_options = array( 'mourning_date', 'background_image', 'announcement', 'marquee_content', 'watermark_text' ); // ------------------------------ // 1. 置顶功能 // ------------------------------ function multi_post_sticky_meta_box() { add_meta_box( 'post_sticky', '文章置顶', 'multi_post_sticky_callback', 'post', 'side', 'default' ); } add_action('add_meta_boxes', 'multi_post_sticky_meta_box'); function multi_post_sticky_callback($post) { $sticky = get_post_meta($post->ID, '_post_sticky', true); wp_nonce_field('post_sticky_nonce', 'post_sticky_nonce'); echo '<label><input type="checkbox" name="post_sticky" value="1" ' . checked(1, $sticky, false) . '> 置顶此文章</label>'; } function multi_post_sticky_save($post_id) { if (!isset($_POST['post_sticky_nonce']) || !wp_verify_nonce($_POST['post_sticky_nonce'], 'post_sticky_nonce')) { return; } if (isset($_POST['post_sticky'])) { update_post_meta($post_id, '_post_sticky', 1); } else { delete_post_meta($post_id, '_post_sticky'); } } add_action('save_post', 'multi_post_sticky_save'); // ------------------------------ // 2. 网页宠物 // ------------------------------ function multi_web_pet() { echo '<div id="web-pet" style="position:fixed;bottom:20px;right:20px;z-index:9999;">'; echo '<img src="' . MULTI_PLUGIN_URL . 'assets/pet.png" alt="网页宠物" width="80">'; echo '</div>'; } add_action('wp_footer', 'multi_web_pet'); // ------------------------------ // 3. 哀悼模式 // ------------------------------ function multi_mourning_mode() { $mourning_date = get_option('multi_mourning_date', '2025-04-29'); // 默认日期 if (date('Y-m-d') === $mourning_date) { echo '<style>html { filter: grayscale(100%); }</style>'; } } add_action('wp_head', 'multi_mourning_mode'); // ------------------------------ // 4. 禁止复制 & 查看源码 // ------------------------------ function multi_disable_copy_source() { // 禁止复制样式 echo '<style>body { user-select: none; -moz-user-select: none; -webkit-user-select: none; }</style>'; // 禁止查看源码脚本 echo '<script>document.addEventListener("keydown", function(e) { if ((e.ctrlKey && e.key === "u") || e.key === "F12" || e.keyCode === 123) { e.preventDefault(); } });</script>'; } add_action('wp_head', 'multi_disable_copy_source'); // ------------------------------ // 5. 弹幕功能 // ------------------------------ function multi_danmaku() { echo '<div id="danmaku-container"></div>'; echo '<script src="' . MULTI_PLUGIN_URL . 'assets/danmaku.js"></script>'; // 需自行添加弹幕逻辑脚本 } add_action('wp_footer', 'multi_danmaku'); // ------------------------------ // 6. WP 优化 // ------------------------------ function multi_wp_optimization() { // 移除冗余功能 remove_action('wp_head', 'rsd_link'); remove_action('wp_head', 'wlwmanifest_link'); remove_action('wp_head', 'wp_generator'); remove_action('wp_head', 'feed_links', 2); remove_action('wp_print_styles', 'print_emoji_styles'); } add_action('init', 'multi_wp_optimization'); // ------------------------------ // 7. 媒体分类 // ------------------------------ function multi_media_category() { register_taxonomy( 'media_category', 'attachment', array( 'label' => __('媒体分类', 'multifunctional-plugin'), 'hierarchical' => true, 'show_ui' => true, 'query_var' => true ) ); } add_action('init', 'multi_media_category'); // ------------------------------ // 8. 预加载首页 // ------------------------------ function multi_preload_homepage() { echo '<link rel="preload" href="' . home_url() . '" as="document">'; } add_action('wp_head', 'multi_preload_homepage'); // ------------------------------ // 9. 在线客服 & 手机客服 // ------------------------------ function multi_support_buttons() { // 通用客服按钮 echo '<div id="support-button" class="desktop-only">'; echo '<a href="https://your客服链接.com" target="_blank">在线客服</a>'; echo '</div>'; // 手机端专属客服按钮 echo '<div id="mobile-support" class="mobile-only">'; echo '<a href="tel:1234567890">手机客服</a>'; echo '</div>'; } add_action('wp_footer', 'multi_support_buttons'); // ------------------------------ // 10. 网站背景 & 公告 // ------------------------------ function multi_background_announcement() { // 背景图片 $bg_image = get_option('multi_background_image', MULTI_PLUGIN_URL . 'assets/bg.jpg'); echo '<style>body { background-image: url("' . esc_url($bg_image) . '"); }</style>'; // 公告栏 $announcement = get_option('multi_announcement', '欢迎访问我们的网站!'); echo '<div class="site-announcement">' . esc_html($announcement) . '</div>'; } add_action('wp_head', 'multi_background_announcement'); // ------------------------------ // 11. 水印功能 // ------------------------------ function multi_watermark() { $watermark = get_option('multi_watermark_text', '版权所有 © 你的网站'); echo '<style> body::after { content: "' . esc_attr($watermark) . '"; position: fixed; top: 50%; left: 50%; transform: rotate(-45deg) translate(-50%, -50%); opacity: 0.1; font-size: 80px; color: #000; pointer-events: none; } </style>'; } add_action('wp_head', 'multi_watermark'); // ------------------------------ // 12. 后台设置页面 // ------------------------------ function multi_plugin_settings_page() { add_options_page( '多功能插件设置', '多功能插件', 'manage_options', 'multi-plugin-settings', 'multi_settings_html' ); } add_action('admin_menu', 'multi_plugin_settings_page'); function multi_settings_html() { if (!current_user_can('manage_options')) { wp_die(__('你没有权限访问此页面。')); } if (isset($_POST['multi_plugin_save'])) { foreach ($multi_plugin_options as $option) { update_option('multi_' . $option, sanitize_text_field($_POST[$option])); } echo '<div class="updated"><p>设置已保存!</p></div>'; } $options = array(); foreach ($multi_plugin_options as $option) { $options[$option] = get_option('multi_' . $option, ''); } ?> <div class="wrap"> <h1>多功能插件设置</h1> <form method="post"> <table class="form-table"> <tr> <th>哀悼日期 (YYYY-MM-DD)</th> <td><input type="text" name="mourning_date" value="<?php echo esc_attr($options['mourning_date']); ?>"></td> </tr> <tr> <th>背景图片 URL</th> <td><input type="url" name="background_image" value="<?php echo esc_attr($options['background_image']); ?>"></td> </tr> <tr> <th>公告内容</th> <td><input type="text" name="announcement" value="<?php echo esc_attr($options['announcement']); ?>"></td> </tr> <tr> <th>跑马灯内容</th> <td><input type="text" name="marquee_content" value="<?php echo esc_attr($options['marquee_content']); ?>"></td> </tr> <tr> <th>水印文本</th> <td><input type="text" name="watermark_text" value="<?php echo esc_attr($options['watermark_text']); ?>"></td> </tr> </table> <p class="submit"> <input type="submit" name="multi_plugin_save" class="button button-primary" value="保存设置"> </p> </form> </div> <?php } // ------------------------------ // 插件激活时创建默认设置 // ------------------------------ function multi_plugin_activate() { foreach ($multi_plugin_options as $option) { $default = ($option === 'mourning_date') ? '2025-04-29' : ''; add_option('multi_' . $option, $default); } } register_activation_hook(__FILE__, 'multi_plugin_activate'); // ------------------------------ // 资源路径说明(需手动创建目录) // ------------------------------ /* 请在插件目录下创建以下文件夹和文件: - assets/ - pet.png (网页宠物图片) - bg.jpg (默认背景图片) - danmaku.js (弹幕逻辑脚本) - style.css (自定义样式) */
最新发布
05-01
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值