Ottimizzare query meta lente in WordPress
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

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

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!

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.

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?

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.

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.

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
.

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.

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.

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

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

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.

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.

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

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

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

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.
