搜索全站
主题 17 分钟阅读

WP_Query 与主查询:为什么不能用 query_posts

query_posts 会破坏主查询导致分页错乱与性能下降。讲清主查询与次查询的边界、pre_get_posts 的正确用法、WP_Query 常用参数与 wp_reset_postdata 的时机。

U
UFO_PF 2023-03-01 · 阅读 17 分钟
WP 主题
阅读提示
本文约 6,427 字,预计阅读 17 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:用了 query_posts 后,分页翻到第 2 页是重复内容

首页只想显示”推荐”分类的 5 篇文章,搜索到一段代码 query_posts( 'cat=3&posts_per_page=5' ),粘进 index.php 生效了。但点”下一页”到第 2 页,内容和第 1 页一模一样;侧边栏的”最新文章”也开始显示奇怪的结果;页脚的”上一篇/下一篇”指向了错误的文章。

这不是分页插件的问题,而是 query_posts() 把 WordPress 的主查询对象整个覆盖了。

原理分析:主查询只有一个,而且很早就在跑

WordPress 处理请求的顺序是:

① wp-settings.php 加载
② wp() → WP::main() → WP::parse_request() 解析 URL 得到 query_vars
③ $wp_the_query = new WP_Query()  ← 主查询对象,跑一次 SQL
④ $wp_query = $wp_the_query        ← 全局别名
⑤ 条件标签 is_home() / is_category() 等全部由它决定
⑥ template-loader.php 按条件标签选模板
⑦ 模板里 have_posts() / the_post() 遍历的就是它

query_posts() 的源码第一行就是:

function query_posts( $args ) {
    $GLOBALS['wp_query'] = new WP_Query( $args );   // 直接覆盖全局对象
    return $GLOBALS['wp_the_query'] === $GLOBALS['wp_query'] ? false : $GLOBALS['wp_query']->posts;
}

它带来的问题:

问题 后果
覆盖 $wp_query 条件标签全部失真,is_home() 判断出错
丢弃已执行的查询 白跑一条 SQL,页面多一次数据库往返
分页参数丢失 paged 没传进去,第 2 页还是第 1 页内容
污染全局状态 后续的侧边栏小组件、导航函数读到错误数据
wp_reset_query() 才能恢复 很多代码忘了调,污染持续到页脚

正确的做法是分清两种场景:

场景 该用什么
想改变”当前页面本来就查的内容” pre_get_posts 过滤器改主查询
想在页面里额外查一批内容(侧边栏、相关推荐) new WP_Query() 次查询

保姆级实操

第 1 步:改主查询用 pre_get_posts

add_action( 'pre_get_posts', function ( $query ) {
    // 三个必须判断的条件,缺一不可
    if ( is_admin() || ! $query->is_main_query() ) {
        return;
    }

    // 首页只显示"推荐"分类,每页 5 篇
    if ( $query->is_home() ) {
        $query->set( 'cat', 3 );
        $query->set( 'posts_per_page', 5 );
    }

    // 搜索结果排除页面类型
    if ( $query->is_search() ) {
        $query->set( 'post_type', 'post' );
    }

    // 产品归档按销量排序
    if ( $query->is_post_type_archive( 'product' ) ) {
        $query->set( 'posts_per_page', 24 );
        $query->set( 'meta_key', '_sales' );
        $query->set( 'orderby', 'meta_value_num' );
        $query->set( 'order', 'DESC' );
    }
} );

两个必写的判断:is_admin() 防止影响后台列表;$query->is_main_query() 防止误伤侧边栏的自定义查询。这一段代码放插件里,不要放主题 functions.php——它属于查询逻辑而非外观,划分依据见functions.php 与插件职责划分。

第 2 步:额外查一批内容用 new WP_Query

$related = new WP_Query( array(
    'post_type'           => 'post',
    'posts_per_page'      => 4,
    'post__not_in'        => array( get_the_ID() ),
    'category__in'        => wp_get_post_categories( get_the_ID() ),
    'ignore_sticky_posts' => true,
    'no_found_rows'       => true,   // 不需要分页 → 跳过 SQL_CALC_FOUND_ROWS,省一次查询
) );

if ( $related->have_posts() ) :
    echo '<ul class="related-list">';
    while ( $related->have_posts() ) :
        $related->the_post();
        printf(
            '<li><a href="%s">%s</a><span>%s</span></li>',
            esc_url( get_permalink() ),
            esc_html( get_the_title() ),
            esc_html( get_the_date() )
        );
    endwhile;
    echo '</ul>';
    wp_reset_postdata();   // 必须!恢复全局 $post
else :
    echo '<p>' . esc_html__( '暂无相关文章。', 'mimangfei' ) . '</p>';
endif;

wp_reset_postdata() 的作用是把全局 $post 恢复为主查询的当前文章。忘记它的典型症状是:相关文章之后的”评论区”显示了错误文章的评论。

第 3 步:常用参数速查

$args = array(
    // 基础
    'post_type'      => array( 'post', 'product' ),
    'post_status'    => 'publish',
    'posts_per_page' => 10,
    'paged'          => max( 1, get_query_var( 'paged' ) ),
    'offset'         => 0,                       // 注意:与 paged 同时用会打乱分页
    'orderby'        => 'date',
    'order'          => 'DESC',

    // 分类法
    'category__in'      => array( 3, 7 ),
    'category__not_in'  => array( 12 ),
    'tax_query'         => array(
        'relation' => 'AND',
        array(
            'taxonomy' => 'product_cat',
            'field'    => 'slug',
            'terms'    => 'phone',
        ),
    ),

    // 自定义字段
    'meta_query' => array(
        'relation' => 'OR',
        array(
            'key'     => '_featured',
            'value'   => '1',
            'compare' => '=',
        ),
        array(
            'key'     => '_views',
            'value'   => 1000,
            'type'    => 'NUMERIC',
            'compare' => '>',
        ),
    ),

    // 性能
    'no_found_rows'          => true,   // 不分页时开启
    'fields'                 => 'ids',  // 只要 ID 时开启
    'update_post_term_cache' => false,  // 不用分类信息时关掉
    'update_post_meta_cache' => false,  // 不用自定义字段时关掉
);
$q = new WP_Query( $args );

'fields' => 'ids' 与三个 false 是大型站点最常用的优化组合,能把一次查询的附带开销降到最低。

第 4 步:只要一批数据用 get_posts

$posts = get_posts( array(
    'posts_per_page' => 5,
    'meta_key'       => '_views',
    'orderby'        => 'meta_value_num',
) );

get_posts() 是 WP_Query 的轻量封装,默认 ignore_sticky_posts=true、no_found_rows=true、不触发 pre_get_posts。它返回文章对象数组,不污染全局,适合”随手取几条”的场景;但它不能分页,需要分页就得回到 new WP_Query()。

第 5 步:用 query 对象自带的方法遍历

$q = new WP_Query( array( 'post_type' => 'product', 'posts_per_page' => -1, 'fields' => 'ids' ) );

printf( '共找到 %d 个产品', (int) $q->found_posts );
printf( '当前页 %d 个', count( $q->posts ) );
printf( '总页数 %d', (int) $q->max_num_pages );

if ( $q->have_posts() ) {
    foreach ( $q->posts as $post_id ) {
        // fields => ids 时 $q->posts 是 ID 数组
        echo esc_html( get_the_title( $post_id ) );
    }
}

注意 fields => 'ids' 时要手动传 ID 给 get_the_title(),因为 the_post() 不会设置全局 $post。

第 6 步:用 Query Monitor 验证

装 Query Monitor 插件,页面底部会显示:SQL 总数、重复查询、慢查询、本次请求的所有条件标签值。排查分页与查询问题时,先看它给出的”主查询 query_vars”是否与你预期一致,再决定改哪里。

常见踩坑

  1. query_posts() 出现在任何模板里。 一律替换为 pre_get_posts(改主查询)或 new WP_Query()(次查询)。
  2. pre_get_posts 里忘了 $query->is_main_query()。 会把菜单、小组件的查询一起改掉。
  3. 忘记 wp_reset_postdata()。 后续代码读到错误文章,表现为评论、面包屑、上一篇下一篇全部错位。
  4. offset 与 paged 同时使用。 offset 会覆盖分页偏移,第 2 页会从第 N+1 条开始,导致翻页跳内容。要跳过前几条请用 pre_get_posts 里判断页码动态算 offset,或直接排除 post__not_in。
  5. posts_per_page => -1。 数据量大时直接拖垮数据库,宁可分页也不要”全查出来再说”。
  6. 案例: 某站在 archive.php 顶部用 query_posts( 'posts_per_page=12' ) 想控制归档页每页数量,结果分类分页全部失效、面包屑显示”首页”。改成 pre_get_posts 后恢复正常,同时数据库查询从 34 条降到 21 条——因为 query_posts 让主查询白跑了一次。这个案例也说明:query_posts 不只是”不推荐”,它在性能上也是实打实的浪费。
  7. 在 WP_Query 循环里嵌套另一个 WP_Query 且不重置。 内层循环结束后必须 wp_reset_postdata(),否则外层 while 的游标状态被破坏。分页相关的完整写法见paginate_links 与自定义查询分页。

总结

判断标准只有一句:你是想改”这一页本来要查的东西”,还是想”额外再查一批东西”? 前者用 pre_get_posts(记得 is_admin() + is_main_query() 双判断),后者用 new WP_Query()(记得 wp_reset_postdata())。query_posts() 任何情况下都不用。性能上再补三条:不分页就开 no_found_rows => true,只要 ID 就用 fields => 'ids',用不到的 term/meta 缓存就关掉。用 Query Monitor 复核查询数量,比凭感觉优化可靠得多。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。