An In-Depth Guide to Conquering WP_Query

An In-Depth Guide to Conquering WP_Query

Getting posts from the WordPress database is one of my favorite topics. The flexibility of the WP_Query class – which allows you to do this – is awesome, it enables all the fancy CMS-like features you see on the front-end.

Do you need to list all tasks entered by a certain user between two specific dates? No problem! Do you want to list all scheduled posts and pages in a specific category, but only if it was modified less than two weeks ago? You can do that, too!

The WP_Query class is easy to understand and easy to implement. In this comprehensive tutorial – complete with lots of example code snippets – we’ll look at how you can use the class to make WordPress do your bidding.

What is a WordPress Query?

Let’s back up a bit and talk about queries. In general, when someone says query they mean a query to the database – we ask it for some information. This can be anything from all the phone numbers of all our users to all the categories created.

When referring to “a query” or “a WordPress query” we usually mean a query that retrieves some posts for us. Almost all WordPress pages create a query for you on their own. On the index page (the main blog view) WordPress queries the database for your latest posts. On a category archive page the latest posts within the category are retrieved. On author pages the latest posts from the author are pulled, and so on.

Even single posts and single pages use a query. In those cases they always return one result, but query it is nevertheless. So almost any default page you load contains a query that is performed automatically.

The Loop: How Queries Are Used

It’s one thing to retrieve a bunch of posts, and it’s a whole other matter to actually display them. Each theme uses different HTML and styling elements to do this, but what they all share is “the loop.” The loop simply refers to a WordPress mechanism that steps through the retrieved posts, enabling themes to display them. It looks something like this:

OK, so this isn’t just a loop, it’s also an if/else statement. It is a simplified example of what you might find on any page which lists a bunch of posts. First we use have_posts() to check if any posts are returned. Then, as long as there are posts we create an element for them and output the title and the content. The the_post() function is responsible for processing some more post data and incrementing the counter. At the end we display some text if there are no posts to show.

Before we move on, let’s clarify some terminology. The term “the loop” is used to describe the loop on the page which lists the posts pulled in by WordPress by default. Let’s look at an example to clarify this. If you scroll to the bottom of an article right here on WPMU DEV, you’ll see a related articles section:

related articles
Related articles on our blog.

This section lists posts and uses a custom query and a loop to do so. This is not considered “the loop,” however, because the items within it aren’t the ones WordPress lists by default on this page. If you have multiple queries and loops on the same page they are usually referred to as secondary loops and custom queries.

Creating Our First Query

Let’s create our first custom query. Let’s say we want to list posts from a particular author in a particular category. Here’s one way to do that:

The first part of the code above deals with querying a database. As you can see I’ve instantiated a new WP_Query object by passing it some arguments. Don’t worry if you don’t understand the fancy object oriented terminology here because using it is very straightforward. I passed my user’s nicename as the author_name parameter and the category’s slug as the category_name parameter.

To list these posts we need to modify our loop slightly. Instead of using have_posts() and the_post() we use them as methods of the WP_Query object. What this means in practice is prepending $variable_name-> before these functions.

In future examples I won’t show the loop but the idea is always the same. If our variable is called $custom_posts you’ll need to use $custom_posts->have_posts() and $custom_posts->the_post(). Easy-peasy.

WP_Query Parameters

Creating custom queries is all about using the parameters. How to get posts from particular dates, authors, custom post types, custom taxonomies, statuses and more. Let’s take a look at all the parameters with some basic examples for each.

Author Parameters

There are four separate parameters you can use to grab authors. The author parameter accepts a single author ID or a comma separated list of author IDs. Using negative numbers will result in that author ID being excluded.

author_name is a parameter you can use if you want to pass the author’s nicename instead if his/her ID. Take care as this is not necessarily the same as his/her username.

The two remaining author-related parameters are author__in and author__not_in. These were added later in WordPress 3.7 enabling much-needed array support. With their help you can supply your author needs as arrays.

This doesn’t seem like much of a change from using the author parameter but from a modularization and best practices point of view it is a world apart.

Use Case: Listing Articles From Top Authors

Let’s take a look at how you could approach listing articles by top authors. Let’s assume that you have a rating system in place. Each author’s global rating is stored in the user meta table with the key of rating. We can combine the get_users()function and a custom loop to grab the posts of authors with the highest ratings.

Category Parameters

Restricting posts based on categories can be done with no less than five separate functions. The cat parameter behaves just like the author parameter from above, accepting integers to include and negative integers to exclude:

The category_name is horribly named because it actually accepts a category slug. You can usually guess a slug from the name by converting all letters to lowercase, omitting all special characters apart from underscores and dashes and turning spaces into dashes. A category named “Book Reviews” will likely have the slug: “book-reviews.”

You can use arrays of categories with three parameters. category__in and category__not_in behave just like their author counterparts. In addition to these you can use category__and, which will make sure that only posts that are assigned all categories given are retrieved.

Use Case: Get Articles from Your Most Used Categories

In this scenario we’ll retrieve a list of categories based on the item count within that category. We’ll feed this to our query to get posts from the top 3 most frequently used categories.

Tag Parameters

You’d think that tags would have the same set of arguments, and you would almost be right, but there are two extra here.

Since tag, tag_id, tag__and, tag__in and tag__not_in should be familiar by now, let’s look at some quick examples:

I’d like to point out two things. First of all, the WordPress core is inconsistent. category expects an ID, tag expects a slug, category_name actually expects the slug and so on. The takeaway here is that you should always strive to make things as consistent as possible but everyone makes mistakes. Sometimes you are forced to make them for legacy reasons, for example. Take criticism with a pinch of salt!

The second thing I wanted to point out was that you should always use array when possible. Even though the tag parameter allows you to handle multiple tags, I recommend using tag__in and the other array-based ones.

To make things even easier tags have an additional two parameters: tag_slug__and and tag_slug__in. Apparently there is no tag_slug__not_in. Don’t ask why! These behave as you would expect them to:

Use Case: Combining Tags, Categories And Authors

Since we know a few parameters now, why not start combining them? Let’s take two specific authors and show everything they’ve written that has been featured (by adding the “featured” tag to it) in the “Books” or “Movies” category.

Post Types

WordPress has a number of built-in post types: post, page, attachment, revision and navigation menu item. It has become commonplace to add custom post types for managing projects, your portfolio, products, forums and so on. Using the custom post type parameter post_type you can narrow down your posts based on post type.

When a string is passed to this parameter it will look for posts with the given post type only. If an array of post types is passed, posts will be returned for any matching post types.

Note that this parameter has a default value, which is post. If you want to return other post types you must specify them explicitly. If you are using the tax_query parameter (more on this later) the default value becomes any.

Post Status

Just like we did for post types we can specify a single post status or multiple post statuses to pull posts from. The post_status parameter takes a string if a single status is used or an array if you want to select from more than one. WordPress has eight built-in statuses:

  • publish – a published post or page.
  • pending – post is pending review.
  • draft – a post in draft status.
  • auto-draft – a newly created post, with no content.
  • future – a post to publish in the future.
  • private – not visible to users who are not logged in.
  • inherit – a revision. see get_children.
  • trash – post is in trashbin (available with Version 2.9).

You can use any to indicate that you want to include all post statuses. The default is publish. Make sure to specify if you want something else.

Use Case: Sneak Peeks Of Upcoming Products

If you run a bookstore and you know that a potential bestseller is coming out soon you could use a custom WordPress query to list them. No need to publish these posts and then use some special trickery to exclude them from view until they can be ordered, just list scheduled posts directly:

The Search Parameter

The horribly named s parameter takes a string, which is used to search within your posts. Make sure to URL encode your search string (you can use PHP’s urlencode function) to account for spaces and other special characters.

Use Case: Searching Post Subsets

A search of your whole content can be performed from the default search widget so using the s parameter is most useful when combined with other parameters. The following examples allow you to search various subsets of posts:

Password Protected Posts

In the publishing settings box you can choose to password protect a post. This is great for protecting private content or building a subscription model, perhaps. Whatever the case may be, you can use the has_password parameter to specify whether or not you want password protected posts listed.

If the value of this parameter is true the query will grab posts that have passwords associated with them. If false you will get only those posts that do not have passwords. If the parameter is set to null or omitted all posts will be shown. You can use the post_password to list posts that are protected by a specified password.

Use Case: Online Treasure Hunt

You can create a fun game using WordPress. Users must de-cypher a puzzle, getting a password for a list of posts, which contain clues to the final solution. The user must put the password in a form. Once submitted it takes the user to a page, which uses the submitted password as a query parameter, something like this:

By using the full loop from the beginning of the post you can make sure the user gets a “no posts found” message if the incorrect password is given. If the correct one has been entered, a list of posts will be shown protected by that password.

Including, Excluding and Targeting Specific Posts

There are nine separate parameters for grabbing one or more specific posts. p is the parameter you can use to grab one specific post-based on its ID. To use a page slug you can pass a string to the name parameter. page_id and pagename accomplish the same task, but for pages these two parameters are a bit confusing. If you pass the ID of a page to the p parameter, it will not work! On the other hand, even if you don’t set post_type specifically, page_id will work as expected.

Think of it like this: When you use p or name it is as if the post type was set to post. When you use page_id and pagename it is as if the post type was set to page.

The post__in and post__not_in params work similarly to the ones we saw for categories and tags. They accept an array of post IDs.

The post_parent parameter takes a single ID. Only posts, which are child posts of the given ID, will be listed. The post_parent__in and post_parent__not_in parameters take an array of post IDs – we’re used to this convention by now.

Keep the post type restrictions in mind when using these parameters. If you include the IDs of multiple post types you will need to specify any as the post type, or the exact types you are looking for.

Use Case: Get All Post Attachments

If you’d like to list all images and other attachments of a post you can use the parent parameter to your advantage. This is perfect for auto-generating galleries or download lists.

Taxonomy Queries

Now that you have a good understanding of parameters and how they are used, it’s time to look at a more advanced one: tax_query.

This parameter is itself an array, which you can use to create more complex conditions and use custom taxonomies. Let’s take a look at it through an example, courtesy of the WordPress Codex.

Our taxonomy query has two conditions and a relation. The relation dictates whether at least one condition (OR), or all conditions (AND) must be true. The conditions themselves are arrays that define the taxonomy restrictions.

The first condition states that the posts we are looking for must have the movie genre action or comedy assigned. The second condition states that we are looking for all posts, except for those associated with the three listed actors.

Since both conditions must be true (due to the AND restriction), this query will result in all action and comedy movies that star actors other than the ones listed.

Each condition has five parameters of its own:

  • taxonomy – where you specify the taxonomy slug
  • field – choose between term_id, name or slug
  • terms – use a single integer or string or an array of integers or strings
  • operator – how multiple terms are handled. IN will allow posts belonging to any of the listed terms. NOT IN will allow any posts that don’t include any of the listed terms. AND will only allow posts that include all of the listed terms>
  • include_children – Wether or not to include children for hierarchical taxonomies

Use Case: Advanced Content Filtering

Taxonomy filters have a wide range of uses. From dealing with custom product taxonomies to creating an IMDB-style movie database you can do a lot with them. Here’s another example that allows for advanced book filtering:

In this particular case the user wants to view all books which are a bit more upbeat (don’t contain the negative tags we’ve given) but they can’t be from the comedy or romance genres.

Meta Queries

Meta queries are similar to taxonomy queries in structure. They allow you to query posts based on their values in the post meta table. WordPress offers four parameters here which can be used (meta_key, meta_value, meta_value_num and meta_compare) but can be substituted with meta_query which is far better. Let’s look at an example:

This example shows a meta query which returns books that are over 500 pages in length, the syntax is quite easy after the taxonomy query. Let’s look at the subparameters you can use to specify what you want:

  • key – The key of the custom field
  • value – The value you are looking for
  • compare – The value comparison operator. Possible values are ‘=’, ‘!=’, ‘>’, ‘>=’, ‘<‘, ‘<=’, ‘LIKE’, ‘NOT LIKE’, ‘IN’, ‘NOT IN’, ‘BETWEEN’, ‘NOT BETWEEN’, ‘EXISTS’, and ‘NOT EXISTS’
  • type – The data type of the field. Possible values are ‘NUMERIC’, ‘BINARY’, ‘CHAR’, ‘DATE’, ‘DATETIME’, ‘DECIMAL’, ‘SIGNED’, ‘TIME’, ‘UNSIGNED’

Don’t forget the relation parameter, which you can use to indicate the relationship between the separate query clauses – it works just like in the taxonomy queries above.

Use Case: Posts With Featured Images

There are tons and tons of ways to use meta queries. One great example is grabbing only posts with featured images. The ID of the featured image is stored in the post meta table using the _thumbnail_id key. We can use a meta query to check for posts where the value of this meta key is not empty.

Date Queries

Dates have a similar history to meta queries. WP_Query offers 8 parameters to narrow down your post list by date, all of which can be replaced with the date_query. I’ll be looking at date_query exclusively because using it is better practice.

Date queries can be a bit more complex, let’s look at the parameters before we get into the examples. Here is the full list, courtesy of the WordPress Codex:

  • year – 4 digit year (e.g. 2011).
  • month – Month number (from 1 to 12).
  • week – Week of the year (from 0 to 53).
  • day – Day of the month (from 1 to 31).
  • hour – Hour (from 0 to 23).
  • minute – Minute (from 0 to 59).
  • second – Second (0 to 59).
  • after – Date to retrieve posts after. Accepts strtotime()-compatible string, or array of ‘year’, ‘month’, ‘day’ values:
    • year Accepts any four-digit year. Default is empty.
    • month The month of the year. Accepts numbers 1-12. Default: 12.
    • day The day of the month. Accepts numbers 1-31. Default: last day of month.
  • before – Date to retrieve posts before. Accepts strtotime()-compatible string, or array of ‘year’, ‘month’, ‘day’ values:
    • year Accepts any four-digit year. Default is empty.
    • month The month of the year. Accepts numbers 1-12. Default: 1.
    • day The day of the month. Accepts numbers 1-31. Default: 1.
  • inclusive – For after/before, whether exact value should be matched or not.
  • compare – See WP_Date_Query::get_compare().
  • column – Column to query against. Default: ‘post_date’.
  • relation – OR or AND, how the sub-arrays should be compared. Default: AND.

That’s a bit of a mouthful but it does allow us to use dates with considerable flexibility. By using the first seven parameters you can target date ranges all in one go:

To have more control over date ranges you can use the before and after parameters. You’ll need to specify the year/month-day as sub-array members. Use the inclusive parameter in conjunction to specify wether or not the boundaries should be taken into account.

This example (from the Codex) will return all posts between January 1st 2013 and February 28, 2013. Since inclusive is set to true, posts on the boundary date will also be returned.

Date functionality here is based on the WP_Date_Query class. I recommend giving it a read for a more thorough understanding – it can do quite a lot!

Use Case: Seasonal Past Posts

Many websites have seasonal content: Christmas posts, New Year’s roundups, Valentines Day stuff and so on. By using a custom date query you can add relevant time-sensitive content to your website from your archives.

The short example below will pull posts from April 1st, spreading April fool’s cheer perhaps?


I wouldn’t blame you for thinking that we’ll be looking at one parameter here or perhaps two. In reality there are no less than seven parameters for handling pagination.

The most basic one you will probably use is posts_per_page, which essentially determines the number of posts to pull from the database. Setting it to -1 will grab all available posts.

paged is a frequently used parameter, it determines which page of results show. It is used in conjunction with posts_per_page. If 10 posts are shown per page and the paged parameter is set to 4 your fourth set of 10 posts will be returned. nopaging allows you to ignore paging completely if it is set to true.

You may also find yourself in need of the offset parameter which works just like an offset in MySQL – it offsets the posts retrieved by the given amount. This tends to break pagination unless you know what you’re doing, I would use this sparingly.

If you find yourself bothered by sticky posts you can use ignore_sticky_posts to ignore them. By default sticky posts are placed at the top of the list, as well as the natural location they would be in. To disable this behaviour set this parameter to false.

The posts_per_archive_page parameter sets how many pages are shown on archive pages. These would be all pages where the is_archive() and is_search() functions return true. If this parameter isn’t used the posts_per_page parameter is used.

Finally, the page parameter shows the posts that would normally show up just on page X of a Static Front Page.

The example above returns the second set of 20 posts from the database while ignoring sticky posts.

Ordering Results

When listing your results, the order in which they are shown is just as important as what is returned. Two parameters give you control over this: order and orderby.

The orderby parameter determines the order in which posts are returned. There are quite a few options, let’s take a look:

  • none – No order (natural MySQL selection order)
  • ID – Order by post id
  • author – Order by author id
  • title – Order by title
  • name – Order by post slug.
  • type – Order by post type
  • date – Order by date
  • modified – Order by last modified date
  • parent – Order by parent id
  • rand – Random order
  • comment_count – Order by number of comments
  • menu_order – Order by Page Order
  • meta_value – Order by the meta value
  • meta_value_num – Order by numeric meta value
  • post__in – Preserve order given in the post__in parameter

Almost all of these are self explanatory, let’s take a more detailed look at ordering posts by meta value. To get this to work you need to specify the meta_key parameter as well so WordPress knows which key to pull meta values for. You should also take care to specify the correct type since ordering lead to unexpected results otherwise.

if you are ordering regular text you should be fine. If you are ordering numeric values, use meta_value_num instead of meta_value. Alternatively you can specify the meta_type parameter which allows you to specify the data type. See the meta_query section for data types you can use.

Finally, the order parameter determines the direction of the ordering. Use ASC for ascending, DESC for descending. Take a look at the examples below for a practical look at how ordering can be done for various use cases.

For the sake of thoroughness I thought I’d mention the option to order by multiple columns. Pre 4.0 you could do this using a space separated value for the orderby parameter, like this: orderby => 'title menu_order'. Since WordPress 4.0 this was improved, you can now use an array and specify the order separately:

Use Case: Order Products By Price

A frequently needed feature in eCommerce themes is the ability to order products by price. In the example below we look for products over $20 and list the results by price, lowest to highest:

Returning Values

It seems that the fields parameter is not well-known, even though it is one of the most useful features. You can pass two values to this parameter or omit it. If omitted you will end up with post objects as usual. If you pass ids an array of post IDs will be returned. If you pass id=>parent an associative array of ID – parent relationships will be returned.

The reason I think this is such a useful feature is optimization. Some plugins use WP_Query to grab post IDs for galleries or other shortcodes. Many do not utilize the return parameter and end up giving the server a lot more work to do – work which is never used in the code. If you just need IDs make sure to use the fields parameter.

Use Case: Programmatically Placing A Gallery

If you want a gallery after each of your posts, showing all the images in that post you can utilize a query in tandem with the gallery shortcode.

First, we grab all attachments from the post, making sure to return the IDs only. We then implode this array to make a string containing a comma separated list of IDs we can use in the shortcode. Finally we use do_shortcode() to create the gallery.


This one is a bit obscure and not implemented very modularly. The idea is that if you use the perm parameter and supply readable as the value, only posts which the current user can read will be shown. This is useful if you want to list regular and private posts but make sure to only list those private posts that the user has access to.


This one is for advanced optimization – it allows you to control the caching behaviour of queries. Generally it is a good idea to add results of a query to the cache, chances are it will be used again and again in the same form. Your list of top 10 posts is not likely to change from query-to-query for example.

There are two cases where you may want to control what is cached. If you list posts without needing their metadata or taxonomy data you can use update_post_meta_cache and update_post_term_cache to disable the respective cache. If you are just doing a one-off query for a quick test you can also use cache_results to disable caching of the post results themselves.

One instance where I’ve used this was creating a quick export table of all the posts in the database. The export was essentially a CSV file of all post IDs, titles, categories and tags associated with the posts. Caching this one-off query makes no sense, especially since there were 8,000 posts with lots and lots of meta data.

WP_Query Properties

So far we’ve been looking at the parameters we can pass to WP_Query. Since we are talking about a class here, not a simple function there are properties and methods we can use to manipulate it. The WP_Query Properties Documentation has a full list of properties. I’ll show you some of the more useful ones and how to use them, in case you aren’t familiar with properties.

I personally tend to use $query_vars, $found_posts, $post_count and $max_num_pages. $query_vars holds the array that was passed to the class, it contains all the parameters we used when making our query.

The $found_posts property holds the number of posts found in total that match our query. The $post_count property holds the number of posts currently being shown. These are usually not the same since pagination is being used. There may be 52 posts found, but only 10 are displayed currently.

Beware as the $post_count is also not necessarily the same as the value used in the posts_per_page argument. If 52 posts are found and 10 are shown per page the last page will look like this: 52 posts found, 10 posts displayed per page but currently, only two are being shown.

Finally, $max_num_pages holds the number of pages needed to display the results. This is basically the result of $found_posts / $posts_per_page rounded up to the next whole number.

WP_Query Methods

When talking about classes, properties are just like variables, methods are just like functions. WP_Query has a number of methods, although you won’t really need to use them directly in most cases. The two most important ones you already know are have_posts() and the_post(). Take a look back at the beginning of the article for how these work.

There are some useful functions like rewind_posts() which essentially resets the loop. You may want to use this if you are outputting the exact same list in two separate locations.

The get() and set() methods allow you to get and set a specific query variable. Again, these are usually not used by themselves only in special circumstances. Take a look at the Method Documentation for more information.

Data Safety: Validating, Sanitizing And Escaping

Custom queries are frequently paired with front-end forms. A typical scenario may be a custom-coded filter system allowing the user more access over the post list. Take a look at our Adding Post Filters To Your WordPress Site article for an introduction.

If you are accepting any form of user input you should always escape the data to prevent SQL injection attacks and other headaches. Take a look at the excellent Validating, Santitizing And Escaping User Data article in the Codex for more information about this topic.

Modifying the Raw SQL Query

There are a number of filters that allow you to hook into the actual SQL query that is performed upon the use of WP_Query. This is rarely needed so do try and work with the given parameters. More often than not (way more often) you can do everything you need since this class is pretty powerful.

Filters such as posts_where and posts_join allow you to modify specific clauses in your SQL statement. While WP_Query is flexible, it doesn’t currently allow for very complex WHERE clauses involving multiple taxonomies and multiple relations. In these cases you could use these filters to modify the query directly.

The WP_Query Filters section is a good place to start but you will need considerable SQL knowledge to utilize these filters properly.

Wrapping Up

This article hopefully gave you a better understanding of the WP_Query class and just how useful it is when dealing with post lists. Its use doesn’t stop at listing posts – you can combine the class with custom post types, taxonomies and meta data to build a powerful and useful CMS system.

If you’re interested, there are a few classes which work similarly or are used within WP_Query and other sections of WordPress. WP_Comment_Query can be used to grab comments in much the same way as WP_Query. WP_Meta_Query handles all the meta queries and WP_Date_Query handles all the date restrictions for both classes and will probably be used in upcoming parts of the WP ecosystem.