Optimizare query meta în WordPress pentru performanță crescută

23 aug. 2014, 22:51:15
Vizualizări: 16.1K
Voturi: 11

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

Poți adăuga explicația MySQL (EXPLAIN SELECT …, pe sistemul tău) pentru interogarea rezultată?

David David
24 aug. 2014 17:41:59
Toate răspunsurile la întrebare 5
16

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

24 aug. 2014 17:21:06
Comentarii

Mulțumesc mult pentru ajutor! O să încerc și o să vă anunț.

user1706680 user1706680
24 aug. 2014 21:12:49

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!

user1706680 user1706680
30 aug. 2014 22:53:37

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.

user1706680 user1706680
31 aug. 2014 01:44:24

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?

bonger bonger
31 aug. 2014 03:01:46

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.

user1706680 user1706680
31 aug. 2014 11:30:26

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.

bonger bonger
31 aug. 2014 17:00:57

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.

user1706680 user1706680
3 sept. 2014 12:09:42

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.

bonger bonger
3 sept. 2014 16:23:16

Uraaa, funcționează! Pot să-ți mulțumesc cu o mică donație?

user1706680 user1706680
3 sept. 2014 18:08:08

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.

bonger bonger
3 sept. 2014 18:38:59

Știi dacă funcționează în 4.1.1?

user1706680 user1706680
19 feb. 2015 11:05:47

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

bonger bonger
2 mar. 2015 18:26:26

Nu funcționează pe WP 4.2.4. Încă foarte lent.

vee vee
13 aug. 2015 17:59:51

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

bonger bonger
15 aug. 2015 15:18:37

@bonger Nu-ți face griji, nu e vina ta.

vee vee
15 aug. 2015 23:04:35

Doar părerea mea: rezolv problemele de genul acesta folosind 2 interogări wp separate și apoi unesc rezultatele programatic prin PHP - este destul de rapid (exemplul meu recent, timpii de încărcare scad de la 4s la 0.3)

trainoasis trainoasis
9 dec. 2019 13:03:33
Arată celelalte 11 comentarii
2

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.

25 aug. 2014 11:37:44
Comentarii

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.

bonger bonger
25 aug. 2014 14:45:19

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

bonger bonger
25 aug. 2014 19:24:36
0

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

}
8 sept. 2016 01:01:49
1

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

23 mai 2017 05:10:04
Comentarii

Informații foarte bune pentru a înțelege ce se întâmplă și ce se poate face, lucruri utile

sMyles sMyles
4 ian. 2022 21:20:03
2

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.

23 aug. 2014 23:07:12
Comentarii

Mulțumesc pentru sugestie – momentan am doar un articol, așa că modificarea post_per_pagenu va schimba nimic.

user1706680 user1706680
23 aug. 2014 23:23:10

Ok, dacă abia începi, poți în schimb să modifici numărul de chei. Ce părere ai?

Tomás Cot Tomás Cot
23 aug. 2014 23:39:28