I read @nacin's You don't know Query yesterday and was sent down a bit of a querying rabbit hole. Before yesterday, I was (wrongly) using query_posts() for all my querying needs. Now I'm a little bit wiser about using WP_Query(), but still have some gray areas.

What I think I know for sure:

If I'm making additional loops anywhere on a page—in the sidebar, in a footer, any kind of "related posts", etc—I want to be using WP_Query(). I can use that repeatedly on a single page without any harm. (right?).

What I don't know for sure

  1. When do I use @nacin's pre_get_posts vs. WP_Query()? Should I use pre_get_posts for everything now?
  2. When I want to modify the loop in a template page — lets say I want to modify a taxonomy archive page — do I remove the if have_posts : while have_posts : the_post part and write my own WP_Query()? Or do I modify the output using pre_get_posts in my functions.php file?


The tl;dr rules I'd like to draw from this are:

  1. Never use query_posts anymore
  2. When running multiple queries on a single page, use WP_Query()
  3. When modifying a loop, do this __________________.

Thanks for any wisdom


ps: I have seen and read: When should you use WP_Query vs query_posts() vs get_posts()? Which adds another dimension — get_posts. But doesn't deal with pre_get_posts at all.


5 Answers 5


You are right to say:

Never use query_posts anymore


pre_get_posts is a filter, for altering any query. It is most often used to alter only the 'main query':

function wpse50761_alter_query($query){

      if( $query->is_main_query() ){
        //Do something to main query

(I would also check that is_admin() returns false - though this may be redundant.). The main query appears in your templates as:

if( have_posts() ):
    while( have_posts() ): the_post();
       //The loop

If you ever feel the need to edit this loop - use pre_get_posts. i.e. If you are tempted to use query_posts() - use pre_get_posts instead.


The main query is an important instance of a WP_Query object. WordPress uses it to decide which template to use, for example, and any arguments passed into the url (e.g. pagination) are all channelled into that instance of the WP_Query object.

For secondary loops (e.g. in side-bars, or 'related posts' lists) you'll want to create your own separate instance of the WP_Query object. E.g.

$my_secondary_loop = new WP_Query(...);
if( $my_secondary_loop->have_posts() ):
    while( $my_secondary_loop->have_posts() ): $my_secondary_loop->the_post();
       //The secondary loop

Notice wp_reset_postdata(); - this is because the secondary loop will override the global $post variable which identifies the 'current post'. This essentially resets that to the $post we are on.


This is essentially a wrapper for a separate instance of a WP_Query object. This returns an array of post objects. The methods used in the loop above are no longer available to you. This isn't a 'Loop', simply an array of post object.

global $post;
$args = array( 'numberposts' => 5, 'offset'=> 1, 'category' => 1 );
$myposts = get_posts( $args );
foreach( $myposts as $post ) :  setup_postdata($post); ?>
    <li><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></li>
<?php endforeach; wp_reset_postdata(); ?>

In response to your questions

  1. Use pre_get_posts to alter your main query. Use a separate WP_Query object (method 2) for secondary loops in the template pages.
  2. If you want to alter the query of the main loop, use pre_get_posts.
  • So is there any scenario when one would go straight to get_posts() rather than WP_Query?
    – urok93
    Commented Aug 25, 2012 at 16:09
  • 2
    @drtanz - yes. Say for instance you don't need pagination, or sticky posts at the top - in these instances get_posts() is more efficient. Commented Aug 25, 2012 at 18:00
  • 1
    But wouldn't that add an extra query where we could just modify pre_get_posts to modify the main query?
    – urok93
    Commented Aug 26, 2012 at 20:54
  • 2
    @StephenHarris Right =) If you use next_post() on the object instead of using the_post, you don't step on the global query and don't need to remember to use wp_reset_postdata afterwards.
    – Privateer
    Commented Sep 19, 2015 at 18:01
  • 1
    True =) I never use any of those so I don't miss them.
    – Privateer
    Commented Sep 24, 2015 at 16:34

There are two different contexts for loops:

  • main loop that happens based on URL request and is processed before templates are loaded
  • secondary loops that happen in any other way, called from template files or otherwise

Problem with query_posts() is that it is secondary loop that tries to be main one and fails miserably. Thus forget it exists.

To modify main loop

  • don't use query_posts()
  • use pre_get_posts filter with $query->is_main_query() check
  • alternately use request filter (a little too rough so above is better)

To run secondary loop

Use new WP_Query or get_posts() which are pretty much interchangeable (latter is thin wrapper for former).

To cleanup

Use wp_reset_query() if you used query_posts() or messed with global $wp_query directly - so you will almost never need to.

Use wp_reset_postdata() if you used the_post() or setup_postdata() or messed with global $post and need to restore initial state of post-related things.

  • 4
    Rarst meant wp_reset_postdata()
    – Gregory
    Commented Jun 1, 2012 at 9:18

There are legitimate scenarios for using query_posts($query), for example:

  1. You want to display a list of posts or custom-post-type posts on a page (using a page template)

  2. You want to make pagination of those posts work

Now why would you want to display it on a page instead of using an archive template?

  1. It's more intuitive for an administrator (your customer?) - they can see the page in the 'Pages'

  2. It's better for adding it to menus (without the page, they'd have to add the url directly)

  3. If you want to display additional content (text, post thumbnail, or any custom meta content) on the template, you can easily get it from the page (and it all makes more sense for the customer too). See if you used an archive template, you'd either need to hardcode the additional content or use for example theme/plugin options (which makes it less intuitive for the customer)

Here's a simplified example code (which would be on your page template - e.g. page-page-of-posts.php):

 * Template Name: Page of Posts

while(have_posts()) { // original main loop - page content
  the_title(); // title of the page
  the_content(); // content of the page
  // etc...

// now we display list of our custom-post-type posts

// first obtain pagination parametres
$paged = 1;
if(get_query_var('paged')) {
  $paged = get_query_var('paged');
} elseif(get_query_var('page')) {
  $paged = get_query_var('page');

// query posts and replace the main query (page) with this one (so the pagination works)
query_posts(array('post_type' => 'my_post_type', 'post_status' => 'publish', 'paged' => $paged));

// pagination

// loop
while(have_posts()) {
  the_title(); // your custom-post-type post's title
  the_content(); // // your custom-post-type post's content

wp_reset_query(); // sets the main query (global $wp_query) to the original page query (it obtains it from global $wp_the_query variable) and resets the post data

// So, now we can display the page-related content again (if we wish so)
while(have_posts()) { // original main loop - page content
  the_title(); // title of the page
  the_content(); // content of the page
  // etc...

Now, to be perfectly clear, we could avoid using query_posts() here too and use WP_Query instead - like so:

// ...

global $wp_query;
$wp_query = new WP_Query(array('your query vars here')); // sets the new custom query as a main query

// your custom-post-type loop here


// ...

But, why would we do that when we have such a nice little function available for it?

  • 2
    Brian, thanks for that. I've been struggling to get pre_get_posts to work on a page in EXACTLY the scenario you describe: client needs to add custom fields/content to what otherwise would be an archive page, so a "page" needs to be created; client needs to see something to add to nav menu, as adding a custom link escapes them; etc. +1 from me!
    – Will Lanni
    Commented Dec 13, 2012 at 11:07
  • 3
    That can also be done using "pre_get_posts". I did that to have a "static front page" listing my custom post types in a custom order and with a custom filter. This page is also paginated. Check out this question to see how it works: wordpress.stackexchange.com/questions/30851/… So in short, there is still no more legitimate scenario for using query_posts ;)
    – 2ndkauboy
    Commented Jan 12, 2015 at 14:47
  • 2
    Because "It should be noted that using this to replace the main query on a page can increase page loading times, in worst case scenarios more than doubling the amount of work needed or more. While easy to use, the function is also prone to confusion and problems later on." Source codex.wordpress.org/Function_Reference/query_posts Commented Mar 9, 2015 at 16:31
  • 1
    THis answer is all kinds of wrong. You can create a "Page" in WP with the same URL as the Custom post type. EG if your CPT is Bananas, you can get a page named Bananas with the same URL. Then you'd end up with siteurl.com/bananas. As long as you have archive-bananas.php in your theme folder, then it will use the template and "override" that page instead. As stated in one of the other comments, using this "method" creates twice the workload for WP, thus should NOT ever be used. Commented Jun 19, 2015 at 20:07

I modify WordPress query from functions.php:

//unfortunately, "IS_PAGE" condition doesn't work in pre_get_posts (it's WORDPRESS behaviour)
//so you can use `add_filter('posts_where', ....);`    OR   modify  "PAGE" query directly into template file

add_action( 'pre_get_posts', 'myFunction' );
function myFunction($query) {
    if ( ! is_admin() && $query->is_main_query() )  {
        if (  $query->is_category ) {
            $query->set( 'post_type', array( 'post', 'page', 'my_postType' ) );
            add_filter( 'posts_where' , 'MyFilterFunction_1' ) && $GLOBALS['call_ok']=1; 
function MyFilterFunction_1($where) {
   return (empty($GLOBALS['call_ok']) || !($GLOBALS['call_ok']=false)  ? $where :  $where . " AND ({$GLOBALS['wpdb']->posts}.post_name NOT LIKE 'Journal%')"; 
  • would be interested to see this example but where clause is on custom meta. Commented Mar 3, 2017 at 16:20

Just to outline some improvements to the accepted answer since WordPress evolved over the time and some things are different now (five years later):

pre_get_posts is a filter, for altering any query. It is most often used to alter only the 'main query':

Actually is an action hook. Not a filter, and it will affect any query.

The main query appears in your templates as:

if( have_posts() ):
    while( have_posts() ): the_post();
       //The loop

Actually, this is also not true. The function have_posts iterates the global $wp_query object that is not related only to the main query. global $wp_query; may be altered with the secondary queries also.

function have_posts() {
    global $wp_query;
    return $wp_query->have_posts();


This is essentially a wrapper for a separate instance of a WP_Query object.

Actually, nowadays WP_Query is a class, so we have an instance of a class.

To conclude: At the time @StephenHarris wrote most likely all this was true, but over the time things in WordPress have been changed.

  • Technically, it's all filters under the hood, actions are just a simple filter. But you are correct here, it's an action that passes an argument by reference, which is how it differs from more simple actions.
    – Milo
    Commented Dec 28, 2016 at 2:16
  • get_posts returns an array of post objects, not a WP_Query object, so that is indeed still correct. and WP_Query has always been a class, instance of a class = object.
    – Milo
    Commented Dec 28, 2016 at 2:16
  • Thanks, @Milo, correct from some reason I had oversimplified model in my head.
    – prosti
    Commented Dec 28, 2016 at 8:51

Not the answer you're looking for? Browse other questions tagged or ask your own question.