本文约 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”是否与你预期一致,再决定改哪里。
常见踩坑
query_posts()出现在任何模板里。 一律替换为pre_get_posts(改主查询)或new WP_Query()(次查询)。pre_get_posts里忘了$query->is_main_query()。 会把菜单、小组件的查询一起改掉。- 忘记
wp_reset_postdata()。 后续代码读到错误文章,表现为评论、面包屑、上一篇下一篇全部错位。 offset与paged同时使用。offset会覆盖分页偏移,第 2 页会从第 N+1 条开始,导致翻页跳内容。要跳过前几条请用pre_get_posts里判断页码动态算 offset,或直接排除post__not_in。posts_per_page => -1。 数据量大时直接拖垮数据库,宁可分页也不要”全查出来再说”。- 案例: 某站在
archive.php顶部用query_posts( 'posts_per_page=12' )想控制归档页每页数量,结果分类分页全部失效、面包屑显示”首页”。改成pre_get_posts后恢复正常,同时数据库查询从 34 条降到 21 条——因为query_posts让主查询白跑了一次。这个案例也说明:query_posts不只是”不推荐”,它在性能上也是实打实的浪费。 - 在
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 复核查询数量,比凭感觉优化可靠得多。
