Наиболее эффективный способ получения записей с postmeta
Мне нужно получить множество записей с их метаданными. Конечно, нельзя получить метаданные со стандартным запросом постов, поэтому обычно приходится использовать get_post_custom()
для каждой записи.
Я пробую с одним пользовательским запросом, вот так:
$results = $wpdb->get_results("
SELECT p.ID,
p.post_title,
pm1.meta_value AS first_field,
pm2.meta_value AS second_field,
pm3.meta_value AS third_field
FROM $wpdb->posts p LEFT JOIN $wpdb->postmeta pm1 ON (
pm1.post_id = p.ID AND
pm1.meta_key = 'first_field_key'
) LEFT JOIN $wpdb->postmeta pm2 ON (
pm2.post_id = p.ID AND
pm2.meta_key = 'second_field_key'
) LEFT JOIN $wpdb->postmeta pm3 ON (
pm3.post_id = p.ID AND
pm3.meta_key = 'third_field_key'
)
WHERE post_status = 'publish'
");
Вроде работает. Возникают проблемы, если вы используете любое из этих мета-полей таким образом, что допускается несколько мета-значений для одного и того же поста. Я не могу придумать join-запрос для этого.
Итак, вопрос 1: Существует ли join, подзапрос или что-то еще, чтобы получить мета-поля с множественными значениями?
Но вопрос 2: Стоит ли это того? Сколько joins к таблице postmeta
можно добавить, прежде чем подход с 2-мя запросами станет предпочтительнее? Я мог бы получить все данные постов одним запросом, затем получить все соответствующие postmeta другим запросом, и объединить мета-данные с данными постов в один результирующий набор в PHP. Будет ли это в итоге быстрее, чем один все более сложный SQL-запрос, если это вообще возможно?
Я всегда думаю: "Дайте как можно больше работы базе данных". Но в этом случае не уверен!

Метаданные записей автоматически кэшируются в памяти для стандартного WP_Query
(и главного запроса), если только вы явно не укажете не делать этого с помощью параметра update_post_meta_cache
.
Поэтому вам не следует писать собственные запросы для этого.
Как работает кэширование метаданных для обычных запросов:
Если параметр update_post_meta_cache
в WP_Query
не установлен в false, то после получения записей из БД будет вызвана функция update_post_caches()
, которая, в свою очередь, вызывает update_postmeta_cache()
.
Функция update_postmeta_cache()
является обёрткой для update_meta_cache()
и по сути выполняет простой SELECT
со всеми ID полученных записей. Это позволяет получить все метаданные для всех записей в запросе и сохранить их в кэше объектов (используя wp_cache_add()
).
Когда вы используете что-то вроде get_post_custom()
, сначала проверяется кэш объектов. Таким образом, дополнительные запросы для получения метаданных на этом этапе не выполняются. Если вы получили запись через WP_Query
, её метаданные уже находятся в памяти и извлекаются оттуда.
Преимущества этого подхода во много раз превосходят создание сложных запросов, но наибольшее преимущество даёт использование кэша объектов. Если вы применяете решение для постоянного кэширования в памяти, такое как XCache, memcached, APC или подобное, и у вас есть плагин, который может связать кэш объектов с ним (например, W3 Total Cache), то весь ваш кэш объектов уже хранится в быстрой памяти. В этом случае для получения данных не требуется никаких запросов — они уже в памяти. Постоянное кэширование объектов имеет множество преимуществ.
Другими словами, ваш запрос, скорее всего, работает намного медленнее, чем использование стандартного WP_Query
и простого решения для постоянного кэширования в памяти. Используйте стандартный WP_Query
. Сэкономьте свои усилия.
Дополнительно: Кстати, update_meta_cache()
умная функция. Она не будет получать метаданные для записей, у которых они уже закэшированы. По сути, она не извлекает одни и те же метаданные дважды. Очень эффективно.
Ещё дополнительно: «Передавайте как можно больше работы базе данных.»... Нет, это веб. Здесь действуют другие правила. Как правило, вы всегда хотите передавать базе данных как можно меньше работы, если это возможно. Базы данных часто медленные или плохо настроены (если вы не настраивали их специально, можете быть уверены, что это так). Часто они используются совместно многими сайтами и перегружены. Обычно у вас больше веб-серверов, чем баз данных. Как правило, вы хотите просто получить нужные данные из БД как можно быстрее и проще, а затем обработать их с помощью кода на стороне веб-сервера. Это общий принцип, конечно, все случаи разные.

Я бы порекомендовал использовать сводный запрос. На примере вашего кода:
SELECT p.ID,
p.post_title,
MAX(CASE WHEN pm1.meta_key = 'first_field' then pm1.meta_value ELSE NULL END) as first_field,
MAX(CASE WHEN pm1.meta_key = 'second_field' then pm1.meta_value ELSE NULL END) as second_field,
MAX(CASE WHEN pm1.meta_key = 'third_field' then pm1.meta_value ELSE NULL END) as third_field,
FROM wp_posts p LEFT JOIN wp_postmeta pm1 ON ( pm1.post_id = p.ID)
GROUP BY
p.ID,p.post_title

Этот запрос сократил моё время выполнения с ~25 сек до ~3 сек при использовании WP_Query. Мне нужно было выполнить его только один раз, поэтому кэширование не требовалось.

Я столкнулся с задачей, когда нужно быстро получить большое количество записей с их мета-данными. Мне нужно извлечь около 2000 записей.
Попробовал вариант, предложенный Otto — выполнить WP_Query::query для всех записей, а затем в цикле вызывать get_post_custom для каждой записи. В среднем это занимало около 3 секунд.
Затем попробовал сводный запрос Ethan (хотя мне не понравилось, что нужно вручную указывать каждый meta_key, который меня интересует). Все равно пришлось перебирать все полученные записи для десериализации meta_value. В среднем это занимало около 1.3 секунд.
Потом попробовал использовать функцию GROUP_CONCAT и получил лучший результат. Вот код:
global $wpdb;
$wpdb->query('SET SESSION group_concat_max_len = 10000'); // необходимо, чтобы получить больше 1024 символов в столбцах GROUP_CONCAT ниже
$query = "
SELECT p.*,
GROUP_CONCAT(pm.meta_key ORDER BY pm.meta_key DESC SEPARATOR '||') as meta_keys,
GROUP_CONCAT(pm.meta_value ORDER BY pm.meta_key DESC SEPARATOR '||') as meta_values
FROM $wpdb->posts p
LEFT JOIN $wpdb->postmeta pm on pm.post_id = p.ID
WHERE p.post_type = 'product' and p.post_status = 'publish'
GROUP BY p.ID
";
$products = $wpdb->get_results($query);
// преобразует продукты, добавляя член ->meta с десериализованными значениями, как ожидается
function massage($a){
$a->meta = array_combine(explode('||',$a->meta_keys),array_map('maybe_unserialize',explode('||',$a->meta_values)));
unset($a->meta_keys);
unset($a->meta_values);
return $a;
}
$products = array_map('massage',$products);
В среднем это заняло 0.7 секунд. Это примерно в четыре раза быстрее, чем решение с get_post_custom() в WP, и примерно в два раза быстрее, чем вариант со сводным запросом.
Возможно, это будет полезно кому-то.

Мне было бы интересно узнать, какие результаты вы получите при использовании решения с постоянным кешированием объектов. В базовом случае кеширование объектов иногда может работать медленнее, в зависимости от вашей базы данных и конфигурации, но в реальных условиях на большинстве хостингов результаты могут сильно варьироваться. Кеширование в памяти невероятно быстрое.

Привет, @Otto. Независимо от того, какой метод я использую для получения данных, я определенно хочу кешировать результат. Я пробовал использовать Transient API для этого, но столкнулся с проблемами памяти. Сериализованная строка для моих 2000 объектов занимает около 8 МБ, и set_transient() завершается с ошибкой (память исчерпана). Также пришлось изменить настройку max_allowed_packet в MySQL. Я рассмотрю вариант кеширования в файл, но пока не уверен в производительности такого решения. Существует ли способ кеширования в память, который сохраняется между запросами?

Да, если у вас есть постоянное кеширование в памяти (XCache, memcached, APC и т.д.) и вы используете плагин для кеширования объектов (W3 Total Cache поддерживает множество типов кешей в памяти), то все объекты кешируются в памяти, что дает многократное ускорение практически всех операций.

Я возвращаю 6000 элементов для использования в схеме фильтрации на backbone/underscore js. Это преобразовало 6-секундный кастомный запрос, который я даже не мог выполнить как WP_Query из-за таймаута, в 2-секундный запрос. Хотя array_map снова существенно замедляет его...

Есть ли какая-то поддержка для создания высокопроизводительного решения, возвращающего все метаданные в рамках WP_Query?

Я столкнулся с ситуацией, когда мне нужно было выполнить эту задачу для создания CSV-документа, и в итоге я работал напрямую с MySQL. Мой код объединяет таблицы постов и метаданных для получения информации о ценах WooCommerce. Предложенное ранее решение требовало использования псевдонимов таблиц в SQL-запросе для корректной работы.
SELECT p.ID, p.post_title,
MAX(CASE WHEN pm1.meta_key = '_price' then pm1.meta_value ELSE NULL END) as price,
MAX(CASE WHEN pm1.meta_key = '_regular_price' then pm1.meta_value ELSE NULL END) as regular_price,
MAX(CASE WHEN pm1.meta_key = '_sale_price' then pm1.meta_value ELSE NULL END) as sale_price,
MAX(CASE WHEN pm1.meta_key = '_sku' then pm1.meta_value ELSE NULL END) as sku
FROM wp_posts p LEFT JOIN wp_postmeta pm1 ON ( pm1.post_id = p.ID)
WHERE p.post_type in('product', 'product_variation') AND p.post_status = 'publish'
GROUP BY p.ID, p.post_title
Однако стоит предупредить, что WooCommerce создал более 300 тысяч строк в таблице метаданных, поэтому она оказалась очень большой и, как следствие, работала очень медленно.

для тех, кто пытается выжать больше производительности из запроса postmeta, вот ещё это: https://wordpress.stackexchange.com/a/392967/27357

ВЕРСИЯ БЕЗ SQL:
Получить все записи и все их мета-значения (метаданные) без использования SQL:
Допустим, у вас есть список ID записей, хранящийся в виде массива ID, например:
$post_ids_list = [584, 21, 1, 4, ...];
Получить все записи и все метаданные одним запросом без использования SQL невозможно,
поэтому нам нужно выполнить 2 запроса (всего лишь 2):
1. Получаем все записи (используя WP_Query)
$request = new WP Query([
'post__in' => $post_ids_list,
'ignore_sticky_posts' => true, //если нужно игнорировать "закрепленные" записи
]);
(Не забудьте вызвать wp_reset_postdata();
если после этого будете использовать "цикл" ;) )
2. Обновляем кеш метаданных
//не путайте: "post" здесь означает тип контента (запись X пользователь X ...), НЕ тип записи ;)
update_meta_cache('post', $post_ids_list);
Для получения метаданных просто используйте стандартную функцию get_post_meta()
, которая, как отметил @Otto:
сначала проверяет кеш :)
Примечание: Если вам не нужны другие данные записей (например, заголовок, содержимое и т.д.), можно выполнить только пункт 2. :-)

используя решение от trevor и модифицируя его для работы с вложенными SQL-запросами. Данный код не тестировался.
global $wpdb;
$query = "
SELECT p.*, (select pm.* From $wpdb->postmeta AS pm WHERE pm.post_id = p.ID)
FROM $wpdb->posts p
WHERE p.post_type = 'product' and p.post_status = 'publish'
";
$products = $wpdb->get_results($query);

Этот вопрос был задан около 12 лет назад, но я столкнулся с этой проблемой совсем недавно, когда мне нужно было искать типы записей по определенному критерию, более сложному, чем то, что можно сделать с базовым WP_Query. Я использовал код ниже для выполнения очень похожей задачи.
SELECT p.ID,p.post_title,pm1.meta_value as lat,pm2.meta_value as lng,pm3.meta_value as city,pm4.meta_value as state,pm5.meta_value as zip
FROM `wp_posts` p
LEFT JOIN `wp_postmeta` pm1 ON pm1.post_id = p.ID AND pm1.meta_key = '_lat'
LEFT JOIN `wp_postmeta` pm2 ON pm2.post_id = p.ID AND pm2.meta_key = '_lng'
LEFT JOIN `wp_postmeta` pm3 ON pm3.post_id = p.ID AND pm3.meta_key = '_city'
LEFT JOIN `wp_postmeta` pm4 ON pm4.post_id = p.ID AND pm4.meta_key = '_state'
LEFT JOIN `wp_postmeta` pm5 ON pm5.post_id = p.ID AND pm5.meta_key = '_zip'
WHERE post_type = "{posttype}" and post_status = "publish";
Другие ответы на этот вопрос предлагают решения, отличные от простого использования LEFT JOIN, как сделал автор изначально. В результате тестирования это действительно оказалось самым быстрым способом получения данных.
SELECT p.ID,
p.post_title,
MAX(CASE WHEN pm1.meta_key = '_lat' then pm1.meta_value ELSE NULL END) as lat,
MAX(CASE WHEN pm1.meta_key = '_lng' then pm1.meta_value ELSE NULL END) as lng,
MAX(CASE WHEN pm1.meta_key = '_city' then pm1.meta_value ELSE NULL END) as city,
MAX(CASE WHEN pm1.meta_key = '_state' then pm1.meta_value ELSE NULL END) as state,
MAX(CASE WHEN pm1.meta_key = '_zip' then pm1.meta_value ELSE NULL END) as zip
FROM
wp_posts p LEFT JOIN wp_postmeta pm1 ON pm1.post_id = p.ID WHERE p.post_type = '{posttype}' AND p.post_status = 'publish'
GROUP BY
p.ID,p.post_title;
Этот запрос очень похож на другой ответ в этом посте от Terry и Ethan и занял 0.0228 секунды для выполнения, в то время как первый запрос занял около 0.0034 секунды, то есть примерно в 6.7 раза быстрее.
Тестирование проводилось на базе данных с всего 322 записями, но при тестировании без ограничения по типу записи и запросе 5234 записей, приведенный выше запрос показал время 0.2408 секунды, а первый запрос - 0.0029 секунды, то есть примерно в 83 раза быстрее. Не уверен, почему он оказался быстрее с 5234 записями по сравнению с 332 записями (я проверил это дважды, чтобы убедиться).
Кратко: для тех, кто ищет самое быстрое решение в 2024 году с настройками WordPress по умолчанию, использование запроса с LEFT JOIN, как показано выше, кажется самым быстрым решением.

Я также столкнулся с проблемой метаполей с множественными значениями. Проблема кроется в самом WordPress. Посмотрите в файле wp-includes/meta.php. Найдите эту строку:
$where[$k] = ' (' . $where[$k] . $wpdb->prepare( "CAST($alias.meta_value AS {$meta_type}) {$meta_compare} {$meta_compare_string})", $meta_value );
Проблема в операторе CAST. В запросе для метазначений переменная $meta_type установлена в CHAR. Я не знаю деталей, как преобразование значения в CHAR влияет на сериализованную строку, но для исправления можно удалить преобразование, чтобы SQL выглядел так:
$where[$k] = ' (' . $where[$k] . $wpdb->prepare( "$alias.meta_value {$meta_compare} {$meta_compare_string})", $meta_value );
Хотя это и работает, вы вмешиваетесь во внутренности WordPress, поэтому другие функции могут сломаться, и это не постоянное решение, особенно если вам потребуется обновить WordPress.
Я исправил это, скопировав SQL, сгенерированный WordPress для нужного метазапроса, а затем написал PHP-код для добавления дополнительных условий AND для искомых meta_values и использовал $wpdb->get_results($sql) для получения итогового результата. Костыльно, но работает.
