WP_Query Explained: A Complete Reference to Arguments and Methods

Introduction

WP_Query is the engine behind almost every list of content WordPress shows. The blog index, category archives, search results, and any custom list you build all run through it. Learn WP_Query well and you can fetch exactly the posts you want, in the order you want, without writing a line of SQL.

This is a reference article. It explains what WP_Query is, how to use it safely, and what each important argument does, grouped so you can find the one you need. Keep it open in a tab while you build queries.

You will learn:

  • What WP_Query is and how it relates to the main query and the Loop
  • The argument groups that cover post type, taxonomy, meta, dates, ordering, and pagination
  • The properties and methods on a query object that you actually use
  • How to keep custom queries fast and avoid the common mistakes

This builds on the conceptual Loop article. If have_posts() and the_post() are unfamiliar, read The WordPress Loop Explained first.

What Is WP_Query?

WP_Query is a PHP class built into WordPress. You give it an array of arguments describing the posts you want, and it runs the database query, stores the results, and gives you methods to loop over them.

There are three ways WP_Query runs:

  • The main query. WordPress builds one WP_Query automatically from the URL on every request. Your templates loop over it with the bare have_posts() and the_post().
  • A new WP_Query. You create your own instance for a secondary list, then loop over that object and call wp_reset_postdata() when done.
  • Through get_posts(). This helper creates a WP_Query internally and returns just the array of posts.

This article focuses on creating your own WP_Query. To change the main query instead, use the pre_get_posts hook, which is covered in its own article.

Basic Usage

A custom query has three steps: build the arguments, create the object, and loop over it. Always reset post data afterward so later template tags keep working.

<?php
$query = new WP_Query( array(
    'post_type'      => 'post',
    'posts_per_page' => 5,
) );

if ( $query->have_posts() ) {
    while ( $query->have_posts() ) {
        $query->the_post();
        the_title( '<h2>', '</h2>' );
    }
    wp_reset_postdata();
}

The argument array is where all the power lives. The rest of this reference walks through the argument groups you will use most.

Post Type and Status Arguments

  • post_type – The post type to query: 'post' (default), 'page', 'any', a custom post type slug, or an array of slugs.
  • post_status – Which statuses to include, such as 'publish' (default for most queries), 'draft', 'private', or 'any'.
  • p – Query a single post by its numeric ID.
  • name – Query a single post by its slug.
  • post__in – An array of post IDs to include. Pair with orderby set to 'post__in' to preserve the order you listed.
  • post__not_in – An array of post IDs to exclude.
  • post_parent – For hierarchical types, return children of this parent ID.
<?php
// The three newest products, excluding two specific IDs.
$args = array(
    'post_type'      => 'product',
    'post_status'    => 'publish',
    'posts_per_page' => 3,
    'post__not_in'   => array( 45, 67 ),
);

A common gotcha: combining post__in and post__not_in in the same query is unsupported and produces unreliable results. Use one or the other.

Author Arguments

  • author – An author ID, or a comma-separated list of IDs.
  • author_name – The author's user_nicename (the slug), not the display name.
  • author__in – An array of author IDs to include.
  • author__not_in – An array of author IDs to exclude.

Category and Tag Arguments

Categories and tags are taxonomies, so they have simple shortcut arguments and a more powerful general form.

  • cat – A category ID, or a list of IDs.
  • category_name – A category slug. Use a comma for OR, a plus for AND.
  • category__in – An array of category IDs; posts in any of them match.
  • category__and – An array of category IDs; only posts in all of them match.
  • category__not_in – An array of category IDs to exclude.
  • tag – A tag slug.
  • tag_id – A single tag ID.
  • tag__in, tag__and, tag__not_in – Array forms that behave like the category equivalents.

Taxonomy Arguments (tax_query)

For custom taxonomies, or for complex category logic, use tax_query. It accepts one or more clauses and a relation between them.

<?php
$args = array(
    'post_type' => 'movie',
    'tax_query' => array(
        'relation' => 'AND',
        array(
            'taxonomy' => 'genre',
            'field'    => 'slug',
            'terms'    => array( 'drama', 'thriller' ),
        ),
        array(
            'taxonomy' => 'rating',
            'field'    => 'slug',
            'terms'    => array( 'pg-13' ),
            'operator' => 'NOT IN',
        ),
    ),
);

Key points for each clause:

  • taxonomy – The taxonomy slug to query.
  • field – How you are identifying terms: 'term_id' (default), 'slug', or 'name'.
  • terms – The term IDs, slugs, or names to match.
  • operator – 'IN' (default), 'NOT IN', 'AND', or 'EXISTS'.
  • relation – At the top level, 'AND' or 'OR' between multiple clauses.

Custom Field Arguments (meta_query)

To filter by post meta (custom fields), use meta_query. Like tax_query, it takes clauses and a relation. This is the right tool whenever you store extra data on posts.

<?php
$args = array(
    'post_type'  => 'event',
    'meta_query' => array(
        'relation' => 'AND',
        array(
            'key'     => 'event_date',
            'value'   => date( 'Y-m-d' ),
            'compare' => '>=',
            'type'    => 'DATE',
        ),
        array(
            'key'     => 'is_featured',
            'value'   => '1',
            'compare' => '=',
        ),
    ),
    'orderby'  => 'meta_value',
    'meta_key' => 'event_date',
    'order'    => 'ASC',
);

Important clause options:

  • key – The meta key to test.
  • value – The value to compare against. Can be an array for BETWEEN or IN.
  • compare – The operator: '=', '!=', '>', '>=', '<', '<=', 'LIKE', 'IN', 'NOT IN', 'BETWEEN', 'EXISTS', and more.
  • type – How to cast the value for comparison: 'CHAR' (default), 'NUMERIC', 'DATE', 'DATETIME'. Set this when comparing numbers or dates, or the comparison runs as text.

Meta queries are flexible but can be slow on large sites because post meta is not indexed for arbitrary lookups. Use them where needed, but prefer taxonomies for data you filter on constantly. Meta queries are covered in depth in the meta query article.

Date Arguments (date_query)

date_query filters posts by publication date with readable arguments.

<?php
$args = array(
    'date_query' => array(
        array(
            'after'     => '2024-01-01',
            'before'    => '2024-12-31',
            'inclusive' => true,
        ),
    ),
);

You can also match parts of a date, for example all posts published in a given month with array( "month" => 6 ), or in the last week with array( "after" => "1 week ago" ). Relative strings that strtotime() understands are accepted.

Search Argument

  • s – A search keyword. WordPress searches post titles and content. Combine it with post_type to scope the search to a specific type.

Order and Orderby Arguments

  • orderby – The field to sort by. Common values: 'date' (default), 'title', 'menu_order', 'rand', 'ID', 'comment_count', 'meta_value', 'meta_value_num', and 'post__in'.
  • order – 'DESC' (default, newest or highest first) or 'ASC'.
  • meta_key – Required when ordering by 'meta_value' or 'meta_value_num'. Use 'meta_value_num' for numeric meta so 2 sorts before 10.

You can sort by multiple fields by passing an array to orderby, for example array( "menu_order" => "ASC", "title" => "ASC" ).

Pagination and Quantity Arguments

  • posts_per_page – How many posts to return. Use -1 for all posts, but be careful: on a large site that can load thousands of rows and exhaust memory.
  • paged – Which page of results to return. Read it from get_query_var( 'paged' ) for archive contexts.
  • offset – Skip this many posts. Note that offset overrides paged, so do not mix them when you need working pagination.
  • nopaging – Set to true to return every matching post and ignore paging entirely.

For a paginated custom query, pass paged and render links with paginate_links() using the max_num_pages property. The pattern is shown in the Loop patterns article.

Performance Arguments

On queries where you do not need everything, these arguments cut unnecessary work.

  • no_found_rows – Set to true when you do not need pagination. It skips the extra COUNT query WordPress runs to calculate total pages, which is a real saving on big tables.
  • update_post_meta_cache – Set to false when you will not read any post meta in the loop, to skip priming the meta cache.
  • update_post_term_cache – Set to false when you will not read terms (categories, tags) in the loop.
  • fields – Set to 'ids' to get back only an array of post IDs, or 'id=>parent' for a lightweight map. This avoids hydrating full post objects when you only need IDs.
<?php
// A fast query for IDs only, with no pagination overhead.
$ids = new WP_Query( array(
    'post_type'      => 'post',
    'posts_per_page' => 20,
    'no_found_rows'  => true,
    'fields'         => 'ids',
) );

One caveat to remember: when no_found_rows is true, WordPress skips the count query, so found_posts and max_num_pages are no longer accurate. Only use no_found_rows on queries where you do not need pagination or a total count.

Useful Properties of the Query Object

After a query runs, these properties tell you about the results:

  • $query->posts – The array of post objects returned.
  • $query->post_count – How many posts are in this page of results.
  • $query->found_posts – The total number of matching posts across all pages.
  • $query->max_num_pages – The total number of pages, used to build pagination links.
  • $query->current_post – The zero-based index of the post currently being looped, useful for styling the first item.
  • $query->is_main_query() – Returns true if this is the main query, important inside pre_get_posts.

Best Practices

  • Always call wp_reset_postdata() after a custom query that uses the_post(), so later template tags read the main post again.
  • Add no_found_rows when you do not need pagination, and fields set to 'ids' when you only need IDs.
  • Avoid posts_per_page set to -1 on user-facing queries. Set a sane limit instead.
  • Set the meta type to 'NUMERIC' or 'DATE' when comparing numbers or dates, or comparisons run as strings.
  • Prefer taxonomies over meta for data you filter on frequently; taxonomy lookups are indexed and faster.
  • Do not build a new WP_Query to change an archive listing. Use pre_get_posts on the main query instead.

Common Mistakes

Using posts_per_page set to -1

Fetching every post sounds convenient but loads the entire result set into memory. On a site with thousands of posts this is slow and can crash the request. Always cap the count, and paginate when you need more.

Comparing numbers as strings in meta_query

Without type set to NUMERIC, a meta comparison treats values as text, so "10" sorts before "9". Set the correct type whenever the value is a number or a date.

Forgetting wp_reset_postdata()

A custom loop that calls the_post() leaves the global $post pointing at the last result. Template tags later on the page then show the wrong data. Reset every time.

Mixing offset and paged

offset overrides the paged calculation, so pagination silently breaks. Use paged for paginated queries and reserve offset for fixed, unpaginated lists.

Troubleshooting

My query returns nothing

Check post_status first; if you query drafts or a custom status without setting post_status, they are excluded. Then dump the arguments and confirm taxonomy and meta keys are spelled correctly. var_dump( $query->request ) shows the actual SQL WordPress built.

Ordering by a custom field does not work

Ensure you set both orderby to meta_value (or meta_value_num) and meta_key to the field name. Use meta_value_num for numbers so they sort numerically.

The query is slow

Large meta_query filters and posts_per_page set to -1 are the usual causes. Add no_found_rows where pagination is not needed, narrow the query with a taxonomy, and consider caching expensive results in a transient.

Frequently Asked Questions

What is the difference between WP_Query and get_posts()?

get_posts() is a thin wrapper that creates a WP_Query and returns only the array of posts, with some defaults like suppress_filters turned on. Use WP_Query when you need the full loop, pagination, or query properties; use get_posts() for a quick array. The full comparison, including query_posts(), is covered in its own article.

How do I change the number of posts on an archive?

Do not build a new WP_Query in the template. Hook pre_get_posts, check is_main_query() and the relevant conditional, and set posts_per_page there. That keeps pagination correct.

Can WP_Query search custom fields?

Yes, through meta_query. The s argument searches titles and content; meta_query filters by custom field values. You can combine both in one query.

How do I get only post IDs for performance?

Set fields to 'ids'. The query then returns an array of IDs instead of full post objects, which is much lighter when you only need to reference posts.

Does WP_Query cache its results?

WordPress caches some of the supporting data (post, meta, and term caches) within a request, but the main SQL query runs each time. For expensive queries that do not change often, store the result in a transient yourself.

Conclusion

WP_Query is the one class worth learning thoroughly, because it underlies every list of content in WordPress. Once you know the argument groups, post type, taxonomy, meta, date, ordering, and pagination, you can express almost any content request as a clean array of arguments.

Keep two habits front of mind: reset post data after every custom loop, and reach for the performance arguments when you do not need everything. Those two alone prevent most WP_Query bugs and slowdowns.

Next, How to Build Custom Queries with WP_Query turns this reference into worked examples, and How to Modify Main Queries with pre_get_posts shows the correct way to change the queries WordPress builds for you.