Tips・WordPress templates and fetching posts

Filtering and sorting by custom field with meta_query

How to write meta_query to filter posts by custom field values like price, stock and event date. Covers a cheat sheet of compare and type, sorting numerically with meta_value_num, and why it sometimes doesn't work.

“Only products under $30.” “Only what’s in stock.”—meta_query is how you filter posts by custom field values. The basic shape is the same nested structure as tax_query.

<?php
$query = new WP_Query([
  'post_type'  => 'shohin',
  'meta_query' => [
    [
      'key'     => 'price',      // the field name
      'value'   => 3000,         // the value to compare against
      'compare' => '<=',         // how to compare
      'type'    => 'NUMERIC',    // the value's type (required for numbers)
    ],
  ],
]);
?>

The most important part is type. Forget it and numeric comparisons come out wrong (why, below).

What the four keys mean

KeyRoleCommon values
keyThe custom field’s name'price', 'event_date'
valueThe value to compare againstNumber, string or array
compareHow to compare'=' (default), '!=', '>', '>=', '<', '<=', 'LIKE', 'IN', 'NOT IN', 'BETWEEN', 'EXISTS', 'NOT EXISTS'
typeThe value’s type'CHAR' (default), 'NUMERIC', 'DATE', 'DATETIME', 'DECIMAL'

Why type is necessary

WordPress stores custom field values as strings, without distinguishing types. So without a type, comparisons follow string rules too.

  • Compared as strings—'100' is smaller than '99' (because it compares the leading 1 against 9)
  • Add 'type' => 'NUMERIC'—it compares as numbers, so 100 > 99

Whenever you handle numbers—price, stock count, score—always include 'type' => 'NUMERIC'. If decimals are involved, use 'DECIMAL'.

compare in practice

// stock of 1 or more (in stock)
['key' => 'stock', 'value' => 0, 'compare' => '>', 'type' => 'NUMERIC'],

// between 1000 and 3000
['key' => 'price', 'value' => [1000, 3000], 'compare' => 'BETWEEN', 'type' => 'NUMERIC'],

// size is either S or M
['key' => 'size', 'value' => ['S', 'M'], 'compare' => 'IN'],

// partial match (the note contains "limited")
['key' => 'note', 'value' => 'limited', 'compare' => 'LIKE'],

// only posts that have something in that field
['key' => 'price', 'compare' => 'EXISTS'],

// only posts where that field is empty (or unset)
['key' => 'price', 'compare' => 'NOT EXISTS'],

BETWEEN and IN require value to be an array.

Combining several conditions

relation decides how they’re joined (AND when omitted).

'meta_query' => [
  'relation' => 'AND',
  ['key' => 'price', 'value' => 3000, 'compare' => '<=', 'type' => 'NUMERIC'],
  ['key' => 'stock', 'value' => 0,    'compare' => '>',  'type' => 'NUMERIC'],
],

Nest them and you can write “(A or B) and C” too.

'meta_query' => [
  'relation' => 'AND',
  [
    'relation' => 'OR',
    ['key' => 'is_new',  'value' => '1'],
    ['key' => 'is_sale', 'value' => '1'],
  ],
  ['key' => 'stock', 'value' => 0, 'compare' => '>', 'type' => 'NUMERIC'],
],

Sorting by custom field value

Separately from filtering, custom fields can drive sorting too. It’s a combination of meta_key and orderby.

// cheapest first
$query = new WP_Query([
  'post_type' => 'shohin',
  'meta_key'  => 'price',
  'orderby'   => 'meta_value_num',   // sort as numbers
  'order'     => 'ASC',
]);
  • 'orderby' => 'meta_value_num'—sort as numbers (price, score, stock)
  • 'orderby' => 'meta_value'—sort as strings (names, dates in Ymd format)

Sorting by two criteria (named conditions)

Give your meta_query conditions names and you can refer to them from orderby. That gives multi-level sorting like “featured first, then cheapest.”

$query = new WP_Query([
  'post_type'  => 'shohin',
  'meta_query' => [
    'featured' => ['key' => 'is_featured', 'compare' => 'EXISTS'],
    'price'    => ['key' => 'price',       'compare' => 'EXISTS', 'type' => 'NUMERIC'],
  ],
  'orderby' => [
    'featured' => 'DESC',
    'price'    => 'ASC',
  ],
]);

The array keys (featured, price) become name tags for the conditions, and orderby sets their order.

Filtering by a date field

ACF (SCF) date fields are stored by default as an Ymd string like 20260725. In that format, comparisons work correctly even as strings, so you can use them as-is.

// only events from today onward, soonest first
$query = new WP_Query([
  'post_type'  => 'event',
  'meta_key'   => 'event_date',
  'orderby'    => 'meta_value',
  'order'      => 'ASC',
  'meta_query' => [
    [
      'key'     => 'event_date',
      'value'   => date('Ymd'),
      'compare' => '>=',
    ],
  ],
]);

If the save format has hyphens, like 2026-07-25, add 'type' => 'DATE' for the comparison.

TipsACF field types: a cheat sheet for displaying each oneACF date field save formats and how to format them for display

Example: pairing it with a filter form

A pattern that takes a URL parameter (?max_price=3000) and builds conditions from it.

<?php
$args = ['post_type' => 'shohin', 'posts_per_page' => 12];

if (!empty($_GET['max_price'])) {
  $args['meta_query'][] = [
    'key'     => 'price',
    'value'   => (int) $_GET['max_price'],   // always convert to a number
    'compare' => '<=',
    'type'    => 'NUMERIC',
  ];
}

$query = new WP_Query($args);
?>

When it gets slow, revisit the design

Post meta is stored as “one value per row,” so the more conditions you add, the more database joins pile up and the slower it gets.

  • The filtering axis is fixed (season, brand, category) → making it a custom taxonomy and using tax_query is faster
  • The filtering axis is a numeric range (price, area) → meta_query is the right fit

“Fields used for filtering in listings become taxonomies; fields that are only displayed on the single page stay custom fields”—that’s a design decision that pays off later.

TipsFiltering by category and taxonomy with tax_queryHow to write tax_query to filter by taxonomy

Summary

  1. meta_query is a set of four: key / value / compare / type. Conditions go in an array inside an array
  2. Numeric comparisons require 'type' => 'NUMERIC' (the default is string comparison, which breaks ordering)
  3. Sort with meta_key plus orderby: meta_value_num. Watch out—posts without a value disappear
  4. If the filtering axis is fixed, making it a taxonomy and using tax_query is faster

Once you’re comfortable writing conditions, “the data exists but I can’t list it” stops happening.

FAQ

Numeric comparisons in meta_query give wrong results.
Custom field values are stored as strings, so without a type they're compared as strings ('100' is judged smaller than '99'). Add 'type' => 'NUMERIC'.
How do I sort by a custom field value?
Specify 'meta_key' => 'price' along with 'orderby' => 'meta_value_num' (for numbers) or 'meta_value' (for strings). Switch ascending and descending with order.
I want posts without a value included in the sort.
Sorting by meta_key drops posts that don't have that key from the list. Either set relation to OR and add a NOT EXISTS condition, or fill in a default value when the post is saved.
I've heard meta_query is slow.
Post meta lives in a table with one value per row, so more conditions mean more JOINs and more slowness. On a large site with a fixed filtering axis, turning it into a custom taxonomy and filtering with tax_query is faster.