Cea mai eficientă modalitate de a obține articole cu postmeta
Am nevoie să obțin mai multe articole cu metadatele lor. Bineînțeles că nu poți obține metadata cu o interogare standard pentru articole, așa că de obicei trebuie să folosești get_post_custom()
pentru fiecare articol.
Încerc cu o interogare personalizată, astfel:
$results = $wpdb->get_results("
SELECT p.ID,
p.post_title,
pm1.meta_value AS first_field,
pm2.meta_value AS second_field,
pm3.meta_value AS third_field
FROM $wpdb->posts p LEFT JOIN $wpdb->postmeta pm1 ON (
pm1.post_id = p.ID AND
pm1.meta_key = 'first_field_key'
) LEFT JOIN $wpdb->postmeta pm2 ON (
pm2.post_id = p.ID AND
pm2.meta_key = 'second_field_key'
) LEFT JOIN $wpdb->postmeta pm3 ON (
pm3.post_id = p.ID AND
pm3.meta_key = 'third_field_key'
)
WHERE post_status = 'publish'
");
Pare să funcționeze. Se împiedică dacă folosești oricare dintre aceste câmpuri meta într-un mod care permite valori meta multiple pentru același articol. Nu-mi pot imagina un join care să rezolve asta.
Deci, întrebarea 1: Există un join, sub-interogare sau altceva pentru a aduce câmpuri meta cu valori multiple?
Dar întrebarea 2: Merită efortul? Câte join-uri la tabelul postmeta
pot adăuga înainte ca o abordare cu 2 interogări să devină preferabilă? Aș putea obține toate datele articolelor într-o interogare, apoi să obțin toate metadatele relevante în alta, și să combin metadatele cu datele articolelor într-un set de rezultate în PHP. Ar fi mai rapid decât o singură interogare SQL din ce în ce mai complexă, dacă acest lucru este măcar posibil?
Întotdeauna mă gândesc: "Dă cât mai multă muncă posibilă bazei de date." Nu sunt sigur în acest caz!
Informațiile meta ale postărilor sunt stocate automat în cache în memorie pentru un WP_Query
standard (și pentru interogarea principală), cu excepția cazului în care specifici să nu se întâmple acest lucru prin utilizarea parametrului update_post_meta_cache
.
Prin urmare, nu ar trebui să scrii propriile interogări pentru acest lucru.
Cum funcționează cache-ul meta pentru interogările normale:
Dacă parametrul update_post_meta_cache
al WP_Query
nu este setat pe false, după ce postările sunt preluate din baza de date, funcția update_post_caches()
va fi apelată, care la rândul ei apelează update_postmeta_cache()
.
Funcția update_postmeta_cache()
este un wrapper pentru update_meta_cache()
și în esență apelează o simplă interogare SELECT
cu toate ID-urile postărilor preluate. Aceasta va obține toate metadatele postărilor, pentru toate postările din interogare, și va salva aceste date în cache-ul de obiecte (folosind wp_cache_add()
).
Când faci ceva de genul get_post_custom()
, acesta verifică mai întâi cache-ul de obiecte. Deci, nu face interogări suplimentare pentru a obține metadatele postării în acest moment. Dacă ai obținut postarea într-un WP_Query
, atunci metadatele sunt deja în memorie și le obții direct de acolo.
Avantajele aici sunt mult mai mari decât realizarea unei interogări complexe, dar cel mai mare avantaj vine din utilizarea cache-ului de obiecte. Dacă folosești o soluție de cache persistentă în memorie precum XCache, memcached sau APC, și ai un plugin care poate lega cache-ul de obiecte de aceasta (de exemplu, W3 Total Cache), atunci întregul cache de obiecte este deja stocat în memorie rapidă. În acest caz, sunt zero interogări necesare pentru a obține datele; acestea sunt deja în memorie. Cache-ul persistent de obiecte este minunat din multe perspective.
Cu alte cuvinte, interogarea ta este probabil mult mai lentă decât utilizarea unei interogări corecte și a unei soluții simple de memorie persistentă. Folosește WP_Query
normal. Economisește-ți efortul.
Suplimentar: update_meta_cache()
este inteligent, apropo. Nu va prelua metadate pentru postările care au deja metadatele în cache. Practic, nu obține aceleași metadate de două ori. Super eficient.
Suplimentar suplimentar: "Dă cât mai multă muncă bazei de date."... Nu, aceasta este web-ul. Se aplică reguli diferite. În general, dorești întotdeauna să dai cât mai puțină muncă posibilă bazei de date, dacă este fezabil. Bazele de date sunt lente sau prost configurate (dacă nu ai configurat-o tu specific, poți paria că acest lucru este adevărat). Adesea, ele sunt partajate între mai multe site-uri și supraîncărcate într-o oarecare măsură. De obicei, ai mai multe servere web decât baze de date. În general, dorești să obții datele dorite din baza de date cât mai repede și simplu posibil, apoi să le sortezi folosind codul de pe partea serverului web. Ca principiu general, desigur, fiecare caz este diferit.

Aș recomanda o interogare pivot. Folosind exemplul tău:
SELECT p.ID,
p.post_title,
MAX(CASE WHEN pm1.meta_key = 'first_field' then pm1.meta_value ELSE NULL END) as first_field,
MAX(CASE WHEN pm1.meta_key = 'second_field' then pm1.meta_value ELSE NULL END) as second_field,
MAX(CASE WHEN pm1.meta_key = 'third_field' then pm1.meta_value ELSE NULL END) as third_field,
FROM wp_posts p LEFT JOIN wp_postmeta pm1 ON ( pm1.post_id = p.ID)
GROUP BY
p.ID,p.post_title

Dacă cauți o interogare în baza de date, acesta este răspunsul corect

Această interogare mi-a redus timpul de la ~25 secunde la ~3 secunde când foloseam WP_Query. Cerința mea era să o rulez doar o singură dată, așa că nu era nevoie de caching.

Am întâlnit un caz în care am nevoie să preiau rapid o mulțime de articole împreună cu informațiile meta asociate. Trebuie să preiau aproximativ 2000 de articole.
Am încercat folosind sugestia lui Otto - rulând WP_Query::query pentru toate articolele, apoi parcurgând și rulând get_post_custom pentru fiecare articol. Această metodă a durat, în medie, aproximativ 3 secunde pentru a se finaliza.
Apoi am încercat interogarea pivot a lui Ethan (deși nu mi-a plăcut că trebuia să cer manual fiecare meta_key care mă interesa). Tot a trebuit să parcurg toate articolele returnate pentru a deserializa meta_value. Această metodă a durat, în medie, aproximativ 1.3 secunde pentru a se finaliza.
Apoi am încercat să folosesc funcția GROUP_CONCAT și am obținut cel mai bun rezultat. Iată codul:
global $wpdb;
$wpdb->query('SET SESSION group_concat_max_len = 10000'); // necesar pentru a obține mai mult de 1024 de caractere în coloanele GROUP_CONCAT de mai jos
$query = "
SELECT p.*,
GROUP_CONCAT(pm.meta_key ORDER BY pm.meta_key DESC SEPARATOR '||') as meta_keys,
GROUP_CONCAT(pm.meta_value ORDER BY pm.meta_key DESC SEPARATOR '||') as meta_values
FROM $wpdb->posts p
LEFT JOIN $wpdb->postmeta pm on pm.post_id = p.ID
WHERE p.post_type = 'product' and p.post_status = 'publish'
GROUP BY p.ID
";
$products = $wpdb->get_results($query);
// transformă produsele pentru a avea un membru ->meta cu valorile deserializate, așa cum era de așteptat
function massage($a){
$a->meta = array_combine(explode('||',$a->meta_keys),array_map('maybe_unserialize',explode('||',$a->meta_values)));
unset($a->meta_keys);
unset($a->meta_values);
return $a;
}
$products = array_map('massage',$products);
Această metodă a durat, în medie, 0.7 secunde. Asta înseamnă aproximativ un sfert din timpul soluției WP get_post_custom() și aproximativ jumătate din timpul soluției cu interogare pivot.
Poate va fi de interes pentru cineva.

Aș fi interesat să văd ce rezultate obțineți cu o soluție de cache persistent pentru obiecte. Cache-ul pentru obiecte poate fi uneori mai lent în cazul de bază, în funcție de baza de date și configurația dumneavoastră, dar rezultatele din lumea reală cu majoritatea gazdelor vor oferi rezultate foarte variate. Caching-ul bazat pe memorie este incredibil de rapid.

Salut @Otto. Indiferent de metoda pe care o folosesc pentru a obține datele, cu siguranță vreau să cachez rezultatul. Am încercat să folosesc API-ul de transient pentru asta, dar întâmpin probleme de memorie. Șirul serializat pentru cele 2000 de obiecte ale mele ocupă ~8M și set_transient() eșuează (memorie epuizată). De asemenea, trebuie să modific setarea max_allowed_packet din MySQL. Voi încerca să le cachez în fișiere, dar nu sunt încă sigur de performanță în acest caz. Există vreo metodă de a cache în memorie care persistă între request-uri?

Da, dacă ai un cache persistent în memorie (XCache, memcached, APC, etc) și folosești un plugin pentru caching de obiecte (W3 Total Cache suportă multe tipuri de cache-uri în memorie), atunci acesta stochează întregul cache de obiecte în memorie, oferindu-ți o creștere de viteză de multe ori pentru aproape orice.

Returnez 6000 de articole pentru a le folosi într-o schemă de filtrare cu backbone/underscore js. Am reușit să reduc un query personalizat care dura 6s și pe care nu-l puteam rula ca WP_Query pentru că depășea timeout-ul, la un query de 2s. Deși array_map îl încetinește din nou destul de mult...

Există vreun suport pentru construirea unui sistem performant care să returneze toate metadatele în cadrul unui WP_Query?

M-am aflat într-o situație în care a trebuit să efectuez această sarcină pentru a crea în final un document CSV. Am ajuns să lucrez direct cu MySQL pentru a realiza acest lucru. Codul meu unește tabelele post și meta pentru a prelua informațiile despre prețurile din WooCommerce, soluția postată anterior a necesitat să folosesc aliasuri pentru tabele în interogarea SQL pentru a funcționa corect.
SELECT p.ID, p.post_title,
MAX(CASE WHEN pm1.meta_key = '_price' then pm1.meta_value ELSE NULL END) as price,
MAX(CASE WHEN pm1.meta_key = '_regular_price' then pm1.meta_value ELSE NULL END) as regular_price,
MAX(CASE WHEN pm1.meta_key = '_sale_price' then pm1.meta_value ELSE NULL END) as sale_price,
MAX(CASE WHEN pm1.meta_key = '_sku' then pm1.meta_value ELSE NULL END) as sku
FROM wp_posts p LEFT JOIN wp_postmeta pm1 ON ( pm1.post_id = p.ID)
WHERE p.post_type in('product', 'product_variation') AND p.post_status = 'publish'
GROUP BY p.ID, p.post_title
Totuși, fiți atenți, WooCommerce a creat peste 300.000 de rânduri în tabela mea meta, așa că a fost foarte mare și, prin urmare, foarte lentă.

pentru cei care încearcă să obțină performanțe mai bune din interogarea postmeta, acest lucru poate fi util: https://wordpress.stackexchange.com/a/392967/27357

VERSIUNE FĂRĂ SQL:
Obțineți toate articolele și toate valorile lor meta (metas) fără SQL:
Să presupunem că aveți o listă de ID-uri de articole stocate ca un array de ID-uri, ceva de genul:
$post_ids_list = [584, 21, 1, 4, ...];
Obținerea tuturor articolelor și a tuturor metadatelor într-o singură interogare nu este posibilă fără a folosi măcar un pic de SQL, așa că trebuie să facem 2 interogări (tot doar 2):
1. Obțineți toate articolele (folosind WP_Query)
$request = new WP Query([
'post__in' => $post_ids_list,
'ignore_sticky_posts' => true, //dacă doriți să ignorați "lipirea" articolelor
]);
(Nu uitați să apelați wp_reset_postdata();
dacă faceți o "buclă" după aceea ;) )
2. Actualizați cache-ul de meta
//nu vă confundați aici: "post" înseamnă tipul de conținut (post X user X ...), NU tipul de articol ;)
update_meta_cache('post', $post_ids_list);
Pentru a obține metadatele, folosiți standardul get_post_meta()
care, după cum a menționat @Otto:
caută mai întâi în cache :)
Notă: Dacă nu aveți nevoie de alte date din articole (cum ar fi titlul, conținutul, ...) puteți face doar pasul 2. :-)

folosind soluția lui Trevor și modificând-o să funcționeze cu SQL imbricat. Aceasta nu a fost testată.
global $wpdb;
$query = "
SELECT p.*, (select pm.* From $wpdb->postmeta AS pm WHERE pm.post_id = p.ID)
FROM $wpdb->posts p
WHERE p.post_type = 'product' and p.post_status = 'publish'
";
$products = $wpdb->get_results($query);

Această întrebare a fost pusă acum aproximativ 12 ani, dar am dat peste această problemă recent când a trebuit să caut tipuri de postări cu un criteriu mai complex decât ceea ce poate fi realizat cu o interogare WP_Query de bază. Am folosit codul de mai jos pentru a face ceva foarte similar.
SELECT p.ID,p.post_title,pm1.meta_value as lat,pm2.meta_value as lng,pm3.meta_value as city,pm4.meta_value as state,pm5.meta_value as zip
FROM `wp_posts` p
LEFT JOIN `wp_postmeta` pm1 ON pm1.post_id = p.ID AND pm1.meta_key = '_lat'
LEFT JOIN `wp_postmeta` pm2 ON pm2.post_id = p.ID AND pm2.meta_key = '_lng'
LEFT JOIN `wp_postmeta` pm3 ON pm3.post_id = p.ID AND pm3.meta_key = '_city'
LEFT JOIN `wp_postmeta` pm4 ON pm4.post_id = p.ID AND pm4.meta_key = '_state'
LEFT JOIN `wp_postmeta` pm5 ON pm5.post_id = p.ID AND pm5.meta_key = '_zip'
WHERE post_type = "{posttype}" and post_status = "publish";
Alte răspunsuri la această întrebare încearcă ceva diferit față de simpla utilizare a LEFT JOIN așa cum a făcut autorul inițial. Prin câteva teste, se pare că este cea mai rapidă metodă de a obține datele.
SELECT p.ID,
p.post_title,
MAX(CASE WHEN pm1.meta_key = '_lat' then pm1.meta_value ELSE NULL END) as lat,
MAX(CASE WHEN pm1.meta_key = '_lng' then pm1.meta_value ELSE NULL END) as lng,
MAX(CASE WHEN pm1.meta_key = '_city' then pm1.meta_value ELSE NULL END) as city,
MAX(CASE WHEN pm1.meta_key = '_state' then pm1.meta_value ELSE NULL END) as state,
MAX(CASE WHEN pm1.meta_key = '_zip' then pm1.meta_value ELSE NULL END) as zip
FROM
wp_posts p LEFT JOIN wp_postmeta pm1 ON pm1.post_id = p.ID WHERE p.post_type = '{posttype}' AND p.post_status = 'publish'
GROUP BY
p.ID,p.post_title;
Această interogare este foarte similară cu un alt răspuns din acest post de Terry și Ethan și a durat 0,0228 secunde pentru a se finaliza, în timp ce prima interogare a durat aproximativ 0,0034 secunde sau de aproximativ 6,7 ori mai rapid.
Acest lucru a fost realizat pe o bază de date cu doar 322 de postări, dar când a fost testat fără a limita la tipul de postare personalizată și interogând 5234 de postări, interogarea de mai sus a obținut un timp de 0,2408s, iar prima interogare a obținut un timp de 0,0029s sau de aproximativ 83 de ori mai rapid. Nu sunt sigur cum a fost mai rapid cu 5234 de postări față de 332 de postări (am testat-o de două ori pentru a fi sigur).
TLDR: Pentru oricine caută cea mai rapidă soluție în 2024 cu o configurație implicită WordPress, utilizarea simplă a interogării LEFT JOIN de mai sus pare a fi cea mai rapidă soluție.

Am întâmpinat și eu problema câmpurilor meta cu valori multiple. Problema este în WordPress însuși. Verificați în wp-includes/meta.php. Căutați această linie:
$where[$k] = ' (' . $where[$k] . $wpdb->prepare( "CAST($alias.meta_value AS {$meta_type}) {$meta_compare} {$meta_compare_string})", $meta_value );
Problema este cu instrucțiunea CAST. Într-o interogare pentru valori meta, variabila $meta_type este setată la CHAR. Nu cunosc detaliile despre cum afectează CAST-ul valoarea ca șir de caractere serializat, dar pentru a remedia problema, puteți elimina CAST-ul astfel încât SQL-ul să arate astfel:
$where[$k] = ' (' . $where[$k] . $wpdb->prepare( "$alias.meta_value {$meta_compare} {$meta_compare_string})", $meta_value );
Acum, chiar dacă această soluție funcționează, modificați componentele interne ale WordPress, așa că alte lucruri s-ar putea să nu mai funcționeze, iar soluția nu este permanentă, mai ales dacă aveți nevoie să actualizați WordPress.
Modul în care am rezolvat eu problema a fost să copiez SQL-ul generat de WordPress pentru interogarea meta dorită și apoi să scriu niște PHP pentru a adăuga instrucțiuni AND suplimentare pentru meta_valorile pe care le caut și să folosesc $wpdb->get_results($sql) pentru rezultatul final. O soluție improvizată, dar funcționează.
