web-dev-qa-db-fra.com

Méta requête terriblement lente

J'ai une méta-requête personnalisée qui est terriblement lente ou ne se charge même pas jusqu'à la fin. Avec jusqu’à trois arrays dans 'meta_query', la requête fonctionne correctement, à partir de quatre, elle ne fonctionne plus.

Lors de la recherche d'une raison, j'ai trouvé ce message mais je ne suis absolument pas familier avec les requêtes de base de données personnalisées.

Toute aide est très appréciée! Je vous remercie!

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

- - - - -

Code mis à jour avec les ajouts faits par 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; // Only makes sense if OR.
    $prepare_args = array();
    $key_value_compares = array();
    foreach ( $query->meta_query->queries as $meta_query ) {
        // Doesn't work for IN, NOT IN, BETWEEN, NOT BETWEEN, NOT EXISTS.
        if ( ! isset( $meta_query['value'] ) || is_array( $meta_query['value'] ) ) return $pieces; // Bail if no value or is 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'] ); // Zap postmeta clauses.
    return $pieces;
}

- - -

$posts->request sorties

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

sans la requête personnalisée

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   

avec la requête personnalisée

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    
6
user1706680

J'ai rencontré ce problème et il semble que MySQL ne gère pas bien les jointures multiples vers la même table (wp_postmeta) et OR-ed WHERE que WP génère ici. Je l'ai traité en réécrivant la jointure et où, comme mentionné dans le message auquel vous vous connectez - voici une version qui devrait fonctionner dans votre cas ( mis à jour pour WP 4.1.1 ) ( mis à jour pour 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; // Only makes sense if OR.
    $prepare_args = array();
    $key_value_compares = array();
    foreach ( $query->meta_query->queries as $key => $meta_query ) {
        if ( ! is_array( $meta_query ) ) continue;
        // Doesn't work for 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; // Bail if no value or is 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 );
    // Zap postmeta clauses.
    $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'] ); // Sorting won't really work but at least make it not crap out.
    return $pieces;
}

et ensuite autour de votre requête:

  add_filter( 'posts_clauses', 'wpse158898_posts_clauses', 10, 2 );
  $posts = new WP_Query($args);
  remove_filter( 'posts_clauses', 'wpse158898_posts_clauses', 10 );

Addendum:

Le correctif pour ceci, ticket 24093 , n’a pas été intégré à la version 4.0 ( et n’a pas résolu ce problème de toute façon ), si bien que j’ai tenté à l’origine une version généralisée de ce qui précède trop floconneux vraiment pour tenter une telle solution alors je l'ai enlevé ...

8
bonger

La réponse courte est que les métadonnées dans WordPress ne sont pas destinées à être utilisées pour des données relationnelles. Extraire des posts de plusieurs conditions dans ses métadonnées n’est pas l’idée derrière les métadonnées. Par conséquent, les requêtes, les structures de table et les index ne sont pas optimisés pour cela.

La réponse la plus longue:

Les résultats de votre méta-requête ressemblent à ceci:

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

Voyons comment MySQL traite cette requête (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

Maintenant, ce que vous pouvez voir, MySQL fait un select sur wp_posts et rejoint 5 fois la table wp_postmeta. Le type ref indique que MySQL doit examiner toutes les lignes de cette table, en faisant correspondre l'index (post_id, meta_key) comparant une valeur de colonne non indexée avec votre clause where, et pour chaque combinaison de lignes. de la table précédente . Le manuel MySQL dit : "Si la clé utilisée ne correspond qu'à quelques lignes, il s'agit d'un bon type de jointure." Et c'est le premier problème: sur un système WordPress moyen, le nombre de post-métas par post peut facilement atteindre 30 à 40 enregistrements ou plus. L'autre clé possible meta_key augmente avec votre nombre de messages. Donc, si vous avez 100 articles et que chacun a une méta _publisher, il y a 100 lignes avec cette valeur comme meta_key dans wp_postmeta, bien sûr.

Pour gérer tous ces résultats possibles, mysql crée une table temporaire (using temporary). Si cette table devient trop grande, le serveur la stocke généralement sur le disque au lieu de la mémoire. Un autre goulot d'étranglement possible.

Solutions possibles

Comme décrit dans les réponses existantes, vous pouvez essayer d’optimiser vous-même la requête. Cela pourrait bien répondre à vos préoccupations, mais pourrait créer des problèmes à mesure que les tables post/postmeta se développent.

Mais si vous souhaitez utiliser l'API WordPress Query, vous devez envisager d'utiliser les taxonomies pour stocker les données pour lesquelles vous souhaitez rechercher des publications.

6
David

Cela pourrait être un peu tard pour le jeu mais j'ai rencontré le même problème. Lors de la création d'un plugin pour gérer la recherche de propriétés, mon option de recherche avancée interroge jusqu'à 20 méta-entrées différentes pour chaque publication afin de trouver celles qui correspondent aux critères de recherche.

Ma solution consistait à interroger directement la base de données à l'aide de $wpdb global. J'ai interrogé chaque méta-entrée individuellement et stocké le post_ids des publications correspondant à chaque critère. J'ai ensuite fait une intersection sur chacun des ensembles correspondants pour obtenir le post_ids qui correspond à tous les critères.

Mon cas était assez simple car je n'avais aucun élément OR que je devais rendre compte, mais ils pouvaient assez facilement être inclus. En fonction de la complexité de votre requête, il s'agit d'une solution rapide et efficace. Bien que, j’admette, c’est une mauvaise option par rapport à la possibilité de faire une vraie requête relationnelle.

Le code ci-dessous a été grandement simplifié par rapport à ce que j’avais utilisé, mais vous pouvez en avoir l’idée.

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."'
        ");//a new operator would need to be created to handle each type of data and comparison. 
    }

    $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;
}

}
2
Aj Best

wp_postmeta a des index inefficaces. Voici une discussion sur ces traitements, ainsi que sur les traitements recommandés:

http://mysql.rjweb.org/doc.php/index_cookbook_mysql#speeding_up_wp_postmeta

1
Rick James

Les salles d'opération sont vraiment chères.

Vous avez trop de clés, mais supposons que vous ne pouvez pas changer cela maintenant. L'autre chose que vous pouvez faire, sans trop de codage, est de changer le nombre de messages que vous recevez, de changer le 'posts_per_page' en 10 ou un nombre plus important, et de voir dans quelle mesure les performances changent.

0
Tomás Cot