Ottimizzare query meta lente in WordPress

23 ago 2014, 22:51:15
Visualizzazioni: 16.1K
Voti: 11

Ho una query meta personalizzata che è terribilmente lenta o non carica nemmeno fino alla fine. Con fino a tre array in 'meta_query' la query funziona bene, con quattro o più smette di funzionare.

Cercando una soluzione ho trovato questo post ma non ho familiarità con le query personalizzate al database.

Ogni aiuto è molto apprezzato! Grazie!

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

– – – – –

Codice aggiornato con le modifiche suggerite da 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 per relazioni OR
    $prepare_args = array();
    $key_value_compares = array();
    foreach ( $query->meta_query->queries as $meta_query ) {
        // Non funziona per IN, NOT IN, BETWEEN, NOT BETWEEN, NOT EXISTS.
        if ( ! isset( $meta_query['value'] ) || is_array( $meta_query['value'] ) ) return $pieces; // Esci se non c'è valore o è un 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'] ); // Rimuovi clausole postmeta
    return $pieces;
}

– – –

$posts->request restituisce

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

Senza la query personalizzata

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 query personalizzata

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
Commenti

Puoi aggiungere la spiegazione MySQL (EXPLAIN SELECT …, sul tuo sistema) dalla Query risultante?

David David
24 ago 2014 17:41:59
Tutte le risposte alla domanda 5
16

Mi sono imbattuto in questo problema e sembra che MySQL non gestisca bene i join multipli alla stessa tabella (wp_postmeta) e le condizioni WHERE con OR che WordPress genera in questo caso. Ho risolto riscrivendo il join e la clausola WHERE come menzionato nel post che hai linkato - ecco una versione che dovrebbe funzionare nel tuo caso (aggiornato per WP 4.1.1) (aggiornato per 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; // Ha senso solo con OR.
    $prepare_args = array();
    $key_value_compares = array();
    foreach ( $query->meta_query->queries as $key => $meta_query ) {
        if ( ! is_array( $meta_query ) continue;
        // Non funziona per 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; // Esci se non c'è valore o è un 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 );
    // Rimuovi le clausole 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'] ); // L'ordinamento non funzionerà davvero ma almeno non fallirà.
    return $pieces;
}

e poi attorno alla tua query:

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

Addendum:

La correzione per questo, ticket 24093, non è stata inclusa in 4.0 (inoltre non ha risolto comunque questo problema), quindi originariamente avevo tentato una versione generalizzata del codice sopra ma è troppo instabile per tentare una soluzione del genere quindi l'ho rimossa...

24 ago 2014 17:21:06
Commenti

Grazie mille per il tuo aiuto! Proverò e ti farò sapere.

user1706680 user1706680
24 ago 2014 21:12:49

Il tuo codice sembra funzionare davvero bene! Non l'ho ancora verificato con tutti i miei post, ma con quelli che ho finora è molto veloce! Grazie mille ancora—apprezzo molto il tuo aiuto!

user1706680 user1706680
30 ago 2014 22:53:37

Un passo indietro: Hai qualche idea sul perché non ottengo più risultati quando uso la tua query personalizzata? Quando uso il codice del mio post originale con solo due meta query ottengo i risultati attesi (titoli collegati), quando uso la tua query non ottengo nulla.

user1706680 user1706680
31 ago 2014 01:44:24

Non ottenere gli stessi risultati è più un passo indietro di dieci! Non riesco a capire perché non funzioni - gli $args sono gli stessi di prima, cioè nessuno nuovo, solo post_type, posts_per_page e la meta_query con X voci?

bonger bonger
31 ago 2014 03:01:46

Ho modificato il mio post originale con le aggiunte che hai fatto. Non ho apportato alcuna modifica al mio codice... Se hai bisogno di ulteriori informazioni, fammelo sapere.

user1706680 user1706680
31 ago 2014 11:30:26

Ancora non riesco a capire cosa stia andando storto, posso solo suggerire di debug della richiesta generata ad esempio inserendo print($posts->request); (o error_log($posts->request);) direttamente dopo il $posts = new WP_Query($args); per vedere se c'è qualcosa di evidentemente sbagliato.

bonger bonger
31 ago 2014 17:00:57

Grazie per il supporto! Troverai gli output di print($posts->request); nel mio codice originale in fondo - sia con la tua query personalizzata che senza. Il error_log($posts->request); è per entrambi 1.

user1706680 user1706680
3 set 2014 12:09:42

Ah capisco - stai usando un prefisso che la risposta non prende in considerazione quando elimina la clausola where - dovrebbe essere '/ AND[^w]+' . $wpdb->postmeta . '.*$/s' nel preg_replace - aggiornerò la risposta.

bonger bonger
3 set 2014 16:23:16

Evviva, funziona! Posso ringraziarti con una piccola donazione?

user1706680 user1706680
3 set 2014 18:08:08

Evviva, finalmente! Ed è molto gentile da parte tua offrirlo - se vuoi, dona alla tua organizzazione benefica preferita (come dicono in TV) - anche tu hai contribuito correggendo la mia risposta, e spero di generalizzarla nel prossimo futuro (nota la vaghezza non impegnativa!) dato che la correzione per WP non sarà inclusa nella 4.0.

bonger bonger
3 set 2014 18:38:59

Sai se funziona nella 4.1.1?

user1706680 user1706680
19 feb 2015 11:05:47

Ciao, no non funziona, deve ignorare le voci non query nell'array meta_query oltre a gestire i riferimenti non alias nella clausola orderby - come minimo, potrebbe servire altro, ma la aggiornerò comunque per ora...

bonger bonger
2 mar 2015 18:26:26

Non funziona su WP 4.2.4. È ancora molto lento.

vee vee
13 ago 2015 17:59:51

@vee sì, hai ragione, tra l'altro il formato della clausola where è cambiato quindi non viene eliminato, aggiornerò la risposta specifica e rimuoverò quella "generalizzata" visto che è piuttosto inutile cercare di aggiornarla continuamente...

bonger bonger
15 ago 2015 15:18:37

@bonger Non preoccuparti, non è colpa tua.

vee vee
15 ago 2015 23:04:35

Solo la mia umile opinione: risolvo i problemi utilizzando 2 query wp separate e poi unisco i risultati programmaticamente via PHP - è ragionevolmente veloce (nel mio esempio recente, i tempi di caricamento passano da 4s a 0.3)

trainoasis trainoasis
9 dic 2019 13:03:33
Mostra i restanti 11 commenti
2

La risposta breve è che i metadati in WordPress non sono pensati per essere utilizzati come dati relazionali. Recuperare post basandosi su diverse condizioni relative ai loro metadati non è l'idea alla base dei metadati. Pertanto, le query, le strutture delle tabelle e gli indici non sono ottimizzati per questo scopo.

La risposta più dettagliata:

Ciò che ottieni con la tua Meta-Query è qualcosa di simile a questo:

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

Analizziamo come MySQL gestisce questa query (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

Quello che puoi vedere è che MySQL effettua una selezione su wp_posts e unisce 5 volte la tabella wp_postmeta. Il tipo ref indica che MySQL deve esaminare tutte le righe in questa tabella, confrontando l'indice (post_id, meta_key) con un valore di colonna non indicizzato nella tua clausola where, e questo per ogni combinazione di righe dalla tabella precedente. Il manuale di MySQL dice: »Se la chiave utilizzata corrisponde solo a poche righe, questo è un buon tipo di join.« E questo è il primo problema: in un sistema WordPress medio, il numero di metadati per post può facilmente crescere fino a 30-40 record o più. L'altra possibile chiave meta_key cresce con il numero di post. Quindi, se hai 100 post e ognuno ha un meta _publisher, ci saranno 100 righe con questo valore come meta_key in wp_postmeta, ovviamente.

Per gestire tutti questi possibili risultati, MySQL crea una tabella temporanea (using temporary). Se questa tabella diventa troppo grande, il server solitamente la memorizza su disco invece che in memoria. Un altro possibile collo di bottiglia.

Possibili soluzioni

Come descritto nelle risposte esistenti, potresti provare a ottimizzare la query da solo. Questo potrebbe funzionare bene per le tue esigenze, ma potrebbe causare problemi man mano che le tabelle post/postmeta crescono.

Ma se vuoi utilizzare l'API di WordPress Query, dovresti considerare di utilizzare le Tassonomie per memorizzare i dati in base ai quali vuoi cercare i post.

25 ago 2014 11:37:44
Commenti

Ah quindi questo "spiega" perché ottieni il risultato la prima volta che la query viene eseguita, ma non nelle esecuzioni immediate successive - ottimo! Ma non posso essere d'accordo con il tuo punto sull'uso delle tassonomie al posto dei metadati, sembra innaturale nella migliore delle ipotesi, se non infattibile. Perché non dovresti interrogare i metadati in modo relazionale? - WP ha molto codice che si occupa proprio di questo utilizzo, e lo fa anche lui stesso! Per me è semplicemente un bug in WP - dovrebbe essere in grado di generare codice molto migliore qui, piuttosto che generare stupidamente join dopo join.

bonger bonger
25 ago 2014 14:45:19

C'è un ticket WP (di 3 anni fa!) su questo 24093 che ha una patch che lo risolve utilizzando sotto-query. Non entrerà però nella versione 4.0...

bonger bonger
25 ago 2014 19:24:36
0

Potrei essere un po' in ritardo ma mi sono imbattuto nello stesso problema. Durante lo sviluppo di un plugin per gestire la ricerca di proprietà, la mia opzione di ricerca avanzata interrogava fino a 20 diverse voci meta per ogni post per trovare quelle che corrispondevano ai criteri di ricerca.

La mia soluzione è stata interrogare direttamente il database utilizzando la variabile globale $wpdb. Ho interrogato ogni voce meta individualmente e memorizzato i post_ids dei post che corrispondevano a ciascun criterio. Successivamente ho eseguito un'intersezione tra ciascun set di risultati corrispondenti per ottenere i post_ids che soddisfacevano tutti i criteri.

Il mio caso era relativamente semplice perché non avevo elementi OR da considerare, ma potrebbero essere inclusi abbastanza facilmente. A seconda della complessità della tua query, questa è una soluzione funzionante e veloce. Tuttavia, ammetto che è un'opzione scadente rispetto alla possibilità di eseguire una vera query relazionale.

Il codice seguente è stato semplificato molto rispetto a quello che ho utilizzato, ma puoi farti un'idea.

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."'
        ");//sarebbe necessario creare un nuovo operatore per gestire ogni tipo di dato e confronto.
    }

    $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 set 2016 01:01:49
1

La tabella wp_postmeta ha indici inefficienti. Ecco una discussione sull'argomento, insieme alle soluzioni consigliate:

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

23 mag 2017 05:10:04
Commenti

Informazioni molto utili per farsi un'idea di cosa sta succedendo e cosa si può fare, ottimo materiale

sMyles sMyles
4 gen 2022 21:20:03
2

Gli OR sono davvero costosi.

Hai troppe chiavi, ma supponiamo che ora non puoi cambiarle. L'altra cosa che puoi fare, senza troppo codice, è modificare il numero di post che stai ottenendo, cambia 'posts_per_page' a 10, o a un numero più grande, e vedi quanto cambiano le prestazioni.

23 ago 2014 23:07:12
Commenti

Grazie per il suggerimento – attualmente ho solo un articolo, quindi modificare post_per_page non cambierà nulla.

user1706680 user1706680
23 ago 2014 23:23:10

Ok, se stai appena iniziando allora, forse puoi cambiare il numero di chiavi. Cosa ne pensi?

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