Наиболее эффективный способ получения записей с postmeta

11 янв. 2012 г., 00:18:46
Просмотры: 64.6K
Голосов: 48

Мне нужно получить множество записей с их метаданными. Конечно, нельзя получить метаданные со стандартным запросом постов, поэтому обычно приходится использовать 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-запрос, если это вообще возможно?

Я всегда думаю: "Дайте как можно больше работы базе данных". Но в этом случае не уверен!

4
Комментарии

Я не уверен, что вам вообще нужно делать соединения (joins). Комбинация get_posts() и get_post_meta() возвращает те же данные. Фактически, использование соединений менее эффективно, так как вы можете получать данные, которые не будете использовать позже.

rexposadas rexposadas
11 янв. 2012 г. 01:00:38

Разве метаданные записей не кэшируются автоматически в любом случае?

Manny Fleurmond Manny Fleurmond
11 янв. 2012 г. 03:13:23

@rxn, если у меня возвращается несколько сотен записей (они относятся к пользовательскому типу записей), разве это не создаст значительную нагрузку на БД, сначала выполнив get_posts(), а затем get_post_meta() для каждой из них?

@MannyFleurmond, сложно найти точную информацию о встроенном кэшировании в WP, но насколько я знаю, оно кэширует данные в рамках одного запроса. Запрос к серверу для получения этих данных — это AJAX-вызов, и я не думаю, что что-то ещё будет получать эти данные до него.

Steve Taylor Steve Taylor
11 янв. 2012 г. 15:01:25

На самом деле, я работаю с множественными запросами и кеширую результаты. Оказывается, нам нужны не только метаданные записей, включая поля с множественными значениями, но и данные о пользователях, связанных с записями через метаполя (два набора таких полей), плюс их пользовательские метаданные. Чистый SQL определенно не подходит для этого случая!

Steve Taylor Steve Taylor
11 янв. 2012 г. 16:20:29
Все ответы на вопрос 8
0
64

Метаданные записей автоматически кэшируются в памяти для стандартного 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() умная функция. Она не будет получать метаданные для записей, у которых они уже закэшированы. По сути, она не извлекает одни и те же метаданные дважды. Очень эффективно.

Ещё дополнительно: «Передавайте как можно больше работы базе данных.»... Нет, это веб. Здесь действуют другие правила. Как правило, вы всегда хотите передавать базе данных как можно меньше работы, если это возможно. Базы данных часто медленные или плохо настроены (если вы не настраивали их специально, можете быть уверены, что это так). Часто они используются совместно многими сайтами и перегружены. Обычно у вас больше веб-серверов, чем баз данных. Как правило, вы хотите просто получить нужные данные из БД как можно быстрее и проще, а затем обработать их с помощью кода на стороне веб-сервера. Это общий принцип, конечно, все случаи разные.

24 янв. 2012 г. 07:56:35
4
45

Я бы порекомендовал использовать сводный запрос. На примере вашего кода:

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
24 янв. 2012 г. 07:21:46
Комментарии

Этот ответ следует отметить как правильный.

Luke Luke
13 мар. 2012 г. 08:59:26

Если вы ищете запрос к базе данных, это правильный ответ

Alex Seidlitz Alex Seidlitz
9 апр. 2015 г. 01:34:01

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

Kush Kush
3 июн. 2018 г. 18:58:55

Какой элегантный запрос. LEFT JOIN не подошли в моем случае из-за низкой скорости... этот вариант помог мне экспортировать тысячи записей менее чем за секунду. +1

GDY GDY
1 дек. 2021 г. 14:29:30
6
12

Я столкнулся с задачей, когда нужно быстро получить большое количество записей с их мета-данными. Мне нужно извлечь около 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, и примерно в два раза быстрее, чем вариант со сводным запросом.

Возможно, это будет полезно кому-то.

7 окт. 2012 г. 18:23:05
Комментарии

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

Otto Otto
13 окт. 2012 г. 06:07:26

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

Trevor Mills Trevor Mills
29 окт. 2012 г. 22:52:31

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

Otto Otto
10 нояб. 2012 г. 19:09:20

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

Jake Jake
14 дек. 2013 г. 21:51:37

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

atwellpub atwellpub
13 сент. 2014 г. 00:45:28

Я работал над похожим решением, жаль, что SQL, похоже, не умеет возвращать групповые массивы или объекты. Можно попробовать вложенные select'ы, но по моему предыдущему опыту работы с SQL, они слишком сильно замедляют запрос.

Jonathan Joosten Jonathan Joosten
24 сент. 2014 г. 10:50:51
Показать остальные 1 комментариев
1

Я столкнулся с ситуацией, когда мне нужно было выполнить эту задачу для создания 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 тысяч строк в таблице метаданных, поэтому она оказалась очень большой и, как следствие, работала очень медленно.

9 сент. 2015 г. 16:19:48
Комментарии

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

Terry Kernan Terry Kernan
23 окт. 2021 г. 08:00:12
0

ВЕРСИЯ БЕЗ 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. :-)

31 янв. 2017 г. 14:27:15
0

используя решение от 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);
24 сент. 2014 г. 11:01:35
0

Этот вопрос был задан около 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, как показано выше, кажется самым быстрым решением.

25 апр. 2024 г. 00:09:04
1
-1

Я также столкнулся с проблемой метаполей с множественными значениями. Проблема кроется в самом 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) для получения итогового результата. Костыльно, но работает.

23 апр. 2012 г. 22:25:59
Комментарии

Я не пробовал, но использование фильтра get_meta_sql, который следует за этой строкой, конечно, было бы предпочтительнее, чем изменение основного кода.

Steve Taylor Steve Taylor
25 апр. 2012 г. 13:29:14