What is WP_Query?
WP_Query is a PHP class that retrieves posts from the WordPress database based on parameters you set. It lets you control which posts or pages WordPress fetches and displays (by category, date, author, and more) without writing SQL yourself. Developers use WP_Query in theme and plugin code to build custom loops that fetch specific posts.
More About WP_Query
WP_Query is written in PHP, the programming language WordPress runs on, and it talks to the WordPress database for you: you describe the posts you want as an array of arguments, and the class builds and runs the SQL. WordPress runs WP_Query itself on every front-end page load, including searches, to fetch the right content for the requested URL.
The results feed The Loop, the code your theme uses to step through posts and print them. Since The Loop runs through whatever posts the query returns, changing the WP_Query arguments changes what content appears on the page. How that content looks is still controlled by your theme’s templates. By writing a custom loop with your own arguments, you can show visitors a specific group of posts without making them search for it: everything in one category, by one author, from one date range, or your most-commented pieces.
WP_Query can be helpful when you want to display posts from a custom post type or build a new page display: set its post_type parameter to the type’s slug. Creating the type itself is separate work, done with the register_post_type() function.
The parameters WP_Query accepts
WP_Query accepts parameters for a post’s type and status, its categories and taxonomies, author, dates, keywords, custom fields, sort order, and pagination: 18 families in all, each documented in the WP_Query class reference. The ones you’ll reach for first:
- post_type and post_status: which kind of content to fetch, and whether drafts count.
- cat, tag, and tax_query: posts in a category, with a tag, or in any custom taxonomy term.
- author: posts by one or more authors.
- date_query: posts from one date or a date range.
- s: posts matching a keyword search.
- meta_key, meta_value, and meta_query: posts filtered by a custom field.
- orderby and order: the sort, by date, title, comment count, and more.
- posts_per_page and paged: how many posts to fetch, and which page of results.
The main query and your custom queries
On each request, WordPress parses the URL into a query, builds one main WP_Query instance (the global $wp_query object), and uses it to load the page’s primary content. That object’s is_* properties (is_single, is_page, is_search, is_category, and more than 20 others) record what kind of request is being served, and they’re what WordPress conditional tags check. Any WP_Query you create yourself runs alongside it as a secondary query, and the arguments you pass do the same job as the query variables WordPress reads from the URL.
A basic WP_Query loop
A basic WP_Query loop takes 4 steps: create the query, check $query->have_posts(), call $query->the_post() on each pass, and finish with wp_reset_postdata(). Suppose you want to highlight popular content by pulling your 5 most-commented posts: in your arguments array, set 'orderby' => 'comment_count' to sort by comment count and 'posts_per_page' => 5 to cap the list, then follow the standard sequence from the WP_Query class reference:

- Create the query and keep a reference to it:
$query = new WP_Query( $args ). - Check
$query->have_posts()to confirm the query found anything. - Loop while
$query->have_posts()is true, calling$query->the_post()on each pass. That sets up the global $post object that template tags like get_the_title() rely on. - After the loop, call
wp_reset_postdata()to point template tags back at the main query’s post.
Two details trip people up. Call have_posts() and the_post() as methods on your own object, as above: the bare functions address the main query, so a custom loop written with them steps through the wrong posts. And step 4 is the one developers forget, a classic WP_Query bug: skip wp_reset_postdata() and every template tag after your loop keeps reading your custom query’s last post instead of the page’s own content.
WP_Query vs. get_posts() vs. query_posts()
WordPress gives you 3 main ways to fetch a list of posts, and they aren’t interchangeable:
- WP_Query is the full class: your own loop, pagination properties like max_num_pages, and conditional methods. Use it for secondary loops in theme templates.
- get_posts() runs WP_Query for you and returns a plain array of post objects, with no loop setup. It’s the simplest choice for a short list, and the class reference recommends it for admin-side queries, where wp_reset_postdata() may not behave as expected.
- query_posts() throws out the main query and replaces it. Its own documentation says it isn’t intended for plugins or themes and can more than double a page’s query work in the worst case. Avoid it: alter the main query with pre_get_posts instead.
When to change the main query instead
To change what an archive, a search results page, or the blog home displays, don’t run a second query: WordPress has already run the main one by the time your template loads. Use the pre_get_posts hook, which fires after the main query’s variables are set but before it hits the database, so you can adjust its arguments in place. The decision rule:
- Use pre_get_posts to change what an existing page shows: filter a search, resize an archive.
- Use a new WP_Query for extra secondary loops: related posts, featured sections, or sidebar widgets.
Inside a pre_get_posts callback, check $query->is_main_query() first, or you’ll rewrite every query on the page, custom loops included. When you write your first custom query, start from the Standard Loop example in the class reference and change one argument at a time.
Frequently Asked Questions
Can WP_Query fetch pages and custom post types, or only posts?
Yes. WP_Query can fetch pages and any registered custom post type. Its default post_type is 'post': set it to 'page' to query pages, or to a custom type's slug. For users, comments, or taxonomy terms, WordPress provides separate classes: WP_User_Query, WP_Comment_Query, and WP_Term_Query.
Is WP_Query faster than get_posts()?
Not meaningfully. get_posts() runs WP_Query internally, so the database work is nearly identical. get_posts() does set no_found_rows to true, skipping the total-match count used for pagination, which trims a little work when you only need a flat list.
Does WP_Query cache its results?
Yes, since WordPress 6.1 (2022). WP_Query caches database query results by default: run the same query twice and the second is served from cache, not fresh SQL, with the biggest gains on sites using persistent object caching. Set cache_results to false to opt out per query.
How does WP_Query paginate results?
Set posts_per_page for the page size and paged for the page number. After the query runs, the max_num_pages property (found posts divided by posts per page) tells you how many pages of results exist.
Can WP_Query filter posts by custom field?
Yes. Use the meta_key and meta_value parameters for a simple match, or a meta_query array for multiple conditions with comparison operators. The class reference documents both under its custom field (post meta) parameters.