Meta Queries

meta_query filters posts by custom fields, with compare operators, type casting, and a join per clause.

Syntax'meta_query' => array( 'relation' => 'AND', array( 'key' => '_price', 'value' => 100, 'compare' => '>=', 'type' => 'NUMERIC' ) )

meta_query filters a post query by values in the wp_postmeta table. It is parsed by WP_Meta_Query, which turns each clause into a JOIN onto wp_postmeta plus a WHERE condition.

Structure

meta_query is an array of clauses, optionally prefixed by 'relation' => 'AND' or 'OR'; the default is 'AND'. Each clause takes key, value, compare and type. compare defaults to '=' and accepts '=', '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'REGEXP', 'NOT REGEXP', 'RLIKE', 'EXISTS' and 'NOT EXISTS'. type defaults to 'CHAR' and accepts 'NUMERIC', 'BINARY', 'CHAR', 'DATE', 'DATETIME', 'DECIMAL', 'SIGNED', 'TIME' and 'UNSIGNED'. Clauses may be nested to build mixed AND/OR logic.

Shorthand and longhand

The top-level meta_key and meta_value arguments are shorthand: WordPress folds them into a single meta_query clause. 'meta_key' => 'colour', 'meta_value' => 'red' is the same query as a meta_query with one clause of key 'colour' and value 'red'. Naming a clause key also lets orderby sort by that clause.

The performance cost

wp_postmeta.meta_value is a longtext column with no index on its value, so every clause adds an unindexed JOIN. Two or three clauses on a large table will show up in your slow query log. If you filter on a field constantly, it belongs in a taxonomy or its own table.

Example

Example · php
<?php
// Products in stock, priced between 10 and 50, cheapest first.
$products = new WP_Query(
	array(
		'post_type'      => 'product',
		'posts_per_page' => 20,

		'meta_query'     => array(
			'relation' => 'AND', // this is the default; stated for clarity

			'price_clause' => array( // named so orderby can reference it
				'key'     => '_price',
				'value'   => array( 10, 50 ),
				'compare' => 'BETWEEN',
				'type'    => 'NUMERIC', // without this it is a string compare
			),
			array(
				'key'     => '_stock_status',
				'value'   => 'instock',
				'compare' => '=', // the default
			),
			array(
				'key'     => '_hidden',
				'compare' => 'NOT EXISTS', // no 'value' needed for EXISTS/NOT EXISTS
			),
		),

		'orderby'        => 'price_clause', // sort by the named clause
		'order'          => 'ASC',
	)
);

// Three clauses means three JOINs onto wp_postmeta. Measure before shipping.

When to use it

  • Filter products by a numeric price range stored in post meta
  • Exclude posts that have a particular flag set with NOT EXISTS
  • Sort an archive by a custom field such as a rating or start date
  • Combine two custom-field conditions with OR inside an otherwise AND query

More examples

meta_key/meta_value shorthand and its meta_query longhand

The shorthand is everywhere in older code and reads fine for one equality test. Switch to the longhand the moment you need a second condition, a comparison operator other than '=', or numeric casting.

Example · php
<?php
// Shorthand: the top-level arguments.
$q = new WP_Query(
	array(
		'post_type'  => 'staff',
		'meta_key'   => 'department',
		'meta_value' => 'engineering',
	)
);

// Longhand: exactly what WordPress builds from the shorthand above.
$q = new WP_Query(
	array(
		'post_type'  => 'staff',
		'meta_query' => array(
			array(
				'key'     => 'department',
				'value'   => 'engineering',
				'compare' => '=',
				'type'    => 'CHAR',
			),
		),
	)
);

// meta_key survives on its own for ordering, with no filtering:
$q = new WP_Query(
	array(
		'post_type' => 'staff',
		'meta_key'  => 'last_name',
		'orderby'   => 'meta_value', // use 'meta_value_num' for numbers
		'order'     => 'ASC',
	)
);

Nested clauses for mixed AND/OR logic

Nesting is how you express (A AND (B OR C)) — a flat array with one relation cannot. Each leaf clause is still a separate JOIN, so keep nested groups shallow and cache the result if the page is hot.

Example · php
<?php
// Events that are either free, or discounted below 20 — and always public.
$events = new WP_Query(
	array(
		'post_type'  => 'event',
		'meta_query' => array(
			'relation' => 'AND',
			array(
				'key'   => 'visibility',
				'value' => 'public',
			),
			array(
				'relation' => 'OR', // a nested group
				array(
					'key'   => 'is_free',
					'value' => '1',
				),
				array(
					'key'     => 'price',
					'value'   => 20,
					'compare' => '<',
					'type'    => 'NUMERIC',
				),
			),
		),
	)
);

Replacing a hot meta filter with a taxonomy

Post meta is a key-value bag designed for reading, not filtering. When a field is a fixed set of values you filter and browse by, model it as a taxonomy: you get indexed queries, archive URLs and an admin UI for free.

Example · php
<?php
// Slow: an unindexed JOIN on every archive request.
$slow = new WP_Query(
	array(
		'post_type'  => 'product',
		'meta_query' => array(
			array(
				'key'   => 'brand',
				'value' => 'acme',
			),
		),
	)
);

// Fast: the term tables are indexed for exactly this lookup.
// Register 'brand' with register_taxonomy() and query it instead.
$fast = new WP_Query(
	array(
		'post_type' => 'product',
		'tax_query' => array(
			array(
				'taxonomy' => 'brand',
				'field'    => 'slug',
				'terms'    => 'acme',
			),
		),
	)
);

Discussion

  • Be the first to comment on this lesson.