WP_Query: выборки, которых нет в интерфейсе WordPress
Афишу с будущими событиями, без рекламных меток и с сортировкой по цене в админке не собрать, а фильтровать в PHP накладно. Разбираем, что умеет WP_Query штатно и когда пора писать posts_where.

Страница афиши должна показать события, которые ещё не прошли и у которых указана цена. Материалы с меткой sponsored туда попадать не должны. В админке такой фильтр не собрать: она умеет рубрики, авторов и месяцы. Первое решение обычно такое: get_posts() с posts_per_page => -1 и отбор в цикле PHP. В память уходят все записи ради десяти на экране, а пагинацию приходится писать вручную. Почти всё нужное умеет сам WP_Query. Материал рассчитан на WordPress 6.x и PHP 7.4+.
Штатные аргументы, которых нет в админке
Сначала проверьте, не решается ли задача обычными параметрами. Три из них закрывают большую часть «невозможных» выборок: meta_query, tax_query и date_query. Все три принимают вложенные группы с relation AND или OR.
meta_query: тип и именованные клаузы
Чаще всего упускают две вещи. Первая: type. Без него значения сравниваются как строки, и «10» оказывается меньше «9». Вторая: строковые ключи у клауз. По ним orderby принимает массив и сортирует сразу по нескольким полям (с WordPress 4.2). В примере афиша: будущие события с заполненной ценой, сначала по дате, затем по цене.
<?php
// WordPress 6.x
$upcoming = new WP_Query( array(
'post_type' => 'event',
'post_status' => 'publish',
'posts_per_page' => 10,
'no_found_rows' => true,
// клаузы объединяются через AND, пока не указан другой relation
'meta_query' => array(
'event_date_clause' => array(
'key' => 'event_date',
'value' => current_time( 'Y-m-d' ),
'compare' => '>=',
'type' => 'DATE',
),
'price_clause' => array(
'key' => 'event_price',
'compare' => 'EXISTS',
'type' => 'NUMERIC',
),
),
'orderby' => array(
'event_date_clause' => 'ASC',
'price_clause' => 'ASC',
),
) );Поля event_date и event_price здесь условные, подставьте свои. Дата должна храниться в формате Y-m-d: для «ДД.ММ.ГГГГ» сравнение по больше-или-равно даст неверный результат. Параметр no_found_rows отключает подсчёт общего числа записей¹.
¹ По умолчанию WP_Query считает не только страницу результатов, но и сколько записей подошло всего (поле found_posts), чтобы построить пагинацию. Это дополнительная работа для базы. Если пагинации нет, подсчёт можно отключить.
tax_query и date_query: исключения и дни недели
У условия в tax_query есть operator. NOT IN исключает записи с термином, NOT EXISTS находит записи без единого термина таксономии. В date_query можно фильтровать не только «после» и «до», но и по dayofweek (1 это воскресенье, 7 суббота). По умолчанию сравнение идёт по локальному времени сайта, для UTC укажите column со значением post_date_gmt.
<?php
// WordPress 6.x: новости за 30 дней, опубликованные в будни, без метки sponsored
$weekday_news = new WP_Query( array(
'category_name' => 'news',
'posts_per_page' => 20,
'tax_query' => array(
array(
'taxonomy' => 'post_tag',
'field' => 'slug',
'terms' => array( 'sponsored' ),
'operator' => 'NOT IN',
),
),
'date_query' => array(
array( 'after' => '30 days ago' ),
array(
'dayofweek' => array( 2, 6 ), // понедельник - пятница
'compare' => 'BETWEEN',
),
),
) );Когда аргументов не хватает: posts_where
Условий вроде «пустая цитата» или «текст длиннее 3000 знаков» среди аргументов нет. Здесь помогает фильтр posts_where. Приём: передать в WP_Query собственный аргумент и проверять его в фильтре. Неизвестные ключи запрос не отбрасывает, их читает $query->get().
Фильтр SQL, который не знает, к какому запросу относится, рано или поздно сломает чужой.
<?php
// WordPress 6.x, PHP 7.4+
add_filter(
'posts_where',
function ( string $where, WP_Query $query ): string {
$min_length = absint( $query->get( 'min_content_length' ) );
if ( 0 === $min_length ) {
return $where; // чужой запрос, не трогаем
}
global $wpdb;
return $where . $wpdb->prepare(
" AND CHAR_LENGTH({$wpdb->posts}.post_content) >= %d",
$min_length
);
},
10,
2
);
$long_reads = new WP_Query( array(
'post_type' => 'post',
'min_content_length' => 3000,
'fields' => 'ids',
'no_found_rows' => true,
'posts_per_page' => 50,
) );Длина считается вместе с HTML-разметкой и шорткодами. Если нужно менять JOIN или сортировку, используйте posts_clauses: он отдаёт все части запроса одним массивом.
Сравнение подходов
| Подход | Что получаете | Чем платите |
|---|---|---|
| Штатные аргументы | Поддержка ядра, кэширование записей, совместимость с плагинами | Как правило, свой JOIN с wp_postmeta на каждое условие по мете; в стандартной схеме значение меты не индексируется |
| posts_where, posts_clauses | Любое условие по колонкам wp_posts, свои JOIN и сортировка | Ручной SQL, риск конфликта с плагинами, которые правят тот же запрос |
| Запрос через $wpdb | Агрегаты, GROUP BY, выборка не записей | Нет фильтров и логики статусов WP_Query, prepare и права доступа на вас |
| Отбор в PHP после get_posts() | Проще всего написать | Все записи в памяти, пагинация вручную |
Когда что выбирать
- Условие выражается через мету, таксономии или даты: штатные аргументы. Они всегда первые.
- Условие касается колонок wp_posts (длина текста, пустая цитата, маска в post_name):
posts_where. - Нужны агрегаты, например число записей по рубрикам, или результат не должен быть списком записей:
$wpdb. - Если
EXPLAINпоказывает полный проход по wp_postmeta, значение стоит вынести в отдельную таблицу с индексом.
Что ломается
- Нет
type: «10» меньше «9», диапазоны цен возвращают мусор. EXISTSпроверяет наличие строки меты, а не значения. Поле, сохранённое как пустая строка, пройдёт.- Массивы в мете хранятся сериализованной строкой. Искать по ним через
LIKEможно, но это хрупко. Надёжнее писать значения отдельными строками меты под одним ключом или завести таксономию. get_posts()по умолчанию включаетsuppress_filters, поэтомуposts_whereв нём не сработает. БеритеWP_Queryили передайте'suppress_filters' => false.- Фильтр без проверки своего аргумента заденет главный запрос, виджеты и списки в админке.
no_found_rows => trueотключает пагинацию: страницы считать будет нечем.
Что делать дальше
Поставьте плагин Query Monitor на тестовую копию сайта и посмотрите SQL своей выборки: сколько в нём JOIN и сколько он выполняется. Для запроса, который тормозит, снимите EXPLAIN. Если выборка нужна на каждой странице, кэшируйте результат. Инвалидация кэша это отдельная тема, здесь она не разбирается.
Источники: WP_Query: справочник класса, WordPress Developer Resources, Хук posts_where, WordPress Developer Resources
Частые вопросы
Что делать, если у меня WordPress старше 6.x?
Почему мой posts_where не срабатывает?
Как отсортировать по мете, которой нет у части записей?
Безопасно ли подставлять пользовательский ввод в аргументы?
Чем WP_Query отличается от get_posts()?
Что проверить перед выкатыванием на прод?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.