Forma más eficiente de obtener entradas con postmeta

11 ene 2012, 00:18:46
Vistas: 64.6K
Votos: 48

Necesito obtener un grupo de entradas con sus metadatos. Por supuesto, no se pueden obtener metadatos con una consulta estándar de posts, así que generalmente hay que hacer un get_post_custom() para cada entrada.

Estoy intentando con una consulta personalizada, así:

$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'
");

Parece funcionar. Falla si usas cualquiera de esos campos meta de una manera que permita múltiples valores meta para el mismo post. No puedo pensar en un join que haga eso.

Entonces, pregunta 1: ¿Existe algún join, subconsulta o lo que sea para incluir campos meta con múltiples valores?

Pero pregunta 2: ¿Vale la pena? ¿Cuántos joins a la tabla postmeta debo agregar antes de que un enfoque de 2 consultas sea preferible? Podría obtener todos los datos de posts en una consulta, luego obtener todos los postmeta relevantes en otra, y combinar los meta con los datos del post en un conjunto de resultados en PHP. ¿Terminaría siendo más rápido que una única consulta SQL cada vez más compleja, si es que es posible?

Siempre pienso: "Dale todo el trabajo posible a la base de datos". ¡No estoy seguro en este caso!

4
Comentarios

No estoy seguro si realmente quieres hacer los joins. La combinación de get_posts() y get_post_meta() te devuelve los mismos datos. De hecho, es menos eficiente usar los joins ya que podrías estar recuperando datos que no usarás después.

rexposadas rexposadas
11 ene 2012 01:00:38

¿Los metadatos de los posts no se cachean automáticamente de todos modos?

Manny Fleurmond Manny Fleurmond
11 ene 2012 03:13:23

@rxn, si tengo varios cientos de posts devueltos (son un custom post type), seguramente es una carga pesada para la base de datos hacer get_posts(), y luego get_post_meta() para cada uno de ellos?

@MannyFleurmond, es difícil encontrar información concreta sobre el caching incorporado de WP, pero por lo que sé, cachearía cosas por solicitud. La llamada al servidor para obtener estos datos es una llamada AJAX, y no creo que nada más vaya a obtener datos antes que esto.

Steve Taylor Steve Taylor
11 ene 2012 15:01:25

En realidad, estoy optando por múltiples consultas y almacenando en caché los resultados. Resulta que no solo necesitamos meta datos de las publicaciones, incluyendo campos que tienen múltiples valores, sino también datos de usuarios conectados a las publicaciones a través de campos meta (dos conjuntos de estos), además de los meta datos de esos usuarios. ¡Definitivamente SQL puro no es una opción!

Steve Taylor Steve Taylor
11 ene 2012 16:20:29
Todas las respuestas a la pregunta 8
0
64

La información de metadatos de las publicaciones se almacena automáticamente en caché en la memoria para una consulta estándar WP_Query (y para la consulta principal), a menos que especifiques lo contrario utilizando el parámetro update_post_meta_cache.

Por lo tanto, no deberías escribir tus propias consultas para esto.

Cómo funciona el almacenamiento en caché de metadatos para consultas normales:

Si el parámetro update_post_meta_cache en WP_Query no está establecido en false, entonces, después de recuperar las publicaciones de la base de datos, se llamará a la función update_post_caches(), que a su vez llama a update_postmeta_cache().

La función update_postmeta_cache() es un envoltorio para update_meta_cache() y esencialmente realiza una consulta SELECT simple con todos los ID's de las publicaciones recuperadas. Esto obtendrá todos los metadatos de las publicaciones en la consulta y guardará esos datos en la caché de objetos (utilizando wp_cache_add()).

Cuando haces algo como get_post_custom(), primero verifica esa caché de objetos. Así que no está realizando consultas adicionales para obtener los metadatos en este punto. Si has obtenido la publicación en una WP_Query, los metadatos ya están en memoria y se recuperan directamente desde allí.

Las ventajas aquí son muchas veces mayores que hacer una consulta compleja, pero la mayor ventaja proviene del uso de la caché de objetos. Si utilizas una solución de caché en memoria persistente como XCache, memcached, APC o similar, y tienes un plugin que puede vincular tu caché de objetos a ella (W3 Total Cache, por ejemplo), entonces toda tu caché de objetos ya está almacenada en memoria rápida. En cuyo caso, no hay ninguna consulta necesaria para recuperar tus datos; ya están en memoria. La caché de objetos persistente es excelente en muchos aspectos.

En otras palabras, tu consulta probablemente es mucho más lenta que usar una consulta adecuada y una solución simple de memoria persistente. Usa la consulta normal WP_Query. Ahorra esfuerzo.

Adicional: Por cierto, update_meta_cache() es inteligente. No recupera metadatos para publicaciones que ya tienen sus metadatos en caché. Básicamente, no obtiene los mismos metadatos dos veces. Súper eficiente.

Adicional adicional: "Delega tanto trabajo como sea posible a la base de datos."... No, esto es la web. Aplican reglas diferentes. En general, siempre quieres delegar el menor trabajo posible a la base de datos, si es factible. Las bases de datos son lentas o están mal configuradas (si no la configuraste específicamente, puedes apostar que esto es cierto). A menudo son compartidas entre muchos sitios y están sobrecargadas en cierta medida. Por lo general, tienes más servidores web que bases de datos. En general, simplemente quieres obtener los datos que necesitas de la base de datos de la manera más rápida y simple posible, y luego procesarlos usando el código del lado del servidor web. Como principio general, por supuesto, cada caso es diferente.

24 ene 2012 07:56:35
4
45

Recomendaría una consulta de pivote. Usando tu ejemplo:

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 ene 2012 07:21:46
Comentarios

Esta respuesta debería marcarse como correcta.

Luke Luke
13 mar 2012 08:59:26

Si estás buscando una consulta a la base de datos, esta es la respuesta correcta

Alex Seidlitz Alex Seidlitz
9 abr 2015 01:34:01

Esta consulta redujo mi tiempo de ~25 seg a ~3 seg cuando usaba WP_Query. Mi requerimiento era ejecutarla solo una vez, así que no necesitaba caché.

Kush Kush
3 jun 2018 18:58:55

¡Qué consulta tan elegante! Los left joins no funcionaron en mi caso porque son bastante lentos... esto me ayudó a exportar miles de publicaciones en menos de un segundo. +1

GDY GDY
1 dic 2021 14:29:30
6
12

Me encontré con un caso donde necesitaba recuperar rápidamente una gran cantidad de publicaciones junto con su información meta asociada. Necesitaba recuperar alrededor de 2000 publicaciones.

Primero probé usando la sugerencia de Otto - ejecutando WP_Query::query para todas las publicaciones, y luego iterando y ejecutando get_post_custom para cada publicación. Esto tomó, en promedio, unos 3 segundos en completarse.

Luego probé la consulta pivot de Ethan (aunque no me gustó tener que pedir manualmente cada meta_key que me interesaba). Todavía tuve que iterar a través de todas las publicaciones recuperadas para deserializar el meta_value. Esto tomó, en promedio, unos 1.3 segundos en completarse.

Finalmente probé usando la función GROUP_CONCAT, y obtuve el mejor resultado. Aquí está el código:

global $wpdb;
$wpdb->query('SET SESSION group_concat_max_len = 10000'); // necesario para obtener más de 1024 caracteres en las columnas 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);

// procesa los productos para tener un miembro ->meta con los valores deserializados como se espera
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);

Esto tomó en promedio 0.7 segundos. Eso es aproximadamente un cuarto del tiempo de la solución WP get_post_custom() y la mitad de la solución de consulta pivot.

Quizás esto le sea útil a alguien.

7 oct 2012 18:23:05
Comentarios

Me interesaría saber qué resultados obtienes con una solución de caché de objetos persistente. El caché de objetos a veces puede ser más lento para el caso base, dependiendo de tu base de datos y configuración, pero en entornos reales con la mayoría de hosts los resultados pueden variar ampliamente. El almacenamiento en caché basado en memoria es increíblemente rápido.

Otto Otto
13 oct 2012 06:07:26

Hola @Otto. Independientemente del método que use para obtener los datos, definitivamente quiero almacenar en caché el resultado. He intentado usar la API de transitorios para hacerlo, pero me encuentro con problemas de memoria. La cadena serializada para mis 2000 objetos ocupa ~8M y set_transient() falla (memoria agotada). También tuve que cambiar el ajuste max_allowed_packet de MySQL. Voy a explorar guardar el caché en archivos, pero no estoy seguro aún del rendimiento allí. ¿Hay alguna forma de almacenar en caché en memoria que persista entre solicitudes?

Trevor Mills Trevor Mills
29 oct 2012 22:52:31

Sí, si tienes una caché de memoria persistente (XCache, memcached, APC, etc.) y usas un plugin de caché de objetos (W3 Total Cache soporta muchos tipos de cachés en memoria), entonces almacena todo el caché de objetos en memoria, proporcionándote una aceleración múltiple de prácticamente todo.

Otto Otto
10 nov 2012 19:09:20

Estoy devolviendo 6000 elementos para usar en un esquema de filtrado con backbone/underscore js. Esto tomaba una consulta personalizada de 6s que ni siquiera podía ejecutar como WP_Query porque excedía el tiempo límite, y lo convertí en una consulta de 2s. Aunque el array_map lo ralentiza bastante de nuevo...

Jake Jake
14 dic 2013 21:51:37

¿Existe algún soporte para construir un sistema de alto rendimiento que devuelva todos los metadatos dentro de un WP_Query?

atwellpub atwellpub
13 sept 2014 00:45:28

Estaba trabajando en una solución similar, una pena que SQL no parezca poder devolver arrays u objetos agrupados. Podrías probar con select anidados, pero por mi experiencia previa con SQL, parecen ralentizar demasiado la petición.

Jonathan Joosten Jonathan Joosten
24 sept 2014 10:50:51
Mostrar los 1 comentarios restantes
1

Me encontré en una situación donde necesitaba realizar esta tarea para finalmente crear un documento CSV. Terminé trabajando directamente con MySQL para hacerlo. Mi código une las tablas de posts y meta para recuperar la información de precios de WooCommerce, la solución publicada anteriormente requería que usara alias de tabla en el SQL para que funcionara correctamente.

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

Sin embargo, ten en cuenta que WooCommerce creó más de 300K filas en mi tabla meta, por lo que era muy grande y, por lo tanto, muy lenta.

9 sept 2015 16:19:48
Comentarios

para aquellos que están intentando exprimir más rendimiento de la consulta postmeta, esto también puede ayudar: https://wordpress.stackexchange.com/a/392967/27357

Terry Kernan Terry Kernan
23 oct 2021 08:00:12
0

VERSIÓN SIN SQL:

Obtener todos los posts y todos sus valores meta (metas) sin usar SQL:

Digamos que tienes una lista de IDs de posts almacenados como un array de IDs, algo como:

$post_ids_list = [584, 21, 1, 4, ...];

Obtener todos los posts y todas las metas en 1 consulta no es posible sin usar al menos un poco de SQL, así que debemos hacer 2 consultas (solo 2):

1. Obtener todos los posts (usando WP_Query)

$request = new WP Query([
  'post__in' => $post_ids_list,
  'ignore_sticky_posts' => true, //si quieres ignorar los posts "stickies"
]);

(No olvides llamar a wp_reset_postdata(); si vas a hacer un "loop" después ;) )

2. Actualizar la caché de metas

//no te confundas aquí: "post" se refiere al tipo de contenido (post X user X ...), NO al tipo de post ;)
update_meta_cache('post', $post_ids_list);

Para obtener los datos meta simplemente usa el estándar get_post_meta() que, como @Otto señaló:
¡primero revisa la caché! :)

Nota: Si no necesitas otros datos de los posts (como título, contenido, ...) puedes hacer solo el paso 2. :-)

31 ene 2017 14:27:15
0

utilizando la solución de Trevor y modificándola para que funcione con SQL anidado. Esto no ha sido probado.

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 sept 2014 11:01:35
0

Esta pregunta fue formulada hace unos 12 años, pero me encontré con este problema recientemente cuando necesité buscar tipos de publicación con un criterio más complejo de lo que se puede hacer con una WP_Query básica. Utilicé el siguiente código para hacer algo muy similar.

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";

Otras respuestas en esta pregunta intentan algo diferente a simplemente usar LEFT JOIN como hizo el autor originalmente. A través de algunas pruebas, parece ser la forma más rápida de obtener los datos.

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;

Esta consulta es muy similar a otra respuesta en esta publicación de Terry y Ethan y tardó 0.0228 segundos en completarse, mientras que la primera consulta tardó unos 0.0034 segundos en completarse, es decir, aproximadamente 6.7 veces más rápida.

Esto se realizó en una base de datos con solo 322 publicaciones, pero cuando se probó sin limitarla al tipo de publicación personalizada y consultando 5234 publicaciones, la consulta anterior obtuvo un tiempo de 0.2408s y la primera consulta obtuvo un tiempo de 0.0029s, es decir, aproximadamente 83 veces más rápida. No estoy seguro de cómo fue más rápida con 5234 publicaciones frente a 332 publicaciones (lo probé dos veces para asegurarme).

En resumen: Para cualquiera que busque la solución más rápida en 2024 con una configuración predeterminada de WordPress, simplemente usar la consulta LEFT JOIN anterior parece ser la solución más rápida.

25 abr 2024 00:09:04
1
-1

También me encontré con el problema de los campos meta con múltiples valores. El problema está en el propio WordPress. Si revisas en wp-includes/meta.php, busca esta línea:

$where[$k] = ' (' . $where[$k] . $wpdb->prepare( "CAST($alias.meta_value AS {$meta_type}) {$meta_compare} {$meta_compare_string})", $meta_value );

El problema está con la sentencia CAST. En una consulta para valores meta, la variable $meta_type está configurada como CHAR. No conozco los detalles de cómo afecta el CAST del valor a CHAR a la cadena serializada, pero para solucionarlo, puedes eliminar el CAST para que el SQL quede así:

$where[$k] = ' (' . $where[$k] . $wpdb->prepare( "$alias.meta_value {$meta_compare} {$meta_compare_string})", $meta_value );

Ahora bien, aunque esto funciona, estás modificando los archivos internos de WordPress, por lo que otras cosas podrían romperse, y no es una solución permanente, asumiendo que necesitarás actualizar WordPress.

La forma en que lo he solucionado es copiar el SQL generado por WordPress para la consulta meta que quiero y luego escribir algo de PHP para agregar declaraciones AND adicionales para los meta_values que estoy buscando, usando $wpdb->get_results($sql) para el resultado final. Es un poco improvisado, pero funciona.

23 abr 2012 22:25:59
Comentarios

No lo he probado, pero aprovechar el filtro get_meta_sql que sigue a esta línea sería, por supuesto, preferible a hackear el código núcleo.

Steve Taylor Steve Taylor
25 abr 2012 13:29:14