Consulta meta_query extremadamente lenta en WordPress - Soluciones
Tengo una consulta meta personalizada que es extremadamente lenta o ni siquiera carga hasta el final. Con hasta tres arrays
en 'meta_query'
la consulta funciona bien, pero con cuatro o más deja de funcionar.
Al buscar una razón encontré este post pero no estoy familiarizado con consultas personalizadas a la base de datos.
¡Cualquier ayuda es muy apreciada! ¡Gracias!
<?php
$args = array(
'post_type' => $post_type,
'posts_per_page' => -1,
'meta_query' => array(
'relation' => 'OR',
array(
'key'=>'_author',
'value'=> $author_single["fullname"],
'compare' => '='
),
array(
'key'=>'_publisher',
'value'=> $author_single["fullname"],
'compare' => '='
),
array(
'key'=>'_contributor_1',
'value'=> $author_single["fullname"],
'compare' => '='
),
array(
'key'=>'_contributor_2',
'value'=> $author_single["fullname"],
'compare' => '='
),
array(
'key'=>'_contributor_3',
'value'=> $author_single["fullname"],
'compare' => '='
)
)
);
$posts = new WP_Query($args);
if( $posts->have_posts() ) : while( $posts->have_posts() ) : $posts->the_post(); ?>
<li><a href="<?php echo get_the_permalink(); ?>"><?php the_title(); ?></a></li>
<?php endwhile; endif; ?>
– – – – –
Código actualizado con las adiciones de boger:
page.php
<?php
$args = array(
'post_type' => $post_type,
'posts_per_page' => -1,
'meta_query' => array(
'relation' => 'OR',
array(
'key'=>'_author',
'value'=> $author_single["fullname"],
'compare' => '='
),
array(
'key'=>'_publisher',
'value'=> $author_single["fullname"],
'compare' => '='
),
array(
'key'=>'_contributor_1',
'value'=> $author_single["fullname"],
'compare' => '='
),
array(
'key'=>'_contributor_2',
'value'=> $author_single["fullname"],
'compare' => '='
),
array(
'key'=>'_contributor_3',
'value'=> $author_single["fullname"],
'compare' => '='
)
)
);
add_filter( 'posts_clauses', 'wpse158898_posts_clauses', 10, 2 );
$posts = new WP_Query($args);
if( $posts->have_posts() ) : while( $posts->have_posts() ) : $posts->the_post(); ?>
<li><a href="<?php echo get_the_permalink(); ?>"><?php the_title(); ?></a></li>
<?php endwhile; endif;
remove_filter( 'posts_clauses', 'wpse158898_posts_clauses', 10 ); ?>
functions.php
function wpse158898_posts_clauses( $pieces, $query ) {
global $wpdb;
$relation = isset( $query->meta_query->relation ) ? $query->meta_query->relation : 'AND';
if ( $relation != 'OR' ) return $pieces; // Solo tiene sentido si es OR
$prepare_args = array();
$key_value_compares = array();
foreach ( $query->meta_query->queries as $meta_query ) {
// No funciona para IN, NOT IN, BETWEEN, NOT BETWEEN, NOT EXISTS
if ( ! isset( $meta_query['value'] ) || is_array( $meta_query['value'] ) ) return $pieces; // Salir si no hay valor o es array
$key_value_compares[] = '(pm.meta_key = %s AND pm.meta_value ' . $meta_query['compare'] . ' %s)';
$prepare_args[] = $meta_query['key'];
$prepare_args[] = $meta_query['value'];
}
$sql = ' JOIN ' . $wpdb->postmeta . ' pm on pm.post_id = ' . $wpdb->posts . '.ID'
. ' AND (' . implode( ' ' . $relation . ' ', $key_value_compares ) . ')';
array_unshift( $prepare_args, $sql );
$pieces['join'] = call_user_func_array( array( $wpdb, 'prepare' ), $prepare_args );
$pieces['where'] = preg_replace( '/ AND[^w]+wp_postmeta.*$/s', '', $pieces['where'] ); // Eliminar cláusulas postmeta
return $pieces;
}
– – –
$posts->request
muestra
$args = array(
'post_type' => $post_type,
'posts_per_page' => -1,
'meta_query' => array(
'relation' => 'OR',
array(
'key'=>'_author',
'value'=> "Hanna Meier",
'compare' => '='
),
array(
'key'=>'_publisher',
'value'=> "Friedhelm Peters",
'compare' => '='
)
)
);
sin la consulta personalizada
SELECT wp_vacat_posts.* FROM wp_vacat_posts INNER JOIN wp_vacat_postmeta ON (wp_vacat_posts.ID = wp_vacat_postmeta.post_id)
INNER JOIN wp_vacat_postmeta AS mt1 ON (wp_vacat_posts.ID = mt1.post_id) WHERE 1=1 AND wp_vacat_posts.post_type = 'product' AND (wp_vacat_posts.post_status = 'publish' OR wp_vacat_posts.post_status = 'private') AND ( (wp_vacat_postmeta.meta_key = '_author' AND CAST(wp_vacat_postmeta.meta_value AS CHAR) = 'Hanna Meier')
OR (mt1.meta_key = '_publisher' AND CAST(mt1.meta_value AS CHAR) = 'Friedhelm Peters') ) GROUP BY wp_vacat_posts.ID ORDER BY wp_vacat_posts.post_date DESC
con la consulta personalizada
SELECT wp_vacat_posts.* FROM wp_vacat_posts
JOIN wp_vacat_postmeta pm on pm.post_id = wp_vacat_posts.ID AND ((pm.meta_key = '_author' AND pm.meta_value = 'Hanna Meier') OR (pm.meta_key = '_publisher' AND pm.meta_value = 'Friedhelm Peters')) WHERE 1=1 AND wp_vacat_posts.post_type = 'product' AND (wp_vacat_posts.post_status = 'publish' OR wp_vacat_posts.post_status = 'private') AND ( (wp_vacat_postmeta.meta_key = '_author' AND CAST(wp_vacat_postmeta.meta_value AS CHAR) = 'Hanna Meier')
OR (mt1.meta_key = '_publisher' AND CAST(mt1.meta_value AS CHAR) = 'Friedhelm Peters') ) GROUP BY wp_vacat_posts.ID ORDER BY wp_vacat_posts.post_date DESC

Me he encontrado con este problema y parece que MySQL no maneja bien los múltiples joins a la misma tabla (wp_postmeta) y las condiciones WHERE con OR que WordPress genera aquí. Lo solucioné reescribiendo el join y el where como se menciona en el post que enlazas - aquí tienes una versión que debería funcionar en tu caso (actualizado para WP 4.1.1) (actualizado para WP 4.2.4):
function wpse158898_posts_clauses( $pieces, $query ) {
global $wpdb;
$relation = isset( $query->meta_query->relation ) ? $query->meta_query->relation : 'AND';
if ( $relation != 'OR' ) return $pieces; // Solo tiene sentido si es OR.
$prepare_args = array();
$key_value_compares = array();
foreach ( $query->meta_query->queries as $key => $meta_query ) {
if ( ! is_array( $meta_query ) continue;
// No funciona para IN, NOT IN, BETWEEN, NOT BETWEEN, NOT EXISTS.
if ( $meta_query['compare'] === 'EXISTS' ) {
$key_value_compares[] = '(pm.meta_key = %s)';
$prepare_args[] = $meta_query['key'];
} else {
if ( ! isset( $meta_query['value'] ) || is_array( $meta_query['value'] ) ) return $pieces; // Salir si no hay valor o es array.
$key_value_compares[] = '(pm.meta_key = %s AND pm.meta_value ' . $meta_query['compare'] . ' %s)';
$prepare_args[] = $meta_query['key'];
$prepare_args[] = $meta_query['value'];
}
}
$sql = ' JOIN ' . $wpdb->postmeta . ' pm on pm.post_id = ' . $wpdb->posts . '.ID'
. ' AND (' . implode( ' ' . $relation . ' ', $key_value_compares ) . ')';
array_unshift( $prepare_args, $sql );
$pieces['join'] = call_user_func_array( array( $wpdb, 'prepare' ), $prepare_args );
// Eliminar cláusulas postmeta.
$wheres = explode( "\n", $pieces[ 'where' ] );
foreach ( $wheres as &$where ) {
$where = preg_replace( array(
'/ +\( +' . $wpdb->postmeta . '\.meta_key .+\) *$/',
'/ +\( +mt[0-9]+\.meta_key .+\) *$/',
'/ +mt[0-9]+.meta_key = \'[^\']*\'/',
), '(1=1)', $where );
}
$pieces[ 'where' ] = implode( '', $wheres );
$pieces['orderby'] = str_replace( $wpdb->postmeta, 'pm', $pieces['orderby'] ); // El ordenamiento no funcionará realmente pero al menos que no falle.
return $pieces;
}
y luego alrededor de tu consulta:
add_filter( 'posts_clauses', 'wpse158898_posts_clauses', 10, 2 );
$posts = new WP_Query($args);
remove_filter( 'posts_clauses', 'wpse158898_posts_clauses', 10 );
Adenda:
La solución para esto, ticket 24093, no llegó a la versión 4.0 (además no solucionaba este problema de todos modos), así que originalmente intenté una versión generalizada de lo anterior pero es demasiado inestable para intentar tal solución, así que la he eliminado...

¡Tu código parece funcionar muy bien! No lo he probado con todas mis publicaciones aún, pero con las que he probado hasta ahora es realmente rápido. ¡Muchas gracias de nuevo—realmente aprecio tu ayuda!

Un paso atrás: ¿Tienes idea de por qué no obtengo ningún resultado cuando uso tu consulta personalizada? Cuando uso el código de mi publicación original con solo dos meta consultas obtengo los resultados esperados (títulos enlazados), pero cuando uso tu consulta no obtengo nada.

¡No obtener los mismos resultados es como retroceder diez pasos! No entiendo por qué no funcionaría - ¿los $args son los mismos que arriba, es decir, ningún nuevo, solo post_type, posts_per_page y el meta_query con X entradas?

Editaré mi publicación original con las adiciones que hiciste. No realicé cambios en mi código... Si necesitas más información, házmelo saber.

Todavía no logro entender qué está saliendo mal, solo puedo sugerir depurar la solicitud generada, por ejemplo, colocando print($posts->request);
(o error_log($posts->request);
) directamente después de $posts = new WP_Query($args);
para ver si hay algo notablemente incorrecto.

¡Gracias por el soporte! Encontrarás los resultados de print($posts->request);
en mi código original al final—tanto con tu consulta personalizada como sin ella. El error_log($posts->request);
es para ambos 1
.

Ah, ya veo - estás usando un prefijo que la respuesta no tiene en cuenta al eliminar la cláusula where - necesita ser '/ AND[^w]+' . $wpdb->postmeta . '.*$/s'
en el preg_replace - actualizaré la respuesta.

¡Hurra, por fin! Y es muy amable de tu parte ofrecer - si lo deseas, dona a tu organización benéfica favorita (como dicen en la televisión) - tú también contribuiste corrigiendo mi respuesta, y espero generalizarla en un futuro no muy lejano (¡nota la imprecisión no comprometedora!) dado que la corrección para WP no estará en la versión 4.0.

Hola, no, no funciona, necesita ignorar entradas no query en el array meta_query además de adaptarse a referencias no aliadas en la cláusula orderby - como mínimo, puede necesitar otros ajustes, pero lo actualizaré de todos modos por ahora...

@vee sí, tienes razón, entre otras cosas el formato de la cláusula where cambió por lo que no se elimina, actualizaré la respuesta específica y eliminaré la respuesta "generalizada" ya que es bastante desesperante intentar actualizarla continuamente...

La respuesta corta es que los metadatos en WordPress no están diseñados para ser utilizados como datos relacionales. Obtener publicaciones basadas en múltiples condiciones de sus metadatos no es el propósito detrás de los metadatos. Por lo tanto, las consultas, estructuras de tablas e índices no están optimizados para eso.
La respuesta más detallada:
Lo que tu Meta-Query genera es algo como esto:
SELECT wp_4_posts.* FROM wp_4_posts
INNER JOIN wp_4_postmeta ON (wp_4_posts.ID = wp_4_postmeta.post_id)
INNER JOIN wp_4_postmeta AS mt1 ON (wp_4_posts.ID = mt1.post_id)
INNER JOIN wp_4_postmeta AS mt2 ON (wp_4_posts.ID = mt2.post_id)
INNER JOIN wp_4_postmeta AS mt3 ON (wp_4_posts.ID = mt3.post_id)
INNER JOIN wp_4_postmeta AS mt4 ON (wp_4_posts.ID = mt4.post_id)
WHERE 1=1
AND wp_4_posts.post_type = 'post'
AND (wp_4_posts.post_status = 'publish' OR wp_4_posts.post_status = 'private')
AND ( (wp_4_postmeta.meta_key = '_author' AND CAST(wp_4_postmeta.meta_value AS CHAR) = 'Test')
OR (mt1.meta_key = '_publisher' AND CAST(mt1.meta_value AS CHAR) = 'Test')
OR (mt2.meta_key = '_contributor_1' AND CAST(mt2.meta_value AS CHAR) = 'Test')
OR (mt3.meta_key = '_contributor_2' AND CAST(mt3.meta_value AS CHAR) = 'Test')
OR (mt4.meta_key = '_contributor_3' AND CAST(mt4.meta_value AS CHAR) = 'Test') ) GROUP BY wp_4_posts.ID ORDER BY wp_4_posts.post_date DESC
Analicemos cómo MySQL maneja esta consulta (EXPLAIN
):
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE wp_4_posts range PRIMARY,type_status_date type_status_date 124 NULL 5 Using where; Using temporary; Using filesort
1 SIMPLE wp_4_postmeta ref post_id,meta_key post_id 8 wordpress.wp_4_posts.ID 1
1 SIMPLE mt1 ref post_id,meta_key post_id 8 wordpress.wp_4_posts.ID 1
1 SIMPLE mt2 ref post_id,meta_key post_id 8 wordpress.mt1.post_id 1 Using where
1 SIMPLE mt3 ref post_id,meta_key post_id 8 wordpress.wp_4_posts.ID 1
1 SIMPLE mt4 ref post_id,meta_key post_id 8 wordpress.wp_4_postmeta.post_id 1 Using where
Lo que puedes observar es que MySQL realiza una selección en wp_posts
y une 5 veces la tabla wp_postmeta
. El tipo ref
indica que MySQL debe examinar todas las filas en esta tabla, haciendo coincidir el índice (post_id, meta_key) comparando un valor de columna no indexado con tu cláusula where
, y eso para cada combinación de filas de la tabla anterior. El manual de MySQL indica: "Si la clave que se usa coincide solo con unas pocas filas, este es un buen tipo de unión". Y ese es el primer problema: en un sistema WordPress promedio, el número de metadatos por publicación puede crecer fácilmente hasta 30-40 registros o más. La otra clave posible meta_key
crece con tu cantidad de publicaciones. Entonces, si tienes 100 publicaciones y cada una tiene un meta _publisher
, habrá 100 filas con este valor como meta_key
en wp_postmeta
, por supuesto.
Para manejar todos estos posibles resultados, MySQL crea una tabla temporal (using temporary
). Si esta tabla se vuelve demasiado grande, el servidor generalmente la almacena en el disco en lugar de la memoria. Otro posible cuello de botella.
Soluciones posibles
Como se describe en las respuestas existentes, podrías intentar optimizar la consulta por tu cuenta. Eso puede funcionar bien para tus necesidades, pero podría causar problemas a medida que las tablas de publicaciones/metadatos crezcan.
Pero si deseas utilizar la API de Consultas de WordPress, deberías considerar usar Taxonomías para almacenar datos por los que quieras buscar publicaciones.

Ah, así que eso "explica" por qué obtienes el impacto la primera vez que se ejecuta la consulta, pero no en ejecuciones posteriores inmediatas - ¡gran información! Pero no puedo estar de acuerdo con tu punto sobre usar taxonomías en su lugar, eso parece antinatural en el mejor de los casos, si no inviable. ¿Por qué no deberías consultar metadatos relacionalmente? - WP tiene mucho código para manejar exactamente ese uso, ¡y lo hace él mismo! Para mí es simplemente un error en WP - debería ser capaz de generar un código mucho mejor aquí, en lugar de generar tontamente unión tras unión.

Hay un ticket de WP (¡de 3 años de antigüedad!) sobre esto 24093 que tiene un parche que lo soluciona usando subconsultas. Sin embargo, no llegará a la versión 4.0...

Puede que llegue un poco tarde al juego, pero me encontré con el mismo problema. Al construir un plugin para manejar búsquedas de propiedades, mi opción de búsqueda avanzada consultaba hasta 20 entradas meta diferentes para cada publicación para encontrar aquellas que coincidieran con los criterios de búsqueda.
Mi solución fue consultar la base de datos directamente usando el global $wpdb
. Consulté cada entrada meta individualmente y almacené los post_ids
de las publicaciones que coincidían con cada criterio. Luego hice una intersección en cada uno de los conjuntos coincidentes para obtener los post_ids
que coincidían con todos los criterios.
Mi caso era relativamente simple porque no tenía ningún elemento OR
que necesitara considerar, pero podrían incluirse con bastante facilidad. Dependiendo de qué tan compleja sea tu consulta, esta es una solución funcional y rápida. Aunque, admito que es una opción pobre en comparación con poder hacer una verdadera consulta relacional.
El código a continuación ha sido simplificado enormemente de lo que usé, pero puedes entender la idea a partir de él.
class property_search{
public function get_results($args){
$potential_ids=[];
foreach($args as $key=>$value){
$potential_ids[$key]=$this->get_ids_by_query("
SELECT post_id
FROM wp_postmeta
WHERE meta_key = '".$key."'
AND CAST(meta_value AS UNSIGNED) > '".$value."'
");//se necesitaría crear un nuevo operador para manejar cada tipo de dato y comparación.
}
$ids=[];
foreach($potential_ids as $key=>$temp_ids){
if(count($ids)==0){
$ids=$temp_ids;
}else{
$ids=array_intersect($ids,$temp_ids);
}
}
$paged = (get_query_var('paged')) ? get_query_var('paged') : 1;
$args = array(
'posts_per_page'=> 50,
'post_type'=>'property',
'post_status'=>'publish',
'paged'=>$paged,
'post__in'=>$ids,
);
$search = new WP_Query($args);
return $search;
}
public function get_ids_by_query($query){
global $wpdb;
$data=$wpdb->get_results($query,'ARRAY_A');
$results=[];
foreach($data as $entry){
$results[]=$entry['post_id'];
}
return $results;
}
}

wp_postmeta tiene índices ineficientes. Aquí hay una discusión sobre esto, junto con soluciones recomendadas:
http://mysql.rjweb.org/doc.php/index_cookbook_mysql#speeding_up_wp_postmeta

Las consultas OR son realmente costosas.
Tienes demasiadas claves, pero asumamos que no puedes cambiar eso ahora. La otra cosa que puedes hacer, sin demasiado código, es modificar el número de posts que estás obteniendo, cambia 'posts_per_page'
a 10, o un número mayor, y observa cómo cambia el rendimiento.
