批量查询拖垮后台:我用三行代码把 2000 次 `get_post_meta` 压成 1 次,顺便给静态资源上了道"指纹锁"
上周帮客户排查一个后台加载 12 秒的问题,Profiler 一拉,2000 多个 post 的列表页,每个都单独走了 get_post_meta,SQL 日志刷出满屏的 SELECT meta_key...。这场景太经典了,记录一下我现在的"三板斧"。
第一斧:把 N+1 拍死在 WP_Query 里
很多人知道 update_postmeta_cache,但真写查询时还是忘。我的习惯是凡涉及列表,先强制预加载:
$query = new WP_Query( [
'post_type' => 'product',
'posts_per_page' => 50,
'meta_query' => [ /* ... */ ],
'update_postmeta_cache' => true, // 默认其实开了,但显式写上防接手的人乱改
'update_post_term_cache' => true,
'no_found_rows' => true, // 不需要分页总数时,省一次 COUNT(*)
] );
更隐蔽的坑是 get_posts() 默认 suppress_filters = true,有些缓存插件的预加载逻辑会被绕过去。我现在列表查询统一用 WP_Query,不再图省事。
第二斧:给元数据造个"热层"
有些字段是高频只读的,比如商品的 SKU、库存状态。我做了个轻量封装,比直接上对象缓存省点事:
class SKU_Cache {
private static $hot = []; // 请求级内存缓存
public static function get( $product_id ) {
if ( isset( self::$hot[ $product_id ] ) ) {
return self::$hot[ $product_id ];
}
$cached = wp_cache_get( "sku_{$product_id}", 'my_plugin' );
if ( false !== $cached ) {
self::$hot[ $product_id ] = $cached;
return $cached;
}
$sku = get_post_meta( $product_id, '_sku', true );
self::$hot[ $product_id ] = $sku;
wp_cache_set( "sku_{$product_id}", $sku, 'my_plugin', HOUR_IN_SECONDS );
return $sku;
}
}
三层结构:内存 > 对象缓存 > 数据库。请求内重复访问零开销,跨请求走 Redis/Memcached。注意 wp_cache_set 的 group 要自定义,别蹭 posts group,不然被其他插件 wp_cache_flush 误伤。
第三斧:静态资源的"版本指纹"别再用时间戳
以前图省事写 ?ver=,CDN 缓存直接失效。现在改拿文件内容的 hash 前 8 位:
function my_plugin_asset_url( $file ) {
$path = plugin_dir_path( __FILE__ ) . 'assets/' . $file;
$url = plugin_dir_url( __FILE__ ) . 'assets/' . $file;
if ( file_exists( $path ) ) {
$ver = substr( md5_file( $path ), 0, 8 );
return add_query_arg( 'ver', $ver, $url );
}
return $url; // fallback
}
内容不变 hash 不变,CDN 能长期缓存;内容一变 hash 变,浏览器自动拉新。配合 wp_enqueue_script 时:
wp_enqueue_script(
'my-admin-chart',
my_plugin_asset_url( 'js/chart.js' ),
[ 'jquery' ],
null, // ver 已经拼在 URL 里了,这里给 null 防重复
true
);
有个细节:如果用了 SCRIPT_DEBUG,我会把 hash 换成 filemtime,方便开发时刷新即时生效,生产环境切回 md5。
踩过的坑
1. md5_file 对大文件(比如 2MB 的 JS bundle)有点慢,我现在超过 500KB 的走 filemtime,或者构建时预生成 hash 写进 manifest.json。
2. 对象缓存没装时 wp_cache_get 永远返回 false,逻辑里别假设它一定命中。
3. update_postmeta_cache 只预加载 postmeta,自定义表的元数据得自己写 cache_posts_meta 类似的批量填充。
现在那个客户的后台从 12 秒降到 800ms,Profiler 里最慢的变成 WordPress 核心本身的翻译加载了——这我管不了,至少插件这块问心无愧。
你们处理过更狠的 N+1 场景吗?比如跨表 join 或者 get_users 连带用户元数据的情况,有没有更骚的预加载姿势?

