WP_Query: Advanced Techniques and Common Patterns

WP_Query is the primary way to retrieve posts in WordPress. Beyond the basics, it supports complex taxonomy queries, date ranges, relationship queries via post meta, multiple post types, and ordering by custom fields. Here are the patterns you’ll reach for most often in real projects.

Problem: The default query_posts() and the main loop do not cover complex scenarios like filtering by date range, combining taxonomy conditions, or excluding specific post IDs.

Solution: Use WP_Query directly with advanced arguments — meta_query, tax_query, date_query, and post__not_in — to build precisely targeted queries, and always call wp_reset_postdata() after a custom loop.

Multiple post types and complex taxonomy queries:

$query = new WP_Query( [
    'post_type'  => [ 'post', 'portfolio', 'case_study' ],
    'post_status'=> 'publish',
    'tax_query'  => [
        'relation' => 'AND',
        [
            'taxonomy' => 'category',
            'field'    => 'slug',
            'terms'    => [ 'featured', 'staff-picks' ],
            'operator' => 'IN',
        ],
        [
            'taxonomy' => 'post_tag',
            'field'    => 'slug',
            'terms'    => [ 'draft' ],
            'operator' => 'NOT IN',
        ],
    ],
    'posts_per_page' => 10,
] );

Meta query — find posts where a numeric field is within a range:

$query = new WP_Query( [
    'post_type' => 'product',
    'meta_query'=> [
        'relation' => 'AND',
        'price_clause' => [
            'key'     => '_price',
            'value'   => [ 10, 100 ],
            'type'    => 'NUMERIC',
            'compare' => 'BETWEEN',
        ],
        'featured_clause' => [
            'key'     => '_featured',
            'value'   => 'yes',
            'compare' => '=',
        ],
    ],
    // Order by the named meta clause
    'orderby' => [ 'price_clause' => 'ASC' ],
] );

Date query — posts from the last 30 days:

$query = new WP_Query( [
    'post_type'  => 'post',
    'date_query' => [
        [
            'after'     => '30 days ago',
            'inclusive' => true,
        ],
    ],
    'orderby'    => 'date',
    'order'      => 'DESC',
] );

Exclude the current post and sticky posts from a related posts loop:

$related = new WP_Query( [
    'post_type'           => 'post',
    'posts_per_page'      => 4,
    'post__not_in'        => array_merge( [ get_the_ID() ], get_option( 'sticky_posts' ) ),
    'ignore_sticky_posts' => true,
    'tax_query' => [
        [
            'taxonomy' => 'category',
            'field'    => 'term_id',
            'terms'    => wp_get_post_categories( get_the_ID() ),
        ],
    ],
] );

NOTE: Always call wp_reset_postdata() after a custom WP_Query loop — it restores the global $post object so that template functions like the_title() refer to the correct post again. Forgetting this causes subtle bugs in templates that follow your loop.