wppaste
WordPress

WP_Query

Since
1.5.0, 4.5.0
Source
wp-includes/class-wp-query.php:18

Query WordPress posts with WP_Query, the class behind every post loop: filter by post type, taxonomy, meta, date, and more, with pagination built in. Instantiate it with an argument array to build secondary loops, or use its is_*() conditional methods to identify what the current request is for.

The WordPress Query class.

Compatibility

WordPress
since 4.5.0
  • 6.7.7
  • 6.8.8
  • 6.9.7
  • 7.0.4
  • 7.1.0

Present in every tracked release (6.7.7 to 7.1.0).

Usage

WP_Query is the engine behind every post listing in WordPress. The main query that builds each front-end page is a WP_Query instance stored in the global $wp_query, and any theme or plugin can create additional instances to fetch its own sets of posts. You pass an array of query variables to the constructor, the class translates them into a single SQL query against the posts table (joining terms, postmeta, and users as needed), and the results become available through the loop methods.

Before reaching for the class directly, check whether a simpler tool fits. Conditional tags such as is_single(), is_page(), and is_archive() read the main query's flags for you, and template tags like the_title() and the_content() already operate on the current post inside the loop. Instantiate your own WP_Query only when you genuinely need a second, custom set of posts.

The standard loop

The typical pattern: build the query, check have_posts(), then iterate with the_post(), which advances the internal pointer and populates the global $post so template tags work.

$the_query = new WP_Query( array(
	'post_type'      => 'post',
	'posts_per_page' => 5,
) );

if ( $the_query->have_posts() ) {
	echo '<ul>';
	while ( $the_query->have_posts() ) {
		$the_query->the_post();
		echo '<li>' . esc_html( get_the_title() ) . '</li>';
	}
	echo '</ul>';
} else {
	// No posts matched.
}

wp_reset_postdata();

The same loop in template style, convenient inside theme files:

<?php $the_query = new WP_Query( $args ); ?>

<?php if ( $the_query->have_posts() ) : ?>

	<?php while ( $the_query->have_posts() ) : $the_query->the_post(); ?>
		<h2><?php the_title(); ?></h2>
	<?php endwhile; ?>

	<?php wp_reset_postdata(); ?>

<?php else : ?>
	<p><?php esc_html_e( 'Nothing found.', 'textdomain' ); ?></p>
<?php endif; ?>

Restoring post data

Calling the_post() on a secondary query overwrites the global $post. Once your custom loop finishes, call wp_reset_postdata() so template tags refer to the main query's current post again. This matters most inside single templates and inside shortcodes or blocks that run mid-loop.

You do not need wp_reset_query() for a secondary WP_Query; that function exists to undo query_posts(), which replaces the global $wp_query itself and should be avoided entirely. When running queries in admin screens or AJAX handlers, get_posts() is often the safer choice because it never touches the loop globals in the first place.

Multiple independent loops on one page follow the same rule, reset after each:

$recent = new WP_Query( array( 'posts_per_page' => 3 ) );
while ( $recent->have_posts() ) {
	$recent->the_post();
	the_title( '<h3>', '</h3>' );
}
wp_reset_postdata();

$featured = new WP_Query( array( 'category_name' => 'featured' ) );
while ( $featured->have_posts() ) {
	$featured->the_post();
	the_title( '<h3>', '</h3>' );
}
wp_reset_postdata();

Interacting via query() and the accessors

The constructor is a thin wrapper: if you pass arguments, it hands them to the [query()](/reference/classes/wp_query/query) method, which parses them and immediately fetches posts. You can also build an empty instance and run it later, or rerun it with different arguments:

$q = new WP_Query();
$q->query( array( 'post_type' => 'book', 'posts_per_page' => 10 ) );

Use [get()](/reference/classes/wp_query/get) and [set()](/reference/classes/wp_query/set) to read or change individual query vars rather than poking at the $query_vars array directly, and get_query_var() for the main query. After the query runs, $q->posts holds the results, $q->post_count the number fetched for this page, $q->found_posts the total matches, and $q->max_num_pages the page count.

Checking what kind of request this is

Besides fetching posts, WP_Query classifies each request. After parsing, exactly the flags matching the request are true: $is_home, $is_front_page (via the method), $is_single, $is_page, $is_singular, $is_archive, $is_category, $is_tag, $is_tax, $is_author, $is_date (with $is_year, $is_month, $is_day, $is_time), $is_search, $is_feed, $is_404, $is_attachment, $is_paged, $is_post_type_archive, $is_privacy_policy, $is_embed, and a few more. The global conditional tags (is_single(), is_archive(), is_search(), and friends) simply proxy to the main query's methods, which is why they are unreliable before the main query has run (use them at wp or later, or inside pre_get_posts call the methods on the query object you are handed).

add_action( 'pre_get_posts', function ( $query ) {
	if ( ! is_admin() && $query->is_main_query() && $query->is_search() ) {
		$query->set( 'post_type', array( 'post', 'docs' ) );
	}
} );

For archive-style requests, [get_queried_object()](/reference/classes/wp_query/get_queried_object) returns the thing the archive is about: a WP_Term on category, tag, and taxonomy archives, a WP_User on author archives, a WP_Post on singular views, a WP_Post_Type on post type archives. [get_queried_object_id()](/reference/classes/wp_query/get_queried_object_id) returns its ID.

WP_Query vs get_posts() vs pre_get_posts

  • new WP_Query( $args ) is the right tool for a secondary loop you will iterate with have_posts() and the_post(), and when you need the object itself (pagination totals, conditional flags, the generated SQL in $request).
  • get_posts() wraps WP_Query and simply returns the array of posts. It defaults to 'ignore_sticky_posts' => true and 'no_found_rows' => true, and it suppresses most query filters by default (suppress_filters is true). Prefer it for plain "give me these posts" retrieval where you will foreach the array yourself.
  • The pre_get_posts action is for changing a query that WordPress is already going to run, above all the main query. Never build a second query to "replace" the main one on an archive; hook pre_get_posts, check $query->is_main_query() and ! is_admin(), then adjust vars with $query->set(). This keeps pagination, canonical URLs, and template selection consistent.
add_action( 'pre_get_posts', function ( $query ) {
	if ( ! is_admin() && $query->is_main_query() && $query->is_home() ) {
		$query->set( 'posts_per_page', 6 );
		$query->set( 'post_type', array( 'post', 'portfolio' ) );
	}
} );

Query parameters

Everything below is passed as keys in the array given to the constructor (or to [query()](/reference/classes/wp_query/query)). Unspecified keys fall back to defaults: post_type of post, post_status of publish (plus private for logged-in users who can read them), the blog's "posts per page" setting for posts_per_page, and ordering by post_date descending. A query string form ('cat=4&posts_per_page=5') is also accepted, but the array form is clearer and required for the nested structures (tax_query, meta_query, date_query).

Parameters from different groups combine with AND semantics: every condition you add further narrows the result set. See the Combining parameters section for multi-taxonomy and meta-plus-taxonomy patterns.

Author parameters

Restrict results to posts by particular authors.

  • author (int or string): one author ID, a comma-separated list of IDs, or a negative ID to exclude that author.
  • author_name (string): the author's user_nicename (the URL-safe login slug), not the display name.
  • author__in (array): author IDs to include.
  • author__not_in (array): author IDs to exclude.

author__in and author__not_in cannot be used together in one query.

// Posts by author 123.
$q = new WP_Query( array( 'author' => 123 ) );

// Posts by nicename.
$q = new WP_Query( array( 'author_name' => 'rami' ) );

// Several authors, or everyone except two.
$q = new WP_Query( array( 'author__in' => array( 2, 6 ) ) );
$q = new WP_Query( array( 'author__not_in' => array( 2, 6 ) ) );

// Everyone except one author.
$q = new WP_Query( array( 'author' => -12 ) );

Category parameters

Restrict results by category. These apply to the built-in category taxonomy; for custom taxonomies use tax_query.

  • cat (int or string): category ID; a comma-separated list means "in any of these"; negative IDs exclude. Includes posts in child categories.
  • category_name (string): category slug (despite the name). A comma-separated list matches any of the slugs; joining slugs with + requires all of them. Includes children.
  • category__in (array): category IDs, posts in any of them. Does not include child categories.
  • category__not_in (array): category IDs to exclude (children of these categories are not excluded).
  • category__and (array): category IDs, posts must be in all of them. Does not include children.
// In category 4, including its children.
$q = new WP_Query( array( 'cat' => 4 ) );

// By slug, either of two categories.
$q = new WP_Query( array( 'category_name' => 'staff,news' ) );

// Must be in both categories.
$q = new WP_Query( array( 'category__and' => array( 2, 6 ) ) );

// Exclude several categories.
$q = new WP_Query( array( 'cat' => '-12,-34,-56' ) );

Tag parameters

Restrict results by tag (the built-in post_tag taxonomy).

  • tag (string): tag slug; comma-separated for "any of", +-joined for "all of".
  • tag_id (int): tag ID.
  • tag__in (array): tag IDs, any of them.
  • tag__not_in (array): tag IDs to exclude.
  • tag__and (array): tag IDs, all required.
  • tag_slug__in (array): tag slugs, any of them.
  • tag_slug__and (array): tag slugs, all required.
// One tag by slug.
$q = new WP_Query( array( 'tag' => 'cooking' ) );

// Any of these tags.
$q = new WP_Query( array( 'tag' => 'bread,baking' ) );

// All of these tags.
$q = new WP_Query( array( 'tag' => 'bread+baking+recipe' ) );

// Tagged with both IDs 37 and 47.
$q = new WP_Query( array( 'tag__and' => array( 37, 47 ) ) );

Taxonomy parameters

tax_query is the general mechanism for querying any taxonomy, including category, post_tag, post_format, and custom taxonomies. It is parsed by [WP_Tax_Query](/reference/classes/wp_tax_query). The value is always an array of clause arrays, even for a single clause.

Top-level key:

  • relation (string): AND (default) or OR, the logical join between clauses. Omit it when there is only one clause.

Each clause array accepts:

  • taxonomy (string): taxonomy name.
  • field (string): what terms refers to: term_id (default), slug, name, or term_taxonomy_id.
  • terms (int, string, or array): the term or terms to match.
  • include_children (bool): for hierarchical taxonomies, whether descendant terms also match. Default true.
  • operator (string): IN (default), NOT IN, AND (post must have every listed term), EXISTS, or NOT EXISTS (post has any term, or no term, in the taxonomy; terms is ignored for these two).
// Posts with the 'bob' term in a custom 'people' taxonomy.
$q = new WP_Query( array(
	'post_type' => 'post',
	'tax_query' => array(
		array(
			'taxonomy' => 'people',
			'field'    => 'slug',
			'terms'    => 'bob',
		),
	),
) );

Two clauses joined with a relation:

// Action or comedy genre, but never these three actors.
$q = new WP_Query( array(
	'post_type' => 'movie',
	'tax_query' => array(
		'relation' => 'AND',
		array(
			'taxonomy' => 'movie_genre',
			'field'    => 'slug',
			'terms'    => array( 'action', 'comedy' ),
		),
		array(
			'taxonomy' => 'actor',
			'field'    => 'term_id',
			'terms'    => array( 103, 115, 206 ),
			'operator' => 'NOT IN',
		),
	),
) );

Note that setting tax_query changes the default post_type from post to any, so set post_type explicitly when you care. Nesting of clause groups is covered under Combining parameters.

The old shorthand of using a taxonomy slug directly as a query var ('people' => 'bob') has been deprecated since WordPress 3.1; write a tax_query instead. The registered taxonomy's query_var still works for URL-driven queries, but new code should not rely on it for programmatic queries.

An EXISTS example, every post that has at least one term in a taxonomy:

$q = new WP_Query( array(
	'post_type' => 'post',
	'tax_query' => array(
		array(
			'taxonomy' => 'people',
			'operator' => 'EXISTS',
		),
	),
) );

Search parameters

  • s (string): keyword search across post titles, excerpts, and content. Prefix a word with a hyphen to exclude posts containing it ('pillow -sofa' finds pillow posts that never mention sofa).
  • search_columns (array): limit which columns are searched; any of post_title, post_excerpt, post_content. Empty (the default) searches all three. Available since WordPress 6.2.
  • exact (bool): require the search string to match a whole field value rather than a substring. Rarely useful on its own.
  • sentence (bool): treat the search string as one phrase instead of splitting it into terms.

When a search query has no explicit orderby, results are ranked by relevance: full-phrase matches first, then titles containing every term, then titles containing any term, then content matches.

// Title-only search.
$q = new WP_Query( array(
	's'              => 'block themes',
	'search_columns' => array( 'post_title' ),
) );

Post and page parameters

Target specific posts or pages directly. Remember the default post_type is post; page-oriented vars like page_id and pagename imply pages, but post__in and friends respect whatever post_type you set.

  • p (int): a single post ID.
  • name (string): a single post slug.
  • page_id (int): a single page ID.
  • pagename (string): a page slug; for a child page use the path form parent-slug/child-slug.
  • post_parent (int): return children of this ID; 0 returns only top-level items.
  • post_parent__in (array): posts whose parent is any of these IDs.
  • post_parent__not_in (array): posts whose parent is none of these IDs.
  • post__in (array): only these post IDs. An empty array does not mean "no posts"; it is treated as no constraint, so guard against empty input before querying. Sticky posts can still be prepended on home-context queries; add ignore_sticky_posts if that matters.
  • post__not_in (array): exclude these post IDs. Cannot be combined with post__in in the same query.
  • post_name__in (array): only posts with these slugs.
  • title (string): a single exact post title.

post__in and post__not_in want real arrays of integers. A string like '1,2,3' wrapped in an array is one useless element, not three IDs:

// Wrong: one string element.
$q = new WP_Query( array( 'post__not_in' => array( '1,2,3' ) ) );

// Right: an array of integers.
$q = new WP_Query( array( 'post__not_in' => array( 1, 2, 3 ) ) );
// A single post, a single page, children of a page.
$q = new WP_Query( array( 'p' => 7 ) );
$q = new WP_Query( array( 'pagename' => 'contact-us/canada' ) );
$q = new WP_Query( array( 'post_parent' => 93 ) );

// Exactly these pages.
$q = new WP_Query( array(
	'post_type' => 'page',
	'post__in'  => array( 2, 5, 12, 14, 20 ),
) );

Password parameters

  • has_password (bool or null): true for only password-protected posts, false for only unprotected posts, null (default) for both.
  • post_password (string): only posts protected with this exact password.
// Everything without a password.
$q = new WP_Query( array( 'has_password' => false ) );

// Posts using one specific password.
$q = new WP_Query( array( 'post_password' => 'zxcvbn' ) );

Post type parameters

  • post_type (string or array): which post type(s) to query. Default post, but the default becomes any when tax_query is present. Common values: post, page, attachment, revision, nav_menu_item, any registered custom post type, or any (every type except revisions and types registered with exclude_from_search true).

Attachments have a default post_status of inherit, not publish, so querying post_type => 'attachment' returns nothing unless you also set post_status to inherit or any.

// Several types at once, including custom ones.
$q = new WP_Query( array(
	'post_type' => array( 'post', 'page', 'movie', 'book' ),
) );

Status parameters

  • post_status (string or array): which statuses to include. Default is publish; logged-in users also get their readable private posts, public custom statuses are included, and in admin or AJAX context the protected statuses (future, draft, pending) are added. Values: publish, pending, draft, auto-draft, future, private, inherit (revisions and attachments), trash, any registered custom status, or any (everything except inherit, trash, auto-draft, and statuses registered with exclude_from_search true).
// Drafts and scheduled posts.
$q = new WP_Query( array(
	'post_status' => array( 'draft', 'pending', 'future' ),
) );

// All attachments.
$q = new WP_Query( array(
	'post_type'   => 'attachment',
	'post_status' => 'any',
) );

Comment parameters

  • comment_count (int or array): filter posts by how many approved comments they have. As an integer it means an exact match. As an array it takes value (int) and compare (one of =, !=, >, >=, <, <=; default =).
// Posts with at least 25 comments.
$q = new WP_Query( array(
	'comment_count' => array(
		'value'   => 25,
		'compare' => '>=',
	),
) );

Pagination parameters

  • posts_per_page (int): posts per page. -1 returns everything (and makes offset ignored). Defaults to the "Blog pages show at most" setting. In feed context WordPress substitutes the posts_per_rss option; use the post_limits filter if you need to override that.
  • posts_per_archive_page (int): overrides posts_per_page on pages where is_archive() or is_search() is true.
  • paged (int): which page of results, as used by "older posts" links. Pull the live value with get_query_var( 'paged' ).
  • page (int): the page number on a static front page. Also holds the sub-page of a single post split with the nextpage quicktag, so use get_query_var( 'page' ) in a static-front-page template.
  • offset (int): skip this many posts. Warning: a set offset overrides paged and breaks normal pagination; if you need both, compute the offset yourself per page. Ignored when posts_per_page is -1.
  • nopaging (bool): true disables paging and returns all posts. Default false.
  • no_found_rows (bool): true skips counting the total number of matching rows. found_posts and max_num_pages become useless, but the query is cheaper. Ideal for widgets and blocks that never paginate.
  • ignore_sticky_posts (bool): default false, meaning sticky posts are moved to the front of the first page of home-context queries (and this can also affect secondary queries whose vars leave them classified as a home query, including some post__in queries). Set true to leave stickies in natural order. Note that stickies excluded from a filtered query can still be prepended unless this is set.
// Correct pagination for a custom loop.
$paged = get_query_var( 'paged' ) ? get_query_var( 'paged' ) : 1;
$q = new WP_Query( array(
	'posts_per_page' => 5,
	'paged'          => $paged,
) );

// Everything, no paging.
$q = new WP_Query( array( 'posts_per_page' => -1 ) );

Sticky-post recipes:

// Only the newest sticky post; nothing if there are no stickies.
$sticky = get_option( 'sticky_posts' );
$q = new WP_Query( array(
	'posts_per_page'      => 1,
	'post__in'            => $sticky,
	'ignore_sticky_posts' => 1,
) );

// A category listing with stickies in natural date order.
$q = new WP_Query( array(
	'cat'                 => 6,
	'ignore_sticky_posts' => 1,
) );

// Exclude stickies entirely, with working pagination.
$q = new WP_Query( array(
	'cat'                 => 3,
	'ignore_sticky_posts' => 1,
	'post__not_in'        => get_option( 'sticky_posts' ),
	'paged'               => max( 1, get_query_var( 'paged' ) ),
) );

Order and orderby parameters

  • order (string or array): DESC (default) or ASC. Ignored per-key when orderby is an associative array (each key carries its own direction there).
  • orderby (string or array): what to sort by. Default date. Pass a single value, a space-separated list ('menu_order title'), or an associative array of value-to-direction pairs.

Accepted orderby values:

  • none: no ORDER BY clause at all.
  • ID: post ID (note the capitalization).
  • author: author ID.
  • title: post title.
  • name: post slug.
  • type: post type.
  • date: publish date (the default).
  • modified: last modified date.
  • parent: parent post ID.
  • rand: random order. Expensive on large tables; avoid on high-traffic pages.
  • comment_count: number of comments.
  • relevance: search ranking; the default when s is set (phrase match, then all terms in title, then any term in title, then content).
  • menu_order: the manual "Order" field on pages and attachments, usable by any post type; all posts default to 0.
  • meta_value: sort by the value of the custom field named in meta_key. The comparison is alphabetical, so numbers sort as strings (1, 10, 2) unless you set meta_type or use meta_value_num. With meta_type set (for example DATETIME), the matching alias such as meta_value_datetime also works.
  • meta_value_num: numeric sort on the meta_key value.
  • post__in: keep the exact ID order you passed in post__in. order has no effect.
  • post_name__in: keep the slug order passed in post_name__in. order has no effect.
  • post_parent__in: keep the parent-ID order passed in post_parent__in. order has no effect.
// Title, Z to A.
$q = new WP_Query( array( 'orderby' => 'title', 'order' => 'DESC' ) );

// menu_order first, title as tiebreaker.
$q = new WP_Query( array( 'orderby' => 'menu_order title', 'order' => 'ASC' ) );

// Independent directions per key.
$q = new WP_Query( array(
	'orderby' => array( 'title' => 'DESC', 'menu_order' => 'ASC' ),
) );

// Numeric custom field sort.
$q = new WP_Query( array(
	'post_type' => 'product',
	'meta_key'  => 'price',
	'orderby'   => 'meta_value_num',
	'order'     => 'ASC',
) );

To sort by more than one custom field, give the meta_query clauses names and reference those names in the orderby array (see named clauses under Custom field parameters):

$q = new WP_Query( array(
	'meta_query' => array(
		'relation'     => 'AND',
		'state_clause' => array(
			'key'   => 'state',
			'value' => 'Wisconsin',
		),
		'city_clause'  => array(
			'key'     => 'city',
			'compare' => 'EXISTS',
		),
	),
	'orderby'    => array(
		'city_clause'  => 'ASC',
		'state_clause' => 'DESC',
	),
) );

Date parameters

Simple date vars match one fixed period:

  • year (int): four-digit year.
  • monthnum (int): month, 1 to 12.
  • w (int): week of the year, 0 to 53 (MySQL WEEK semantics, influenced by the start_of_week option).
  • day (int): day of the month, 1 to 31.
  • hour (int): 0 to 23.
  • minute (int): 0 to 60.
  • second (int): 0 to 60.
  • m (int): combined year and month, e.g. 202607.

date_query (array) is the flexible form, parsed by [WP_Date_Query](/reference/classes/wp_date_query). Like tax_query and meta_query it is an array of clause arrays, with an optional top-level relation of AND (default) or OR. Each clause accepts:

  • year, month, week, day, hour, minute, second (int): fixed components, as above (also dayofweek, dayofweek_iso, dayofyear).
  • after (string or array): only posts after this date. Takes a strtotime()-compatible string or an array with year, month, day keys.
  • before (string or array): only posts before this date, same formats.
  • inclusive (bool): whether after/before boundaries match exactly. Default false.
  • compare (string): comparison operator for the fixed components, e.g. =, >, <=, BETWEEN, IN.
  • column (string): which date column to test. Default post_date; also post_date_gmt, post_modified, post_modified_gmt.
// Posts from December 12, 2012.
$q = new WP_Query( array(
	'date_query' => array(
		array(
			'year'  => 2012,
			'month' => 12,
			'day'   => 12,
		),
	),
) );

// Business hours on weekdays only.
$q = new WP_Query( array(
	'date_query'     => array(
		array( 'hour' => 9,  'compare' => '>=' ),
		array( 'hour' => 17, 'compare' => '<=' ),
		array( 'dayofweek' => array( 2, 6 ), 'compare' => 'BETWEEN' ),
	),
	'posts_per_page' => -1,
) );

// A date range.
$q = new WP_Query( array(
	'date_query' => array(
		array(
			'after'     => 'January 1st, 2026',
			'before'    => array(
				'year'  => 2026,
				'month' => 2,
				'day'   => 28,
			),
			'inclusive' => true,
		),
	),
) );

Boundary gotcha: a date-only string in before resolves to midnight (00:00:00) of that day, so that day's posts are excluded even with inclusive true. Include a time ('2026-02-28 23:59:59') or use the array form, which inclusive adjusts correctly.

Clauses can target different columns, for example published over a year ago but edited recently:

$q = new WP_Query( array(
	'date_query' => array(
		array(
			'column' => 'post_date_gmt',
			'before' => '1 year ago',
		),
		array(
			'column' => 'post_modified_gmt',
			'after'  => '1 month ago',
		),
	),
) );

date_query clauses can be nested with inner relation keys, the same shape as nested tax_query groups.

Custom field (post meta) parameters

The simple form matches one condition:

  • meta_key (string): custom field key.
  • meta_value (string): custom field value (string comparison).
  • meta_value_num (number): custom field value compared numerically.
  • meta_compare (string): operator for the simple form: = (default), !=, >, >=, <, <=, LIKE, NOT LIKE, IN, NOT IN, BETWEEN, NOT BETWEEN, NOT EXISTS, REGEXP, NOT REGEXP, RLIKE.
  • meta_type (string): cast type used when this key participates in orderby.

meta_query (array) is the full mechanism, parsed by [WP_Meta_Query](/reference/classes/wp_meta_query). It is an array of clause arrays with an optional top-level relation of AND (default) or OR. Each clause accepts:

  • key (string): custom field key.
  • value (string or array): value to compare. Must be an array only for IN, NOT IN, BETWEEN, NOT BETWEEN. Omit it for EXISTS and NOT EXISTS.
  • compare (string): = (default), !=, >, >=, <, <=, LIKE, NOT LIKE, IN, NOT IN, BETWEEN, NOT BETWEEN, EXISTS, NOT EXISTS, REGEXP, NOT REGEXP, RLIKE.
  • type (string): cast for the comparison: CHAR (default), NUMERIC, BINARY, DATE, DATETIME, DECIMAL, SIGNED, TIME, UNSIGNED. DATE works with BETWEEN only when values are stored and compared as YYYY-MM-DD.
// Simple form: key and value together.
$q = new WP_Query( array(
	'meta_key'   => 'color',
	'meta_value' => 'blue',
) );

// Numeric comparison needs meta_value_num or a type cast;
// as strings, '99' sorts greater than '100'.
$q = new WP_Query( array(
	'post_type'    => 'product',
	'meta_key'     => 'price',
	'meta_value'   => '22',
	'meta_compare' => '<=',
) );

meta_query always takes an array of arrays, even for a single clause:

$q = new WP_Query( array(
	'post_type'  => 'product',
	'meta_query' => array(
		array(
			'key'     => 'color',
			'value'   => 'blue',
			'compare' => 'NOT LIKE',
		),
	),
) );

Multiple clauses with a relation:

// color NOT LIKE blue OR price BETWEEN 20 and 100.
$q = new WP_Query( array(
	'post_type'  => 'product',
	'meta_query' => array(
		'relation' => 'OR',
		array(
			'key'     => 'color',
			'value'   => 'blue',
			'compare' => 'NOT LIKE',
		),
		array(
			'key'     => 'price',
			'value'   => array( 20, 100 ),
			'type'    => 'NUMERIC',
			'compare' => 'BETWEEN',
		),
	),
) );

Named clauses: give each clause a string key instead of a numeric index, and those names become valid orderby targets. This is the only way to order by multiple meta fields (see the Order and orderby section for a full example).

EXISTS and NOT EXISTS test for the key's presence regardless of value, useful for "has this field been set at all" queries:

$q = new WP_Query( array(
	'meta_query' => array(
		array(
			'key'     => 'featured_image_alt',
			'compare' => 'NOT EXISTS',
		),
	),
) );

Permission parameters

  • perm (string): intersect post_status with the current user's capabilities. 'readable' keeps only statuses the user can actually read (so private posts show only to users with read_private_posts); 'editable' keeps statuses the user can edit.
// Public posts plus private ones the user is allowed to see.
$q = new WP_Query( array(
	'post_status' => array( 'publish', 'private' ),
	'perm'        => 'readable',
) );

Mime type parameters

  • post_mime_type (string or array): restrict attachments by MIME type. Accepts full types (image/gif), wildcards (image, image/*), or an array of types. Only meaningful with post_type => 'attachment', which also needs post_status => 'inherit'.
// All GIF attachments.
$q = new WP_Query( array(
	'post_type'      => 'attachment',
	'post_status'    => 'inherit',
	'post_mime_type' => 'image/gif',
) );

There is no "not this MIME type" operator; to exclude types, build the allowed list yourself, for example by diffing get_allowed_mime_types() against the types you want removed and passing the remainder as an array.

Caching parameters

  • cache_results (bool): whether to cache the fetched posts. Default true. Since WordPress 6.1 the query itself is also cached (the resulting IDs are stored in the object cache keyed by the query), so repeated identical queries can skip the database; setting this to false opts a query out.
  • update_post_meta_cache (bool): whether to prime the postmeta cache for the results. Default true.
  • update_post_term_cache (bool): whether to prime the term cache for the results. Default true.
  • lazy_load_term_meta (bool): whether term meta for the results should be lazily loaded on first access. Default matches update_post_term_cache.

Leave these alone in normal code; priming caches is what prevents the classic N+1 query problem inside loops. Turn the meta and term caches off only when you know the loop touches neither, for example a bare list of titles and permalinks:

$q = new WP_Query( array(
	'posts_per_page'         => 50,
	'update_post_meta_cache' => false,
	'update_post_term_cache' => false,
) );

Return fields parameter

  • fields (string): shape of the returned results. 'all' (default) returns full WP_Post objects. 'ids' returns a flat array of post IDs. 'id=>parent' returns objects containing only ID and post_parent. Any other value falls back to 'all'.
// Just the IDs, cheap and cache-friendly.
$q   = new WP_Query( array(
	'post_type'      => 'post',
	'posts_per_page' => 100,
	'fields'         => 'ids',
) );
$ids = $q->posts; // array of integers

With 'ids' or 'id=>parent' there is nothing for the loop methods to set up, so iterate $q->posts directly instead of using have_posts(). ID-only queries pair naturally with the caching parameters above: skipping full objects, meta, and terms makes large scans far cheaper.

// Map every page to its parent, e.g. to build a tree.
$q = new WP_Query( array(
	'post_type'      => 'page',
	'posts_per_page' => -1,
	'fields'         => 'id=>parent',
) );
foreach ( $q->posts as $row ) {
	// $row->ID, $row->post_parent
}

Combining parameters

All top-level parameters AND together: post_type plus cat plus s returns posts of that type, in that category, matching that search. The nested query structures then let you express OR logic and grouping inside each dimension.

Multiple taxonomies

One tax_query can span any number of taxonomies. The top-level relation joins the clauses:

// In the 'quotes' category OR having the quote post format.
$q = new WP_Query( array(
	'post_type' => 'post',
	'tax_query' => array(
		'relation' => 'OR',
		array(
			'taxonomy' => 'category',
			'field'    => 'slug',
			'terms'    => array( 'quotes' ),
		),
		array(
			'taxonomy' => 'post_format',
			'field'    => 'slug',
			'terms'    => array( 'post-format-quote' ),
		),
	),
) );

Nested tax_query groups

A clause position can hold a whole sub-group with its own relation, letting you mix AND and OR:

// 'quotes' category OR (quote format AND 'wisdom' category).
$q = new WP_Query( array(
	'post_type' => 'post',
	'tax_query' => array(
		'relation' => 'OR',
		array(
			'taxonomy' => 'category',
			'field'    => 'slug',
			'terms'    => array( 'quotes' ),
		),
		array(
			'relation' => 'AND',
			array(
				'taxonomy' => 'post_format',
				'field'    => 'slug',
				'terms'    => array( 'post-format-quote' ),
			),
			array(
				'taxonomy' => 'category',
				'field'    => 'slug',
				'terms'    => array( 'wisdom' ),
			),
		),
	),
) );

meta_query and date_query nest the same way: replace a clause with an array that has its own relation and inner clauses.

// color = orange OR (color = red AND size = small).
$q = new WP_Query( array(
	'post_type'  => 'product',
	'meta_query' => array(
		'relation' => 'OR',
		array(
			'key'   => 'color',
			'value' => 'orange',
		),
		array(
			'relation' => 'AND',
			array(
				'key'   => 'color',
				'value' => 'red',
			),
			array(
				'key'   => 'size',
				'value' => 'small',
			),
		),
	),
) );

Taxonomy and meta together

tax_query and meta_query coexist in one query and AND together, each keeping its own internal relation:

// Products in the 'outdoor' category priced 50 or less, cheapest first.
$q = new WP_Query( array(
	'post_type'  => 'product',
	'tax_query'  => array(
		array(
			'taxonomy' => 'product_cat',
			'field'    => 'slug',
			'terms'    => 'outdoor',
		),
	),
	'meta_query' => array(
		'price_clause' => array(
			'key'     => 'price',
			'value'   => 50,
			'type'    => 'NUMERIC',
			'compare' => '<=',
		),
	),
	'orderby'    => array( 'price_clause' => 'ASC' ),
) );

Every taxonomy clause and meta clause adds a JOIN, so deeply combined queries get expensive. Keep clause counts sensible, add no_found_rows when you do not paginate, and consider caching the result of genuinely heavy queries.

Properties and methods

Everything the query learned is exposed on the object. The Properties table further down this page lists them all; the ones you will reach for most are $posts (the results), $post_count and $found_posts (this page's count vs. total matches), $max_num_pages, $query_vars (the parsed vars), $request (the generated SQL), and the $is_* flags that classify the query ($is_home, $is_single, $is_archive, and the rest). Read the flags through their method counterparts ([is_home()](/reference/classes/wp_query/is_home), [is_singular()](/reference/classes/wp_query/is_singular), and so on) rather than the raw properties, and prefer [get()](/reference/classes/wp_query/get)/[set()](/reference/classes/wp_query/set) over editing $query_vars directly.

The Methods table below covers the full API: the loop methods ([have_posts()](/reference/classes/wp_query/have_posts), [the_post()](/reference/classes/wp_query/the_post), [rewind_posts()](/reference/classes/wp_query/rewind_posts)), the comment loop equivalents, the conditional methods mirroring each $is_* flag, [get_queried_object()](/reference/classes/wp_query/get_queried_object) for the term, author, or post an archive represents, and [is_main_query()](/reference/classes/wp_query/is_main_query), the check that belongs in every pre_get_posts callback.

Code examples

Every example is editable and runs in a real WordPress booted in your browser by WordPress Playground. Press Run, then edit the code: clicking away re-runs it. Nothing is sent anywhere until you do.

Query by a hierarchical taxonomy term

tax_query selects on terms, and include_children means a parent term also returns posts in its children.

$q = new WP_Query( array(
	'post_type' => 'post',
	'tax_query' => array(
		array(
			'taxonomy'         => 'topic',
			'field'            => 'slug',
			'terms'            => 'engineering',
			'include_children' => true,
		),
	),
) );

echo "matched {$q->found_posts} post(s) under Engineering\n\n";

while ( $q->have_posts() ) {
	$q->the_post();
	echo '- ', get_the_title(), "\n";
}

wp_reset_postdata();

Set include_children to false to match only posts on the parent term itself.

Filter by a custom field with meta_query

meta_query selects on post meta and, unlike meta_key alone, supports comparisons and multiple clauses.

$priced = new WP_Query( array(
	'post_type'  => 'post',
	'meta_query' => array(
		array( 'key' => 'price', 'compare' => 'EXISTS' ),
	),
) );

echo "matched {$priced->found_posts} post(s)\n\n";

while ( $priced->have_posts() ) {
	$priced->the_post();
	echo get_the_title(), ' costs ', get_post_meta( get_the_ID(), 'price', true ), "\n";
}

wp_reset_postdata();

Every clause joins wp_postmeta, so keep the clause count low on large sites.

Common problems and fixes · 6

Why does my custom WP_Query break the rest of the page?

Calling $query->the_post() overwrites the global $post and the global post data that template tags read from. When your loop ends, the rest of the page is still looking at the last post your query returned, so the title, permalink and content of the surrounding template come out wrong. Call wp_reset_postdata() immediately after the loop to restore the globals to the main query's current post. This is only needed when you used the_post(); a loop that reads $query->posts directly never touches the globals.

Why is pagination not working in my secondary loop?

A secondary WP_Query does not read the page number for you. Pass it explicitly with 'paged' => get_query_var( 'paged' ) on most archives, and get_query_var( 'page' ) when the loop is on a static front page, because WordPress uses a different query var there. If page 2 gives you a 404 instead of results, the problem is upstream of the query: the main query is 404ing before your loop runs, which usually means the URL is a static page rather than an archive.

Should I use a new WP_Query or the pre_get_posts hook?

They do different things. new WP_Query runs an additional database query and leaves the main one alone, which is what you want for a sidebar, a related-posts block, or anything secondary. pre_get_posts modifies the query WordPress was already going to run, which is what you want to change an archive, a search page, or the number of posts on the blog index. Replacing the main query with a second WP_Query in a template is the common mistake: it doubles the queries and breaks pagination and body classes. Inside pre_get_posts, always guard with if ( ! is_admin() && $query->is_main_query() ).

What does posts_per_page => -1 actually cost?

It removes the limit, so the query returns every matching row and WordPress instantiates a WP_Post object for each one and primes the meta and term caches for all of them. On a site with a few hundred posts that is unnoticeable; on one with tens of thousands it is a memory exhaustion waiting for the day the content grows. Set a real ceiling you are willing to render. If you genuinely need everything, ask for 'fields' => 'ids' so no post objects are built.

How do I stop WP_Query running the extra count query?

By default the query calculates the total number of matching rows so that pagination can be rendered, which on large tables is often slower than fetching the posts themselves. When a loop does not paginate, set 'no_found_rows' => true. Related switches are 'update_post_meta_cache' and 'update_post_term_cache', which you can set to false when you know you will not read meta or terms from the results.

Why is my meta_query slow?

Each meta clause adds a join against wp_postmeta, and while meta_key is indexed, meta_value is a longtext that is not usefully indexed. Filtering thousands of posts by a meta value therefore scans. If the value is something you filter or archive by rather than merely display, model it as a taxonomy instead: term queries hit indexed integer columns and stay fast as the table grows. Where meta is the right model, keep the clause count low and make the meta_key as selective as possible.

Alternatives and related functions

get_posts
When you want an array of posts and nothing else, with no loop, no globals to reset and no pagination.
pre_get_posts
When the goal is to change what the page is already showing, rather than to run a second query alongside it.
WP_Term_Query
When you are querying categories, tags or custom taxonomy terms rather than posts.
WP_User_Query
When you are querying users, including by role, meta or the posts they have authored.
WP_Comment_Query
When you are querying comments, which have their own table and their own set of arguments.

Hooks and filters fired · 54

Every hook that fires from inside WP_Query, in the order it appears in the class, grouped by the method that fires it.

WP_Query::get_posts()

Properties · 58

$queryarraypublic
Query vars set by the user.
$query_varsarraypublic
Query vars, after parsing.
$tax_queryWP_Tax_Query|nullpublic
Taxonomy query, as passed to get_tax_sql().
$meta_queryWP_Meta_Querypublic
Metadata query container.
$date_queryWP_Date_Querypublic
Date query container.
$queried_objectWP_Term|WP_Post_Type|WP_Post|WP_User|nullpublic
Holds the data for a single object that is queried.
$queried_object_idintpublic
The ID of the queried object.
$requeststringpublic
SQL for the database query.
$postsWP_Post[]|int[]public
Array of post objects or post IDs.
$post_countintpublic
The number of posts for the current query.
$current_postintpublic
Index of the current item in the loop.
$before_loopboolpublic
Whether the caller is before the loop.
$in_the_loopboolpublic
Whether the loop has started and the caller is in the loop.
$postWP_Post|nullpublic
The current post.
$commentsWP_Comment[]public
The list of comments for current post.
$comment_countintpublic
The number of comments for the posts.
$current_commentintpublic
The index of the comment in the comment loop.
$commentWP_Commentpublic
Current comment object.
$found_postsintpublic
The number of found posts for the current query.
$max_num_pagesintpublic
The number of pages.
$max_num_comment_pagesintpublic
The number of comment pages.
$is_singleboolpublic
Signifies whether the current query is for a single post.
$is_previewboolpublic
Signifies whether the current query is for a preview.
$is_pageboolpublic
Signifies whether the current query is for a page.
$is_archiveboolpublic
Signifies whether the current query is for an archive.
$is_dateboolpublic
Signifies whether the current query is for a date archive.
$is_yearboolpublic
Signifies whether the current query is for a year archive.
$is_monthboolpublic
Signifies whether the current query is for a month archive.
$is_dayboolpublic
Signifies whether the current query is for a day archive.
$is_timeboolpublic
Signifies whether the current query is for a specific time.
$is_authorboolpublic
Signifies whether the current query is for an author archive.
$is_categoryboolpublic
Signifies whether the current query is for a category archive.
$is_tagboolpublic
Signifies whether the current query is for a tag archive.
$is_taxboolpublic
Signifies whether the current query is for a taxonomy archive.
$is_feedboolpublic
Signifies whether the current query is for a feed.
$is_comment_feedboolpublic
Signifies whether the current query is for a comment feed.
$is_trackbackboolpublic
Signifies whether the current query is for trackback endpoint call.
$is_homeboolpublic
Signifies whether the current query is for the site homepage.
$is_privacy_policyboolpublic
Signifies whether the current query is for the Privacy Policy page.
$is_404boolpublic
Signifies whether the current query couldn't find anything.
$is_embedboolpublic
Signifies whether the current query is for an embed.
$is_pagedboolpublic
Signifies whether the current query is for a paged result and not for the first page.
$is_adminboolpublic
Signifies whether the current query is for an administrative interface page.
$is_attachmentboolpublic
Signifies whether the current query is for an attachment page.
$is_singularboolpublic
Signifies whether the current query is for an existing single post of any post type (post, attachment, page, custom post types).
$is_robotsboolpublic
Signifies whether the current query is for the robots.txt file.
$is_faviconboolpublic
Signifies whether the current query is for the favicon.ico file.
$is_posts_pageboolpublic
Signifies whether the current query is for the page_for_posts page.
$is_post_type_archiveboolpublic
Signifies whether the current query is for a post type archive.
$query_vars_hashbool|stringprivate
Stores the ->query_vars state like md5(serialize( $this->query_vars ) ) so we know whether we have to re-parse because something has changed
$query_vars_changedboolprivate
Whether query vars have changed since the initial parse_query() call. Used to catch modifications to query vars made via pre_get_posts hooks.
$thumbnails_cachedboolpublic
Set if post thumbnails are cached
$allow_query_attachment_by_filenameboolprotected
Controls whether an attachment query should include filenames or not.
$stopwordsarrayprivate
Cached list of search stopwords.
$compat_fieldsprivate
$compat_methodsprivate
$query_cache_keystringprivate
The cache key generated by the query.

Methods · 68

  • init_query_flags()Resets query flags to false.
  • init()Initiates object properties and sets default values.
  • parse_query_vars()Reparses the query vars.
  • fill_query_vars()Fills in the query variables, which do not exist within the parameter.
  • parse_query()Parses a query string and sets query type booleans.
  • parse_tax_query()Parses various taxonomy related query vars.
  • parse_search()Generates SQL for the WHERE clause based on passed search terms.
  • parse_search_terms()Checks if the terms are suitable for searching.
  • get_search_stopwords()Retrieves stopwords used when parsing search terms.
  • parse_search_order()Generates SQL for the ORDER BY condition based on passed search terms.
  • parse_orderby()Converts the given orderby alias (if allowed) to a properly-prefixed value.
  • parse_order()Parse an 'order' query variable and cast it to ASC or DESC as necessary.
  • set_404()Sets the 404 property and saves whether query is feed.
  • get()Retrieves the value of a query variable.
  • set()Sets the value of a query variable.
  • get_posts()Retrieves an array of posts based on query variables.
  • set_found_posts()Sets up the amount of found posts and the number of pages (if limit clause was used) for the current query.
  • next_post()Sets up the next post and iterate current post index.
  • the_post()Sets up the current post.
  • have_posts()Determines whether there are more posts available in the loop.
  • rewind_posts()Rewinds the posts and resets post index.
  • next_comment()Iterates current comment index and returns WP_Comment object.
  • the_comment()Sets up the current comment.
  • have_comments()Determines whether there are more comments available.
  • rewind_comments()Rewinds the comments, resets the comment index and comment to first.
  • query()Sets up the WordPress query by parsing query string.
  • get_queried_object()Retrieves the currently queried object.
  • get_queried_object_id()Retrieves the ID of the currently queried object.
  • __construct()Constructor.
  • __get()Makes private properties readable for backward compatibility.
  • __isset()Makes private properties checkable for backward compatibility.
  • __call()Makes private/protected methods readable for backward compatibility.
  • is_archive()Determines whether the query is for an existing archive page.
  • is_post_type_archive()Determines whether the query is for an existing post type archive page.
  • is_attachment()Determines whether the query is for an existing attachment page.
  • is_author()Determines whether the query is for an existing author archive page.
  • is_category()Determines whether the query is for an existing category archive page.
  • is_tag()Determines whether the query is for an existing tag archive page.
  • is_tax()Determines whether the query is for an existing custom taxonomy archive page.
  • is_comments_popup()Determines whether the current URL is within the comments popup window.
  • is_date()Determines whether the query is for an existing date archive.
  • is_day()Determines whether the query is for an existing day archive.
  • is_feed()Determines whether the query is for a feed.
  • is_comment_feed()Determines whether the query is for a comments feed.
  • is_front_page()Determines whether the query is for the front page of the site.
  • is_home()Determines whether the query is for the blog homepage.
  • is_privacy_policy()Determines whether the query is for the Privacy Policy page.
  • is_month()Determines whether the query is for an existing month archive.
  • is_page()Determines whether the query is for an existing single page.
  • is_paged()Determines whether the query is for a paged result and not for the first page.
  • is_preview()Determines whether the query is for a post or page preview.
  • is_robots()Determines whether the query is for the robots.txt file.
  • is_favicon()Determines whether the query is for the favicon.ico file.
  • is_search()Determines whether the query is for a search.
  • is_single()Determines whether the query is for an existing single post.
  • is_singular()Determines whether the query is for an existing single post of any post type (post, attachment, page, custom post types).
  • is_time()Determines whether the query is for a specific time.
  • is_trackback()Determines whether the query is for a trackback endpoint call.
  • is_year()Determines whether the query is for an existing year archive.
  • is_404()Determines whether the query is a 404 (returns no results).
  • is_embed()Determines whether the query is for an embedded post.
  • is_main_query()Determines whether the query is the main query.
  • setup_postdata()Sets up global post data.
  • generate_postdata()Generates post data.
  • generate_cache_key()Generates cache key.
  • reset_postdata()After looping through a nested query, this function restores the $post global to the current post in this query.
  • lazyload_term_meta()Lazyloads term meta for posts in the loop.
  • lazyload_comment_meta()Lazyloads comment meta for comments in the loop.

Source code

#[AllowDynamicProperties]class WP_Query { 	/**	 * Query vars set by the user.	 *	 * @since 1.5.0	 * @var array	 */	public $query; 	/**	 * Query vars, after parsing.	 *	 * @since 1.5.0	 * @var array	 */	public $query_vars = array(); 	/**	 * Taxonomy query, as passed to get_tax_sql().	 *	 * @since 3.1.0	 * @var WP_Tax_Query|null A taxonomy query instance.	 */	public $tax_query; 	/**	 * Metadata query container.	 *	 * @since 3.2.0	 * @var WP_Meta_Query A meta query instance.	 */	public $meta_query = false; 	/**	 * Date query container.	 *	 * @since 3.7.0	 * @var WP_Date_Query A date query instance.	 */	public $date_query = false; 	/**	 * Holds the data for a single object that is queried.	 *	 * Holds the contents of a post, page, category, attachment.	 *	 * @since 1.5.0	 * @var WP_Term|WP_Post_Type|WP_Post|WP_User|null	 */	public $queried_object; 	/**	 * The ID of the queried object.	 *	 * @since 1.5.0	 * @var int	 */	public $queried_object_id; 	/**	 * SQL for the database query.	 *	 * @since 2.0.1	 * @var string	 */	public $request; 	/**	 * Array of post objects or post IDs.	 *	 * @since 1.5.0	 * @var WP_Post[]|int[]	 */	public $posts; 	/**	 * The number of posts for the current query.	 *

Changelog

Introduced in 1.5.0. One change between 6.7.7 and 7.1.0.

  1. 6.7.7
  2. 6.8.8
  3. 6.9.7
  4. 7.0.4
  5. 7.1.0

Signature, return type and hooks compared across 5 parsed releases.

7.1.0
Method is_sitemap() added.verified against source
4.5.0
Removed the $comments_popup property.from the docblock
1.5.0
Introduced.from the docblock

About this page

Parsed data
Generated from the wordpress-develop 6.9.7 tag, from src/wp-includes/class-wp-query.php, and regenerated for each WordPress release so it tracks the code rather than a snapshot of it.
Corrections
Something wrong on this page? Report it and it gets fixed in the next regeneration.