WP_Query

What WP_Query is

WP_Query is WordPress’s main class for querying posts from the database. It powers:

  • The main loop
  • Custom loops (recommended way)
  • Complex queries (meta, tax, date, pagination, etc.)

Use it instead of query_posts() (which you should basically never use).


Basic Usage Pattern

$args = [
    'post_type'      => 'post',
    'posts_per_page' => 10,
];

$query = new WP_Query($args);

if ($query->have_posts()) {
    while ($query->have_posts()) {
        $query->the_post();
        the_title();
    }
    wp_reset_postdata();
}

Key points

  • Always call wp_reset_postdata() after a custom loop
  • $query->the_post() sets up global $post

Common Query Arguments

Post Type

'post_type' => 'post' // or 'page', 'product', 'custom_post_type'

Multiple post types:

'post_type' => ['post', 'page']

Pagination

$paged = get_query_var('paged') ? get_query_var('paged') : 1;

$args = [
    'post_type'      => 'post',
    'posts_per_page' => 5,
    'paged'          => $paged,
];

Pagination links:

echo paginate_links([
    'total' => $query->max_num_pages,
]);

Ordering Results

'orderby' => 'date', // date | title | meta_value | rand
'order'   => 'DESC', // ASC | DESC

Order by meta value:

'meta_key' => 'price',
'orderby'  => 'meta_value_num',
'order'    => 'ASC',

Meta Queries (Custom Fields)

Single meta condition:

'meta_query' => [
    [
        'key'     => 'featured',
        'value'   => '1',
        'compare' => '=',
    ]
]

Multiple conditions:

'meta_query' => [
    'relation' => 'AND',
    [
        'key'     => 'price',
        'value'   => 100,
        'compare' => '>=',
        'type'    => 'NUMERIC',
    ],
    [
        'key'     => 'in_stock',
        'value'   => 'yes',
    ],
]

Taxonomy Queries

Basic taxonomy query:

'tax_query' => [
    [
        'taxonomy' => 'category',
        'field'    => 'slug',
        'terms'    => 'news',
    ],
]

Multiple taxonomies:

'tax_query' => [
    'relation' => 'AND',
    [
        'taxonomy' => 'category',
        'field'    => 'term_id',
        'terms'    => [1, 2],
    ],
    [
        'taxonomy' => 'post_tag',
        'field'    => 'slug',
        'terms'    => ['featured'],
    ],
]

Excluding / Including Posts

Exclude by ID:

'post__not_in' => [12, 34]

Include only specific posts:

'post__in' => [10, 20, 30],
'orderby'  => 'post__in',

Date Queries

'date_query' => [
    [
        'after'     => '2024-01-01',
        'before'    => '2024-12-31',
        'inclusive' => true,
    ],
]

Relative dates:

'date_query' => [
    [
        'after' => '1 week ago',
    ],
]

Checking Results Without Loop

if ($query->found_posts > 0) {
    echo $query->found_posts;
}

Access posts array directly:

$posts = $query->posts;

Performance Tips

Limit fields:

'fields' => 'ids'

Disable unnecessary counts:

'no_found_rows' => true

Don’t prime meta/term cache:

'update_post_meta_cache' => false,
'update_post_term_cache' => false,

Main Query vs Custom Query

Modify main query (archive, search, etc.):

add_action('pre_get_posts', function ($query) {
    if (!is_admin() && $query->is_main_query() && is_post_type_archive('product')) {
        $query->set('posts_per_page', 12);
    }
});

Never use WP_Query inside pre_get_posts.


Quick Debugging

echo '<pre>';
print_r($query->query_vars);
echo '</pre>';

Or log SQL:

add_filter('posts_request', function ($sql) {
    error_log($sql);
    return $sql;
});