Querying posts by ACF field values is one of the most common WordPress development tasks, and also one of the most common sources of slow queries. The WP_Query meta_query API works, but how you write it determines whether your query runs in 10ms or 10 seconds.
Basic meta_query Syntax
ACF stores field values as WordPress post meta. A query filtering by an ACF field looks like this:
$args = array(
'post_type' => 'property',
'meta_query' => array(
array(
'key' => 'bedrooms',
'value' => 3,
'compare' => '>=',
'type' => 'NUMERIC',
),
),
);
$query = new WP_Query($args);
The type parameter matters more than most tutorials mention. Without it, WordPress compares values as strings – so “10” sorts before “9” because “1” comes before “9” alphabetically. Always specify NUMERIC for numbers, DATE for dates stored as YYYYMMDD, DATETIME for datetime strings, and CHAR for text (the default).
Multiple Conditions: AND vs OR
To query posts matching multiple field conditions:
$args = array(
'post_type' => 'property',
'meta_query' => array(
'relation' => 'AND',
array(
'key' => 'bedrooms',
'value' => 3,
'compare' => '>=',
'type' => 'NUMERIC',
),
array(
'key' => 'available',
'value' => '1',
'compare' => '=',
),
),
);
Switch relation to 'OR' when you want posts matching any condition. You can nest relation groups for complex logic:
'meta_query' => array(
'relation' => 'AND',
array(
'relation' => 'OR',
array( 'key' => 'city', 'value' => 'London' ),
array( 'key' => 'city', 'value' => 'Manchester' ),
),
array(
'key' => 'price',
'value' => 500000,
'compare' => '<=',
'type' => 'NUMERIC',
),
),
Need this built properly? Describe the project and get a free estimate.
Ordering by a Custom Field
To sort results by an ACF field value:
$args = array(
'post_type' => 'property',
'meta_key' => 'price',
'orderby' => 'meta_value_num',
'order' => 'ASC',
'meta_query' => array(
array(
'key' => 'available',
'value' => '1',
),
),
);
Use meta_value_num for numeric fields and meta_value for text. When you combine filtering and ordering on different meta keys, use named meta_query clauses:
$args = array(
'post_type' => 'property',
'orderby' => array( 'price_clause' => 'ASC' ),
'meta_query' => array(
'price_clause' => array(
'key' => 'price',
'type' => 'NUMERIC',
),
array(
'key' => 'available',
'value' => '1',
),
),
);
The Performance Problem
meta_query joins wp_postmeta to wp_posts on each query. The wp_postmeta table has an index on meta_key, but not on meta_value. A query filtering by meta_value on a large wp_postmeta table (millions of rows) does a full table scan on the filtered rows.
Concrete signs your meta query is slow: Query Monitor shows the query taking over 100ms, the EXPLAIN output shows “Using where” without “Using index”, or you have more than 100,000 posts with the field being queried.
Fixes in order of effort:
- Add a database index on wp_postmeta(meta_value) for the specific meta_key causing problems – requires direct database access but dramatically improves query speed
- Cache the query result with transients when the data does not change frequently
- Consider moving filterable data to a taxonomy instead of post meta – taxonomy queries use indexed tables and perform significantly better at scale
Querying ACF Repeater Field Data
ACF Repeater fields store sub-fields with keys like field_name_0_subfield, field_name_1_subfield. You cannot directly query “posts where any repeater row has X”. The workaround is querying by the row count meta key or using LIKE on the sub-field key pattern:
$args = array(
'post_type' => 'event',
'meta_query' => array(
array(
'key' => 'speakers_%_name',
'value' => 'John Smith',
'compare' => 'LIKE',
),
),
);
This is slow on large datasets. For frequently queried repeater data, store a flattened version in a separate non-repeater field and query that instead.