Consulta meta_query extremadamente lenta en WordPress - Soluciones

23 ago 2014, 22:51:15
Vistas: 16.1K
Votos: 11

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    
1
Comentarios

¿Puedes añadir la explicación de MySQL (EXPLAIN SELECT …, en tu sistema) de la consulta resultante?

David David
24 ago 2014 17:41:59
Todas las respuestas a la pregunta 5
16

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

24 ago 2014 17:21:06
Comentarios

¡Muchas gracias por tu ayuda! Lo probaré y te avisaré.

user1706680 user1706680
24 ago 2014 21:12:49

¡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!

user1706680 user1706680
30 ago 2014 22:53:37

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.

user1706680 user1706680
31 ago 2014 01:44:24

¡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?

bonger bonger
31 ago 2014 03:01:46

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.

user1706680 user1706680
31 ago 2014 11:30:26

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.

bonger bonger
31 ago 2014 17:00:57

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

user1706680 user1706680
3 sept 2014 12:09:42

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.

bonger bonger
3 sept 2014 16:23:16

¡Wohooo, funciona! ¿Puedo agradecerte con una pequeña donación?

user1706680 user1706680
3 sept 2014 18:08:08

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

bonger bonger
3 sept 2014 18:38:59

¿Sabes si esto funciona en la 4.1.1?

user1706680 user1706680
19 feb 2015 11:05:47

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

bonger bonger
2 mar 2015 18:26:26

No funciona en WP 4.2.4. Sigue siendo muy lento.

vee vee
13 ago 2015 17:59:51

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

bonger bonger
15 ago 2015 15:18:37

@bonger No te preocupes, no es tu culpa.

vee vee
15 ago 2015 23:04:35

Solo mi opinión: resuelvo estos problemas usando 2 consultas wp separadas y luego uniendo los resultados programáticamente mediante PHP - es razonablemente rápido (mi ejemplo reciente, los tiempos de carga pasan de 4s a 0.3)

trainoasis trainoasis
9 dic 2019 13:03:33
Mostrar los 11 comentarios restantes
2

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.

25 ago 2014 11:37:44
Comentarios

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.

bonger bonger
25 ago 2014 14:45:19

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

bonger bonger
25 ago 2014 19:24:36
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;
}

}
8 sept 2016 01:01:49
1

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

23 may 2017 05:10:04
Comentarios

Muy buena información para dar una idea de lo que está pasando y lo que se puede hacer, buen material

sMyles sMyles
4 ene 2022 21:20:03
2

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.

23 ago 2014 23:07:12
Comentarios

Gracias por el consejo – actualmente solo tengo una publicación, así que cambiar post_per_page no cambiará nada.

user1706680 user1706680
23 ago 2014 23:23:10

Ok, si recién estás empezando, quizás puedas cambiar el número de claves. ¿Qué opinas?

Tomás Cot Tomás Cot
23 ago 2014 23:39:28