WordPress Taxonomy Queries: WP_Tax_Query in Depth

Every time you pass a tax_query argument to WP_Query, WordPress builds a WP_Tax_Query object internally. Understanding how it works unlocks complex filtering across multiple taxonomies — tags, categories, custom taxonomies, and relationships between them.

Problem: How do you query posts that match multiple taxonomy conditions at once — for example, posts in a specific category AND tagged with a particular term, or posts belonging to either of two categories?

Solution: Pass a tax_query array to WP_Query with relation set to AND or OR, and define one sub-array per taxonomy condition. Use the field key to match by term_id, slug, or name.

Basic single-taxonomy query:

// Posts in the "tutorials" category AND tagged "wordpress"
$query = new WP_Query( [
    'post_type' => 'post',
    'tax_query' => [
        'relation' => 'AND',
        [
            'taxonomy' => 'category',
            'field'    => 'slug',
            'terms'    => 'tutorials',
        ],
        [
            'taxonomy' => 'post_tag',
            'field'    => 'slug',
            'terms'    => 'wordpress',
        ],
    ],
] );

Nested tax_query for complex OR/AND logic:

// (category = 'news' OR category = 'announcements') AND tag != 'archived'
$query = new WP_Query( [
    'post_type' => 'post',
    'tax_query' => [
        'relation' => 'AND',
        [
            'relation' => 'OR',
            [
                'taxonomy' => 'category',
                'field'    => 'slug',
                'terms'    => 'news',
            ],
            [
                'taxonomy' => 'category',
                'field'    => 'slug',
                'terms'    => 'announcements',
            ],
        ],
        [
            'taxonomy' => 'post_tag',
            'field'    => 'slug',
            'terms'    => 'archived',
            'operator' => 'NOT IN',
        ],
    ],
] );

Using term IDs and the EXISTS operator:

// Posts that have ANY value for the custom taxonomy 'color'
$query = new WP_Query( [
    'post_type' => 'product',
    'tax_query' => [
        [
            'taxonomy' => 'color',
            'operator' => 'EXISTS',
        ],
    ],
] );

// Posts with NO term in the 'color' taxonomy
$uncategorised = new WP_Query( [
    'post_type' => 'product',
    'tax_query' => [
        [
            'taxonomy' => 'color',
            'operator' => 'NOT EXISTS',
        ],
    ],
] );

NOTE: When querying by term slug or name, WordPress runs an additional query to convert the slug/name to a term ID. For high-traffic queries, use 'field' => 'term_id' with pre-resolved IDs to save a database round-trip per query execution.