Optimizare query meta în WordPress pentru performanță crescută
Am un query meta personalizat care este extrem de lent sau nu se încarcă deloc. Cu până la trei arrays
în 'meta_query'
funcționează bine, dar cu patru sau mai multe nu mai funcționează.
Când am căutat o soluție, am găsit acest articol dar nu sunt deloc familiarizat cu query-uri personalizate în baza de date.
Orice ajutor este binevenit! Mulțumesc!
<?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; ?>
– – – – –
Cod actualizat cu modificările sugerate 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; // Funcționează doar pentru OR
$prepare_args = array();
$key_value_compares = array();
foreach ( $query->meta_query->queries as $meta_query ) {
// Nu funcționează pentru IN, NOT IN, BETWEEN, NOT BETWEEN, NOT EXISTS
if ( ! isset( $meta_query['value'] ) || is_array( $meta_query['value'] ) return $pieces; // Oprește execuția dacă nu există valoare sau este 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'] ); // Elimină clauzele postmeta
return $pieces;
}
– – –
$posts->request
afișează
$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' => '='
)
)
);
fără query personalizat
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
cu query personalizat
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

Am dat peste această problemă și se pare că MySQL nu gestionează bine multiplele joins către aceeași tabelă (wp_postmeta) și condițiile WHERE cu OR pe care WP le generează aici. Am rezolvat problema prin rescrierea join-ului și a condiției WHERE, așa cum este menționat în postarea la care te referi - iată o versiune care ar trebui să funcționeze în cazul tău (actualizat pentru WP 4.1.1) (actualizat pentru 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; // Are sens doar dacă este OR.
$prepare_args = array();
$key_value_compares = array();
foreach ( $query->meta_query->queries as $key => $meta_query ) {
if ( ! is_array( $meta_query ) ) continue;
// Nu funcționează pentru 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; // Renunță dacă nu există valoare sau este 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 );
// Elimină clauzele 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'] ); // Sortarea nu va funcționa corect, dar măcar nu va genera erori.
return $pieces;
}
și apoi în jurul interogării tale:
add_filter( 'posts_clauses', 'wpse158898_posts_clauses', 10, 2 );
$posts = new WP_Query($args);
remove_filter( 'posts_clauses', 'wpse158898_posts_clauses', 10 );
Adăugare:
Corecția pentru aceasta, ticket 24093, nu a fost inclusă în 4.0 (plus că nu a rezolvat această problemă oricum), așa că inițial am încercat o versiune generalizată a soluției de mai sus, dar este prea instabilă pentru a încerca o astfel de soluție, așa că am eliminat-o...

Codul tău pare să funcționeze foarte bine! Nu l-am verificat încă cu toate articolele mele, dar cu cele pe care le-am testat până acum este foarte rapid! Mulțumesc mult din nou—chiar apreciez ajutorul tău!

Un pas înapoi: Ai vreo idee de ce nu mai primesc niciun rezultat când folosesc interogarea ta personalizată? Când folosesc codul din postarea mea originală cu doar două interogări meta, primesc rezultatele așteptate (titlurile cu link), dar când folosesc interogarea ta nu primesc nimic.

Nu obții aceleași rezultate este mai degrabă un pas înapoi! Nu-mi pot da seama de ce nu ar funcționa - sunt $args la fel ca mai sus, adică nu sunt altele noi, doar post_type, posts_per_page și meta_query cu X intrări?

Am editat postarea mea originală cu adăugările pe care le-ai făcut. Nu am făcut nicio modificare în codul meu... Dacă ai nevoie de informații suplimentare, te rog să-mi spui.

Tot nu-mi pot da seama ce nu merge, pot doar să sugerez să debughezi cererea generată, de exemplu prin a pune print($posts->request);
(sau error_log($posts->request);
) direct după $posts = new WP_Query($args);
pentru a vedea dacă este ceva în mod evident greșit cu ea.

Mulțumesc pentru suport! Vei găsi rezultatele print($posts->request);
în codul meu original de la sfârșit—atât cu interogarea ta personalizată, cât și fără ea. error_log($posts->request);
este pentru ambele 1
.

Ah, înțeleg - folosești un prefix pe care răspunsul nu îl ia în considerare când elimină clauza where - trebuie să fie '/ AND[^w]+' . $wpdb->postmeta . '.*$/s'
în preg_replace - voi actualiza răspunsul.

Ura, în sfârșit! Și e foarte amabil din partea ta să oferi - dacă dorești, donează la organizația ta de caritate preferată (cum se spune la televizor) - și tu ai contribuit corectându-mi răspunsul, și sper să-l generalizez în viitorul nu prea îndepărtat (notă: ambiguitate non-angajantă!) având în vedere că soluția pentru WP nu va fi inclusă în versiunea 4.0.

Salut, nu funcționează, trebuie să ignor intrările non-query în array-ul meta_query plus să adaptez referința ne-aliasată în clauza orderby - cel puțin, poate fi nevoie și de altele, dar voi actualiza oricum pentru moment...

@vee da, ai dreptate, printre altele formatul clauzei where s-a schimbat așa că nu mai este eliminată, voi actualiza răspunsul specific și voi elimina răspunsul "generalizat" deoarece este destul de fără speranță să încerci să-l actualizezi continuu...

Răspunsul scurt este că metadatele în WordPress nu sunt concepute pentru a fi folosite pentru date relaționale. Obținerea postărilor pe baza mai multor condiții legate de metadatele lor nu este ideea din spatele metadatelor. Prin urmare, interogările, structurile tabelelor și indecșii nu sunt optimizați pentru acest lucru.
Răspunsul mai detaliat:
Ceea ce rezultă din interogarea ta de metadate este ceva de genul:
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
Să vedem cum gestionează MySQL această interogare (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
Ce poți observa este că MySQL face o selecție pe wp_posts
și alătură de 5 ori tabelul wp_postmeta
. Tipul ref
indică faptul că MySQL trebuie să examineze toate rândurile din acest tabel, potrivind indexul (post_id, meta_key) comparând o valoare de coloană neindexată cu clauza ta where
, și asta pentru fiecare combinație de rânduri din tabelul anterior. Manualul MySQL precizează: »Dacă cheia folosită se potrivește doar cu câteva rânduri, acesta este un tip bun de alăturare.« Și aceasta este prima problemă: pe un sistem WordPress mediu, numărul de metadate pe postare poate crește ușor până la 30-40 de înregistrări sau mai mult. Cealaltă cheie posibilă, meta_key
, crește odată cu numărul de postări. Deci, dacă ai 100 de postări și fiecare are o metadată _publisher
, există 100 de rânduri cu această valoare ca meta_key
în wp_postmeta
, desigur.
Pentru a gestiona toate aceste rezultate posibile, MySQL creează un tabel temporar (using temporary
). Dacă acest tabel devine prea mare, serverul îl stochează de obicei pe disc în loc de memorie. Un alt potențial punct de blocaj.
Posibile soluții
Așa cum este descris în răspunsurile existente, ai putea încerca să optimizezi interogarea pe cont propriu. Aceasta poate funcționa bine pentru nevoile tale, dar poate duce la probleme pe măsură ce tabelele de postări/metadate cresc.
Dar dacă dorești să folosești API-ul de interogare WordPress, ar trebui să iei în considerare utilizarea taxonomiilor pentru a stoca datele după care dorești să cauți postările.

Ah, deci asta "explică" de primești lovitura prima dată când interogarea rulează, dar nu și la rulările ulterioare imediate - minunat! Dar nu pot fi de acord cu punctul tău despre folosirea taxonomiilor în loc, asta pare nefiresc cel puțin, dac nu chiar imposibil. De ce nu ar trebui să interoghezi metadatele relațional? - WP are mult cod care se ocupă exact de acest caz de utilizare și o face chiar el! Pentru mine este pur și simplu un bug în WP - ar trebui să poată genera un cod mult mai bun aici, în loc să genereze ca un idiot join după join.

Există un ticket WP (vechi de 3 ani!) despre asta 24093 care are un patch care rezolvă problema folosind sub-interogări. Nu va fi inclus în 4.0 însă...

Acest lucru poate fi puțin întârziat, dar am întâmpinat aceeași problemă. Când am construit un plugin pentru gestionarea căutării de proprietăți, opțiunea mea de căutare avansată interoga până la 20 de intrări meta diferite pentru fiecare articol pentru a găsi cele care se potrivesc cu criteriile de căutare.
Soluția mea a fost să interoghez direct baza de date folosind variabila globală $wpdb
. Am interogat fiecare intrare meta individual și am stocat post_ids
(ID-urile articolelor) care se potrivesc cu fiecare criteriu. Apoi am făcut o intersecție între fiecare set de rezultate potrivite pentru a obține post_ids
care se potrivesc cu toate criteriile.
Cazul meu a fost oarecum simplu pentru că nu aveam elemente OR
de luat în considerare, dar acestea ar putea fi incluse relativ ușor. În funcție de cât de complexă este interogarea ta, aceasta este o soluție funcțională și rapidă. Cu toate acestea, recunosc că este o opțiune inferioară în comparație cu posibilitatea de a face o interogare relațională adevărată.
Codul de mai jos a fost simplificat foarte mult față de ceea ce am folosit eu, dar poți înțelege ideea din el.
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."'
");//ar trebui creat un operator nou pentru a gestiona fiecare tip de date și comparație
}
$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;
}
}

Tabelul wp_postmeta are indecși ineficienți. Iată o discuție pe această temă, împreună cu soluții recomandate:
http://mysql.rjweb.org/doc.php/index_cookbook_mysql#speeding_up_wp_postmeta

Interogările OR sunt foarte costisitoare.
Aveți prea multe chei, dar să presupunem că nu le puteți schimba acum. Cealaltă opțiune pe care o aveți, fără prea mult cod, este să modificați numărul de articole pe care le obțineți, schimbați 'posts_per_page'
la 10, sau un număr mai mare, și observați cum se modifică performanța.
