插件查询从 800ms 压到 12ms:我如何用"查询契约"把 WP_Query 的贪婪加载关进笼子里

插件开发 3 浏览 0 回复 返回上级

上周接了个需求,要在后台展示"用户最近互动过的 20 个带特定 meta 的帖子",顺手写了段熟悉的代码:

$posts = get_posts([
    'post_type'      => 'any',
    'posts_per_page' => 20,
    'meta_query'     => [
        [
            'key'   => '_user_interaction',
            'value' => $user_id,
        ],
    ],
    'orderby'        => 'modified',
]);

本地 200 条数据跑得飞快,上线后 8 万条帖子直接炸锅。 profiling 一看,SQL_CALC_FOUND_ROWS 全表扫,meta 表 JOIN 出笛卡尔积,更离谱的是 'post_type' => 'any' 把 revision、attachment 全拉进来了。

这不是 WP_Query 的锅,是我没给它立规矩。后来我给团队定了个"查询契约",核心就三条红线:

一、字段白名单:只许 SELECT 什么,不许碰什么

默认 get_posts 会 SELECT *,然后 post_content 里塞着 5MB 的 base64 图。现在强制加 'fields' => 'ids' 或自定义 WP_Queryposts_fields 钩子做投影裁剪:

add_filter( 'posts_fields', function( $fields, $query ) {
    if ( $query->get( '_my_plugin_minimal' ) ) {
        return "{$GLOBALS['wpdb']->posts}.ID, {$GLOBALS['wpdb']->posts}.post_title";
    }
    return $fields;
}, 10, 2 );

配合 'no_found_rows' => true 关掉计数,'update_post_meta_cache' => false'update_post_term_cache' => false 禁用贪婪缓存预热。这三行组合拳,查询时间直接从 800ms 掉到 45ms。

二、meta 查询的"索引友好"改造

上面那个 meta_query 是 post_id + meta_key + meta_value 的组合,但 MySQL 只会用 meta_key 单列索引,value 上的 LIKE 或范围查询照样全表。我的解法是把"高频筛选维度"反范化到主表自定义列,或者建影子表。

这次选了轻量方案:给 wp_posts 加了个 last_interactor 列,用 wp_insert_postpost_modified 钩子同步维护。查询变成:

$posts = get_posts([
    'post_type'      => 'my_custom_type',  // 明确类型,拒绝 any
    'posts_per_page' => 20,
    'orderby'        => 'modified',
    'date_query'     => [ 'after' => '-30 days' ], // 额外时间窗口剪枝
    'meta_key'       => 'last_interactor', // 现在走覆盖索引
    'meta_value'     => $user_id,
    'no_found_rows'  => true,
    'fields'         => 'ids',
]);

但这里埋了个坑:meta_key + meta_value 的查询在 5.7 以下不会用覆盖索引,因为 meta_value 是 LONGTEXT。升级 8.0 或者改表结构,二选一。

三、结果缓存的"分层策略",不是无脑 transients

之前团队爱用 set_transient 一锤子缓存,结果用户 A 看了缓存,用户 B 的权限过滤没生效。现在拆三层:

1. 原始数据层(对象缓存):上面 get_posts 的结果用 wp_cache_set( "user_feed:{$user_id}", $ids, 'my_plugin', 300 ),TTL 5 分钟。关键:存的是 ID 数组,不是完整对象。

2. 渲染层缓存(页面片段):用 wp_cache_add 做 memoization,同一次请求内重复调用直接返回。

3. 静态资源指纹(长期不变):CSS/JS 用 filemtime 做版本号,配合 wp_enqueue_script$ver 参数,CDN 缓存一年不带怕的。

wp_enqueue_style(
    'my-plugin-admin',
    plugins_url( 'assets/admin.css', __FILE__ ),
    [],
    filemtime( plugin_dir_path( __FILE__ ) . 'assets/admin.css' ) // 内容变,指纹变
);

静态资源加载的"懒到最后一刻"原则

后台插件最容易犯的错:每个页面都加载全量 JS。现在我的加载器长这样:

add_action( 'admin_enqueue_scripts', function( $hook ) {
    $manifest = [
        'toplevel_page_my_plugin' => ['dashboard.js', 'chart.js'],
        'my-plugin_page_settings' => ['settings.js'],
    ];
    
    if ( ! isset( $manifest[ $hook ] ) ) {
        return; // 不匹配?一个字节都不加载
    }
    
    foreach ( $manifest[ $hook ] as $file ) {
        wp_enqueue_script(
            "my-plugin-{$file}",
            plugins_url( "assets/{$file}", __FILE__ ),
            [ 'wp-api', 'wp-element' ], // 只声明真实依赖
            filemtime( plugin_dir_path( __FILE__ ) . "assets/{$file}" ),
            true // 丢 footer
        );
    }
} );

chart.js 从首屏砍掉,只在 dashboard 页出现。settings 页少加载 180KB。

最后压到 12ms 的那一下

对象缓存上了 Redis 之后,发现 wp_cache_get 也有序列化开销。ID 数组改成逗号分隔字符串存,get 之后 explode 解析,比 serialize/unserialize 快 3 倍——这属于 PHP 层面的 micro-optimization,但累积起来就是从 45ms 到 12ms 的最后一脚。

你们插件里有哪种"看起来正常、跑起来要命"的查询模式?贴出来一起拆。

评论0
回复 · 0
还没有回复
微信客服 微信客服