Forma más eficiente de obtener entradas con postmeta
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!

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.

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

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

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é.

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.

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.

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?

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.

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...

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

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.

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

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. :-)

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

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.

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.
