WordPress Meta_Query: Everything You Need to Know

The meta_query parameter in WP_Query lets you filter WordPress posts by their custom field values. If you use Advanced Custom Fields or store data in post meta, meta_query is how you retrieve exactly the posts you need.

The minimal working example looks like this:

$args = array(
    'meta_query' => array(
        array(
            'key'     => 'featured_post',
            'value'   => '1',
            'compare' => '=',
        ),
    ),
);
$q = new WP_Query( $args );

To read a meta value off a single known post, use get_post_meta(); meta_query is for the inverse problem — finding posts that match a meta condition. This guide covers the simple shorthand syntax, querying by single and multiple meta values, nested conditions, date ranges, LIKE searches, comparison operators, the type parameter, EXISTS/NOT EXISTS checks, and sorting by one or multiple custom fields. You’ll want to be familiar with PHP and the basics of adding code snippets to WordPress.

meta_query structure at a glance

'meta_query' => array(
    'relation' => 'AND',  // AND or OR (optional, default AND)
    array(
        'key'     => 'field_name',   // required
        'value'   => 'field_value',  // optional for EXISTS/NOT EXISTS
        'compare' => '=',            // optional, default '='
        'type'    => 'CHAR',         // optional, default 'CHAR'
    ),
    // additional clauses...
)

Each clause inside meta_query accepts four parameters: key (the custom field name), value (what to match), compare (the operator), and type (how to cast the value). Only key is required.


Simple meta_key and meta_value query

For basic queries with a single custom field, you don’t need the full meta_query array. WP_Query accepts meta_key, meta_value, and meta_compare as top-level parameters:

$args = array(
    'post_type'    => 'post',
    'meta_key'     => 'featured',
    'meta_value'   => 'yes',
    'meta_compare' => '=',
);
$query = new WP_Query( $args );

This shorthand works for simple cases. For anything involving multiple fields, nested conditions, or the type parameter, use the meta_query array shown in the sections below.


Query posts by a meta value

The most common use case is filtering posts by a single custom field value.

For example, say your posts have a custom field called featured_post that controls which posts appear in a featured section on your homepage:

$args = array(
    'meta_query' => array(
        array(
            'key'     => 'featured_post',
            'value'   => '1',
            'compare' => '=',
        ),
    ),
    'no_found_rows' => true,
    'fields'        => 'ids',
);

$q = new WP_Query( $args );

The key in the meta_query parameter is the name of your custom field. The value is what you’re matching against. The compare field sets the comparison operator (equals, greater than, LIKE, etc.), which I cover in detail below.

The no_found_rows and fields arguments above are optional performance optimizations. 'no_found_rows' => true skips pagination counting, and 'fields' => 'ids' returns just the post IDs instead of full post objects. Drop both if you need pagination or want to loop through post content.

WP_Query returns an array of posts you can loop through with a standard WordPress loop. This guide focuses on the meta_query parameters rather than how to display the results.


Query posts by multiple meta values

If you want to check a meta key value against multiple values, use the IN operator. Pass an array as the value:

$args = array(
    'meta_query' => array(
        array(
            'key'     => 'city_name',
            'value'   => array( 'New York City', 'London', 'San Francisco' ),
            'compare' => 'IN',
        ),
    ),
);

$q = new WP_Query( $args );

This returns posts where the city_name field matches any of the three values. NOT IN works the same way but returns the opposite: posts where the field does not match any value in the array.


Query posts by multiple meta keys

To filter by multiple custom fields at once, add multiple arrays inside meta_query and set a relation parameter:

$args = array(
    'meta_query' => array(
        'relation' => 'AND',
        array(
            'key'     => 'city_name',
            'value'   => array( 'New York City', 'London', 'San Francisco' ),
            'compare' => 'IN',
        ),
        array(
            'key'     => 'featured_post',
            'value'   => '1',
            'compare' => '=',
        ),
    ),
);

$q = new WP_Query( $args );

The relation field can be set to AND or OR. Use AND when all conditions must match, and OR when any one condition is enough.


Nested meta queries

You can nest meta_query arrays to create complex logic. For example, “get posts where color is red AND (size is large OR size is medium)”:

$args = array(
    'post_type' => 'product',
    'meta_query' => array(
        'relation' => 'AND',
        array(
            'key'   => 'color',
            'value' => 'red',
        ),
        array(
            'relation' => 'OR',
            array(
                'key'   => 'size',
                'value' => 'large',
            ),
            array(
                'key'   => 'size',
                'value' => 'medium',
            ),
        ),
    ),
);
$query = new WP_Query( $args );

The outer relation is AND, so both conditions must be true. The inner array uses OR, so either size value will match. You can nest as many levels as needed.


Query posts between dates

You can filter posts by date values stored in custom fields. The setup for querying posts between two dates:

This assumes your date is formatted as YYYY-MM-DD. If you’re using Advanced Custom Fields, set the field’s return format to YYYY-MM-DD so this works out of the box.

$args = array(
    'meta_query' => array(
        array(
            'key'     => 'my_date_field',
            'value'   => array( '2022-01-01', '2022-12-31' ),
            'type'    => 'DATE',
            'compare' => 'BETWEEN',
        ),
    ),
);

$q = new WP_Query( $args );

If your dates are stored as Unix timestamps, it’s even easier. This snippet converts the date strings to timestamps and compares numerically:

$meta_query_args = array(
    'meta_query' => array(
        array(
            'key'     => 'my_date_field',
            'value'   => array( strtotime( '2022-01-01' ), strtotime( '2022-12-31' ) ),
            'type'    => 'numeric',
            'compare' => 'BETWEEN',
        ),
    ),
);

$meta_query = new WP_Query( $meta_query_args );

You can also use the greater-than or less-than operator in either query above to find posts before or after a single date instead of between two.


The type parameter

By default, WordPress compares meta values as strings. This causes problems when you need numeric or date comparisons. For example, comparing the string “9” with “10” puts 9 after 10 alphabetically. The type parameter tells WordPress to cast values before comparing.

Available type values:

  • NUMERIC casts as an integer (most common for number fields)
  • CHAR is string comparison (the default)
  • DATE is date comparison (expects YYYY-MM-DD format)
  • DATETIME is date-and-time comparison
  • DECIMAL is decimal number comparison
  • SIGNED is a signed integer (allows negative numbers)
  • UNSIGNED is an unsigned integer (positive numbers only)
  • TIME is time comparison
  • BINARY is binary comparison (case-sensitive)

An example using NUMERIC to find posts with a price greater than 50:

$args = array(
    'post_type' => 'product',
    'meta_query' => array(
        array(
            'key'     => 'price',
            'value'   => 50,
            'compare' => '>',
            'type'    => 'NUMERIC',
        ),
    ),
);
$query = new WP_Query( $args );

Without 'type' => 'NUMERIC', WordPress would compare “50” as a string and return incorrect results for values like “9” or “100”.


Check if a meta key EXISTS or NOT EXISTS

Sometimes you need to find posts where a custom field exists at all, regardless of its value. The EXISTS and NOT EXISTS operators handle this. You don’t need to set a value when using these operators.

// Get posts that HAVE a 'featured_image_url' custom field
$args = array(
    'post_type' => 'post',
    'meta_query' => array(
        array(
            'key'     => 'featured_image_url',
            'compare' => 'EXISTS',
        ),
    ),
);
$query = new WP_Query( $args );

// Get posts that do NOT have a 'hide_from_homepage' field
$args = array(
    'post_type' => 'post',
    'meta_query' => array(
        array(
            'key'     => 'hide_from_homepage',
            'compare' => 'NOT EXISTS',
        ),
    ),
);
$query = new WP_Query( $args );

This is useful for filtering out posts that haven’t been assigned a specific custom field yet, or for finding posts where a field was intentionally left empty.


Search meta values with LIKE

The LIKE operator finds posts where a custom field contains a substring. WordPress wraps the value with % wildcards automatically, so searching for “john” matches “John”, “Johnson”, and “john_doe”.

$args = array(
    'post_type' => 'post',
    'meta_query' => array(
        array(
            'key'     => 'author_name',
            'value'   => 'john',
            'compare' => 'LIKE',
        ),
    ),
);
$query = new WP_Query( $args );

Use NOT LIKE to exclude posts that contain the substring. Both operators are case-insensitive by default.


Meta query comparison operators

Every comparison operator available in meta_query and what it does:

OperatorDescriptionValue Type
=Equal to (default)String
!=Not equal toString
>Greater thanString/Numeric
>=Greater than or equal toString/Numeric
<Less thanString/Numeric
<=Less than or equal toString/Numeric
LIKEContains substring (case-insensitive)String
NOT LIKEDoes not contain substringString
INMatches any value in an arrayArray
NOT INDoes not match any value in an arrayArray
BETWEENValue falls between two valuesArray (2 items)
NOT BETWEENValue falls outside two valuesArray (2 items)
EXISTSMeta key exists (no value needed)None
NOT EXISTSMeta key does not exist (no value needed)None
REGEXPMatches a regular expression patternString (regex)
NOT REGEXPDoes not match a regular expressionString (regex)
RLIKEAlias for REGEXPString (regex)

When using the simple meta_key/meta_value shorthand instead of the meta_query array, use meta_compare to set the operator.


How to sort posts by meta fields

WordPress lets you sort query results by custom field values using the orderby parameter in WP_Query.

Set orderby to meta_value and add a meta_key with your field name. This sorts results alphabetically by that field’s value.

$meta_query_args = array(
    'post_type' => 'page',
    'order'     => 'ASC',
    'meta_key'  => 'city_name',
    'orderby'   => 'meta_value',
);

$meta_query = new WP_Query( $meta_query_args );

If your field stores numbers, use meta_value_num instead so WordPress sorts numerically rather than alphabetically:

$args = array(
    'post_type' => 'product',
    'meta_key'  => 'price',
    'orderby'   => 'meta_value_num',
    'order'     => 'ASC',
);
$query = new WP_Query( $args );

Sort by multiple meta fields

To sort by more than one custom field, give each meta_query clause a name (an alias) and reference those names in orderby:

$args = array(
    'post_type' => 'product',
    'meta_query' => array(
        'price_clause' => array(
            'key'     => 'price',
            'type'    => 'NUMERIC',
            'compare' => 'EXISTS',
        ),
        'rating_clause' => array(
            'key'     => 'rating',
            'type'    => 'NUMERIC',
            'compare' => 'EXISTS',
        ),
    ),
    'orderby' => array(
        'price_clause'  => 'ASC',
        'rating_clause' => 'DESC',
    ),
);
$query = new WP_Query( $args );

This sorts products by price (lowest first), then by rating (highest first) for products at the same price.


Frequently asked questions

What is meta_query in WordPress?

meta_query is a WP_Query parameter that filters posts by their custom field values. Each clause accepts a key (field name), value (what to match), compare (the operator like =, LIKE, IN, BETWEEN, EXISTS), and type (how to cast the value). Multiple clauses combine with AND or OR via a relation key.

What is the difference between meta_query and meta_key/meta_value?

The meta_key/meta_value pair is shorthand for a single-clause query. meta_query is the full array syntax that supports multiple clauses, nested conditions, the type parameter, and named clauses for sorting. Use the shorthand for simple cases; use meta_query when you need anything beyond one clause.

How do I query posts by multiple custom fields?

Add multiple arrays inside meta_query and set 'relation' => 'AND' (all conditions must match) or 'relation' => 'OR' (any condition is enough). For complex logic like “A AND (B OR C),” nest arrays with their own relation values.

Why are my numeric meta_query comparisons wrong?

By default, WordPress compares meta values as strings, so “9” ends up greater than “10.” Add 'type' => 'NUMERIC' (or 'DECIMAL' / 'SIGNED' / 'UNSIGNED') to your clause so WordPress casts the values before comparing. For numeric sorting, use orderby => 'meta_value_num' instead of meta_value.

How do I find posts with an empty or missing custom field?

Use 'compare' => 'NOT EXISTS' to find posts where the custom field was never set. For posts where the field exists but is blank, use 'compare' => '=' with 'value' => ''. Don’t set a value when using EXISTS or NOT EXISTS.

Does meta_query slow down WordPress?

Complex meta_query arrays can slow things down on large sites because the wp_postmeta table isn’t indexed for arbitrary key lookups. Keep queries targeted, avoid unnecessary nested OR conditions, and add 'no_found_rows' => true when you don’t need pagination. For sites with thousands of posts and heavy meta filtering, consider moving high-traffic filters into custom taxonomies or a dedicated indexed table.

Can I use meta_query with Advanced Custom Fields (ACF)?

Yes. ACF stores field values in wp_postmeta like any other custom field, so meta_query works directly. Use the ACF field name (not the label) as the key. For date fields, set the ACF return format to Y-m-d so type => 'DATE' comparisons work correctly. For repeater and relationship fields, the meta is stored with indexed keys (e.g. field_0_name), which makes direct meta_query on them more complex.


Bottom line

For one custom field and a simple comparison, the meta_key / meta_value shorthand is fine. For anything beyond that (multiple fields, nested logic, date ranges, numeric comparisons, sorting), reach for the full meta_query array and set type explicitly when values aren’t plain strings. WordPress converts your parameters into optimized SQL behind the scenes, so always use WP_Query rather than writing raw MySQL.

For the full list of WP_Query parameters beyond meta_query, see the WP_Meta_Query documentation on wordpress.org. Related: getting a post ID, inserting posts programmatically, or browse the full WordPress code snippets library.

Picture of Andy Feliciotti

Andy Feliciotti

Andy has been a full time WordPress developer for over 15 years. Through his years of experience has built 100s of sites and learned plenty of tricks along the way. Found this article helpful? Buy Me A Coffee

5 Responses

  1. Hi, is it possible to have multiple meta_queries with AND / OR?

    So I have four variables stored in custom fields, ‘user-1’, ‘user-2’ (both to be checked against current user), ‘date’ (checked against current date), ‘status’. (action/filing) and what i want to achieve in the returned posts is the following;

    Result should be as follows;

    Query 1 is == ‘user-1’ OR ‘user-2’ can be current user

    Query 2 is == AND ‘date’ is earlier than today AND ‘status’ is (action)

    I can get it to work using just one ‘user’ custom field, because then it is just three AND relations, but not to have both queried as it then feels like i have to have two meta_queries, which I don’t know is possible? I have been using Oxygen repeater with query builder to make my query rather than coding it directly. Any thoughts on the matter would be appreciated.

Leave a Reply

Your email address will not be published. Required fields are marked *

WordPress Tips Monthly
Get the latest from SmartWP to your inbox.