Почему orderby meta_value возвращает только посты с существующим meta_key в WordPress
У меня есть следующий wp_query:
$args = array(
'post_type' => 'news', // Тип поста - новости
'orderby' => 'meta_key', // Сортировка по мета-ключу
'order' => 'ASC', // Порядок сортировки - по возрастанию
'meta_key'=>'custom_author_name', // Мета-ключ для сортировки
'post_per_page'=>-1 // Вывести все посты
);
$query = new WP_Query($args);
echo $query->found_posts; // Вывести количество найденных постов
echo выводит 10 результатов, потому что только 10 постов типа news
имеют meta_key = custom_author_name
. Однако есть сотни постов news
, у которых нет строки post_meta с этим конкретным meta_key. Обратите внимание, что здесь не используется meta_query. Не задано meta_value, потому что я только пытаюсь отсортировать посты по meta_key, а не фильтровать по meta_value.
Разве orderby не должен выбирать все посты? И просто их сортировать?
Если да, то почему результат фильтруется? Если meta_key не найден, почему бы просто не использовать пустую строку или совпадение со всеми?
Если нет, то почему?
Если я добавлю meta_key к каждому посту новости (даже если это пустая строка), то я получу ожидаемый результат. Но это кажется огромным количеством ненужных строк в таблице.

Как указано в ответе @ambroseya, так и должно работать. Когда вы объявляете мета-запрос, даже если вы не ищете конкретное значение, он будет запрашивать только записи с объявленным мета-ключом. Если вы хотите включить все записи и отсортировать их по мета-ключу, используйте следующий код:
$args = array(
'post_type' => 'news',
'orderby' => 'meta_value',
'order' => 'ASC',
'meta_query' => array(
'relation' => 'OR',
array(
'key'=>'custom_author_name',
'compare' => 'EXISTS'
),
array(
'key'=>'custom_author_name',
'compare' => 'NOT EXISTS'
)
),
'posts_per_page'=>-1
);
$query = new WP_Query($args);
echo $query->found_posts;
Этот код использует расширенный мета-запрос, который ищет записи как с объявленным мета-ключом, так и без него. Поскольку условие с EXISTS
указано первым, при сортировке по meta_value
будет использоваться первый запрос.

Я попробовал применить ответ @Manny Fleurmond, и, как @Jake, у меня не получилось заставить его работать даже после исправления опечатки: 'orderby' => 'meta_key'
должно быть 'orderby' => 'meta_value'
. (Для полноты также стоит отметить, что должно быть 'posts_per_page'
, а не 'post_per_page'
, но это не влияет на рассматриваемую проблему.)
Если посмотреть на SQL-запрос, фактически сгенерированный ответом @Manny Fleurmond (после исправления опечаток), то получится следующее:
SELECT wp_{prefix}_posts.* FROM wp_{prefix}_posts
LEFT JOIN wp_{prefix}_postmeta ON (wp_{prefix}_posts.ID = wp_{prefix}_postmeta.post_id AND wp_{prefix}_postmeta.meta_key = 'custom_author_name' )
LEFT JOIN wp_{prefix}_postmeta AS mt1 ON ( wp_{prefix}_posts.ID = mt1.post_id )
WHERE 1=1 AND (
wp_{prefix}_postmeta.post_id IS NULL
OR
mt1.meta_key = 'custom_author_name'
) AND wp_{prefix}_posts.post_type = 'news' AND
(wp_{prefix}_posts.post_status = 'publish' OR wp_{prefix}_posts.post_author = 1 AND wp_{prefix}_posts.post_status = 'private')
GROUP BY wp_{prefix}_posts.ID ORDER BY wp_{prefix}_postmeta.meta_value ASC
Это демонстрирует, как WordPress разбирает параметры запроса: для каждого условия meta_query создается таблица, затем определяется, как их соединять и по чему сортировать. Сортировка работала бы нормально, если бы использовалось только одно условие с 'compare' => 'EXISTS'
, но добавление второго условия 'compare' => 'NOT EXISTS'
с оператором OR (как и требуется) нарушает порядок сортировки. В результате LEFT JOIN используется для соединения как первого условия/таблицы, так и второго условия/таблицы — и то, как WordPress объединяет всё это, означает, что таблица, созданная с помощью 'compare' => 'EXISTS'
, фактически заполняется meta_values из ЛЮБОГО произвольного поля, а не только из поля 'custom_author_name'
, которое нас интересует. Поэтому сортировка по этому условию/таблице даст желаемый результат, только если у данного типа записи 'news' есть только одно произвольное поле.
Решение, которое сработало в моей ситуации, — сортировка по другому условию/таблице — с NOT EXISTS. Казалось бы, это противоречит интуиции, но из-за того, как WordPress разбирает параметры запроса, именно в этой таблице meta_value
заполняется только тем произвольным полем, которое нам нужно.
(Единственный способ, которым я это выяснил, — выполнение эквивалентного запроса для моего случая:
SELECT wp_{prefix}_posts.ID, wp_{prefix}_postmeta.meta_value, mt1.meta_value FROM wp_{prefix}_posts
LEFT JOIN wp_{prefix}_postmeta ON (wp_{prefix}_posts.ID = wp_{prefix}_postmeta.post_id AND wp_{prefix}_postmeta.meta_key = 'custom_author_name' )
LEFT JOIN wp_{prefix}_postmeta AS mt1 ON ( wp_{prefix}_posts.ID = mt1.post_id )
WHERE 1=1 AND (
wp_{prefix}_postmeta.post_id IS NULL
OR
mt1.meta_key = 'custom_author_name'
) AND wp_{prefix}_posts.post_type = 'news' AND
(wp_{prefix}_posts.post_status = 'publish' OR wp_{prefix}_posts.post_author = 1 AND wp_{prefix}_posts.post_status = 'private')
ORDER BY wp_{prefix}_postmeta.meta_value ASC
Всё, что я сделал, — изменил отображаемые столбцы и удалил предложение GROUP BY. Это показало мне, что происходит: столбец postmeta.meta_value получал значения из всех meta_keys, а столбец mt1.meta_value — только meta_values из произвольного поля news.)
Решение
Как сказал @Manny Fleurmond, для orderby используется первое условие, так что ответ заключается в простой перестановке условий местами:
$args = array(
'post_type' => 'news',
'orderby' => 'meta_value',
'order' => 'ASC',
'meta_query' => array(
'relation' => 'OR',
array(
'key' => 'custom_author_name',
'compare' => 'NOT EXISTS'
),
array(
'key' => 'custom_author_name',
'compare' => 'EXISTS'
)
),
'posts_per_page' => -1
);
$query = new WP_Query($args);
Альтернативно можно сделать условия ассоциативными массивами и сортировать по соответствующему ключу:
$args = array(
'post_type' => 'news',
'orderby' => 'not_exists_clause',
'order' => 'ASC',
'meta_query' => array(
'relation' => 'OR',
'exists_clause' => array(
'key' => 'custom_author_name',
'compare' => 'EXISTS'
),
'not_exists_clause' => array(
'key' => 'custom_author_name',
'compare' => 'NOT EXISTS'
)
),
'posts_per_page' => -1
);
$query = new WP_Query($args);

Стоит отметить, что если мета-ключ custom_author_name
был установлен, а затем снят, этот meta_key
всё равно будет реагировать на EXISTS
, и в результате такие записи будут отображаться вместе с записями, у которых действительно есть custom_author_name
. В моём случае я использую чекбокс, поэтому применяю "value" => "1"
вместо EXISTS
, но для строк потребуется другой подход.

Именно так это и работает.
Если вы хотите сделать это без добавления строк в таблицу, вам придется выполнить два запроса. Один с meta_key, который даст ограниченные результаты, и другой, который получит весь список; затем использовать PHP для сравнения результатов двух запросов (возможно, удаляя результаты с meta_key из второго запроса, чтобы избежать дублирования, или используя другой подходящий способ в вашей ситуации).

К сожалению, WP_Query
не работает таким образом. Как только вы добавляете компонент "meta", вы создаете своего рода фильтр. Выведите $query->request
и вы увидите, что я имею в виду.
Во-вторых, WP_Query
вообще не поддерживает сортировку по meta ключу. Вы можете сортировать по meta значению для определенного ключа, но не по самому ключу. Снова выведите запрос, чтобы увидеть это. Вы заметите, что компоненты "order" пропадают при попытке сделать это.
Самый чистый способ заставить это работать, на мой взгляд - пара коротких фильтров:
function join_meta_wpse_188287($join) {
remove_filter('posts_join','join_meta_wpse_188287');
global $wpdb;
return ' INNER JOIN '.$wpdb->postmeta.' ON ('.$wpdb->posts.'.ID = '.$wpdb->postmeta.'.post_id)';
}
add_filter('posts_join','join_meta_wpse_188287');
function orderby_meta_wpse_188287($orderby) {
remove_filter('posts_orderby','orderby_meta_wpse_188287');
global $wpdb;
return $wpdb->postmeta.'.meta_key ASC';
}
add_filter('posts_orderby','orderby_meta_wpse_188287');
$args = array(
'post_type' => 'news',
'post_per_page'=>-1
);
$q = new WP_Query($args);
var_dump($q->request); // отладка
var_dump(wp_list_pluck($q->posts,'post_title')); // отладка
