Le prestazioni di un sito WordPress dipendono in modo significativo dal numero, dal tipo e dalla qualità delle query verso il database. Temi, plugin e codice custom possono trasformare una pagina apparentemente semplice in una sequenza di interrogazioni ridondanti o costose (join complessi, meta query non indicizzate, ricerche non selettive). In questo articolo vediamo un approccio concreto: capire cosa sta succedendo, ridurre il lavoro del database, sfruttare cache e indici, e scrivere query più “leggere” con gli strumenti nativi di WordPress.
1) Prima misura, poi ottimizza
Ottimizzare “a sensazione” porta spesso a interventi inutili o controproducenti. Inizia sempre con una fase di osservazione:
- Conta query e tempi: quante query vengono eseguite per pagina e quanto tempo impiegano.
- Individua le query lente: spesso sono poche query problematiche a pesare molto (meta_query, ricerche, tassonomie complesse).
- Associa le query a un componente: tema, plugin o codice custom.
Un ottimo alleato è Query Monitor, che mostra query, call stack e tempi. In ambienti di sviluppo puoi anche abilitare il log delle query lente del database (a livello MySQL/MariaDB) per verificare cosa accade sotto carico.
2) Conoscere le basi: WP_Query, get_posts e query del Loop
WordPress usa WP_Query per la “main query” e per query secondarie. Molti problemi nascono dall’uso improprio di query aggiuntive dentro template (soprattutto dentro cicli annidati) o dall’uso di funzioni che non rispettano la main query.
2.1 Usa pre_get_posts per modificare la main query
Se devi cambiare cosa appare in una pagina di archivio o in home, è spesso meglio intervenire sulla main query invece di crearne una nuova. Questo riduce query duplicate e migliora compatibilità con paginazione e plugin SEO.
add_action('pre_get_posts', function (WP_Query $q) {
if (is_admin() || !$q->is_main_query()) {
return;
}
if ($q->is_home()) {
$q->set('post_type', ['post']);
$q->set('posts_per_page', 10);
$q->set('ignore_sticky_posts', true);
}
});
Note importanti:
- Controlla sempre
is_admin()eis_main_query(). - Evita di impostare criteri troppo generici (rischi di colpire più contesti del previsto).
2.2 Preferisci query “mirate” invece di query generiche
Più la query è selettiva (filtri chiari e indicizzabili), meno lavoro farà il database. Alcuni parametri migliorano molto il costo:
post_typeesplicitopost_statusesplicito (soprattutto in contesti custom)orderbyeordercoerenti con indici disponibili (vedi sezioni su meta e tassonomie)posts_per_pagecontenuto (e paginazione corretta)
3) Ridurre i dati letti: quando non ti servono “tutti” i campi
Molto spesso una query recupera interi oggetti post (con campi e cache correlate) quando in realtà servono solo ID o pochi dati. Ridurre “quanto” leggi è una delle ottimizzazioni più efficaci.
3.1 Recupera solo gli ID
Se ti servono solo gli ID per un’elaborazione successiva, usa fields => 'ids'. Riduce quantità di dati trasferiti e lavoro di idratazione degli oggetti.
$q = new WP_Query([
'post_type' => 'product',
'post_status' => 'publish',
'posts_per_page' => 50,
'fields' => 'ids',
'no_found_rows' => true,
]);
$product_ids = $q->posts;
3.2 Disattiva il calcolo del totale quando non serve
Quando non ti serve la paginazione (o non ti interessa il numero totale dei risultati), imposta no_found_rows => true. WordPress eviterà la query aggiuntiva per calcolare il totale righe.
3.3 Evita la priming cache inutile
WP_Query può “pre-caricare” cache di meta e termini. È utile quando poi li userai, ma è overhead quando non ti servono.
$q = new WP_Query([
'post_type' => 'post',
'posts_per_page' => 20,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
]);
Usa questi flag solo se sei sicuro che non utilizzerai meta/termini nel rendering (o che li caricherai altrove in modo più efficiente).
4) Evitare la “query nella query” e il classico N+1
Il problema N+1 in WordPress si presenta quando per ogni elemento di una lista esegui una query aggiuntiva (meta, termini, relazioni). Su 20 post, diventano 21 query o più. Le cause comuni:
get_post_meta()dentro un loop senza caching efficacewp_get_post_terms()per ogni post- query secondarie per recuperare dati correlati (es. “prodotti simili”) per ogni item
Strategie pratiche:
- Recupera tutti gli ID e poi carica i dati in batch.
- Sfrutta la priming cache in modo consapevole (o disattivala se inutile).
- Riduci le query correlate: raggruppa operazioni, usa caching applicativo (transient/object cache).
5) Meta query: utili, ma spesso costose
Le meta query sono uno dei principali punti critici perché si appoggiano alla tabella wp_postmeta, che può crescere rapidamente e non è indicizzata in modo ottimale per ricerche complesse. Alcune linee guida:
5.1 Mantieni le meta query semplici
- Evita molte condizioni in OR: costringono il DB a lavorare di più.
- Evita
LIKEsu grandi volumi di metadati: è quasi sempre non selettivo. - Specifica sempre il tipo (
type) quando confronti numeri o date.
$q = new WP_Query([
'post_type' => 'event',
'posts_per_page' => 10,
'meta_query' => [
[
'key' => 'event_date',
'value' => date('Y-m-d'),
'compare' => '>=',
'type' => 'DATE',
],
],
'orderby' => 'meta_value',
'meta_key' => 'event_date',
'order' => 'ASC',
]);
5.2 Se filtri spesso per un meta specifico, valuta un modello dati migliore
Quando un meta diventa un campo “strutturale” (es. prezzo, data evento, stato), la soluzione più performante può essere:
- una tabella custom dedicata (con indici mirati) per quel tipo di dato;
- una tassonomia (se il campo è categoriale e non numerico continuo);
- in alcuni casi, un campo standard del post (se applicabile) o un “denormalized field” calcolato.
Non è sempre necessario, ma su grandi volumi è spesso l’unico modo per rendere query prevedibili e scalabili.
6) Tax query e relazioni: attenzione a join e cardinalità
Le query basate su tassonomie passano da wp_term_relationships e tabelle correlate. In genere sono più efficienti delle meta query, ma possono diventare costose quando:
- usi molte tassonomie insieme con relazioni complesse;
- usi operatori poco selettivi;
- hai termini con moltissimi post associati (alta cardinalità).
$q = new WP_Query([
'post_type' => 'post',
'posts_per_page' => 12,
'tax_query' => [
'relation' => 'AND',
[
'taxonomy' => 'category',
'field' => 'slug',
'terms' => ['tutorial'],
],
[
'taxonomy' => 'post_tag',
'field' => 'slug',
'terms' => ['performance', 'wordpress'],
],
],
]);
Consigli pratici:
- Preferisci filtri su tassonomie quando il campo è categoriale.
- Evita di combinare molte condizioni superflue.
- Se stai filtrando per un termine enorme, valuta paginazione e caching aggressivo.
7) Ordinamenti e paginazione: i costi nascosti
Ordinare è spesso più costoso di filtrare, soprattutto se l’ordinamento non è supportato da indici. Alcuni casi tipici:
ORDER BY RAND(): sconsigliato, scala malissimo.- ordinamento per meta numerico senza tipo o senza strategia dati adeguata.
- paginazione profonda (page 200+) con offset elevati: anche con indici, il DB deve “saltare” molte righe.
7.1 Alternative a ORDER BY RAND()
Per contenuti “casuali”:
- precalcola una lista random e mettila in cache (transient) per un certo periodo;
- usa una selezione basata su un seed (es. giorno corrente) per rendere il “random” stabile per un giorno;
- seleziona un intervallo di ID e pesca in modo più economico.
7.2 Evita offset quando possibile
Se devi implementare infinite scroll o paginazioni profonde, valuta tecniche basate su “seek method” (paginazione per cursore), ad esempio usando l’ultima data/ID visto come parametro per la pagina successiva, invece di OFFSET.
8) Caching: object cache, transients e caching delle pagine
La query perfetta è quella che non esegui. WordPress offre più livelli di caching che puoi combinare:
- Object cache: cache in memoria per oggetti (post, meta, termini). Con un backend persistente (es. Redis/Memcached) diventa molto efficace.
- Transients: cache applicativa con scadenza; ottima per risultati di query costose e relativamente stabili.
- Page cache (server/CDN/plugin): evita l’esecuzione di PHP e query per pagine pubbliche.
8.1 Esempio pratico con transient
Cache del risultato di una query “pesante” (ad esempio una lista di post in evidenza basata su meta/tassonomie):
$cache_key = 'home_featured_posts_v1';
$post_ids = get_transient($cache_key);
if ($post_ids === false) {
$q = new WP_Query([
'post_type' => 'post',
'posts_per_page' => 8,
'fields' => 'ids',
'no_found_rows' => true,
'meta_query' => [
[
'key' => 'featured',
'value' => '1',
],
],
]);
$post_ids = $q->posts;
set_transient($cache_key, $post_ids, 10 * MINUTE_IN_SECONDS);
}
// In seguito puoi caricare i post da ID in modo controllato.
$posts = array_map('get_post', $post_ids);
Buone pratiche:
- Usa chiavi versionate (es.
_v1) per invalidare facilmente quando cambi logica. - Invalida la cache su eventi di aggiornamento contenuti (hook su
save_post, aggiornamento meta, ecc.).
9) Indici e database: quando serve intervenire a basso livello
In contesti ad alto traffico o con dataset grandi, può servire ottimizzare anche il database. Alcuni interventi tipici:
- Verificare dimensione e frammentazione delle tabelle.
- Analizzare query lente con
EXPLAIN. - Valutare indici aggiuntivi in tabelle custom (consigliato) o, con cautela, su tabelle core in casi specifici.
Nota: aggiungere indici sulle tabelle core può creare incompatibilità con aggiornamenti o assunzioni di plugin. Se hai un requisito strutturale (es. filtri frequenti su un campo), la strategia più pulita è una tabella custom con indici ad hoc.
10) Scrivere query custom con $wpdb: sicurezza e prestazioni
Quando WP_Query non basta, puoi usare $wpdb. Fallo con attenzione:
- Usa sempre
$wpdb->prepare()per evitare SQL injection. - Seleziona solo colonne necessarie.
- Metti in cache i risultati se la query è ripetuta spesso.
global $wpdb;
$status = 'publish';
$limit = 20;
$sql = $wpdb->prepare(
"SELECT ID
FROM {$wpdb->posts}
WHERE post_type = %s
AND post_status = %s
ORDER BY post_date_gmt DESC
LIMIT %d",
'post',
$status,
$limit
);
$post_ids = $wpdb->get_col($sql);
11) Ottimizzazioni “facili” che spesso fanno la differenza
- Riduci query duplicate: evita di richiamare più volte la stessa informazione nella stessa richiesta; salva il risultato in variabili.
- Evita loop annidati con query: prima raccogli gli ID, poi costruisci rendering senza ulteriori query per item.
- Limita l’uso di meta per ricerche: se devi cercare in meta spesso, valuta un indice esterno o struttura dati diversa.
- Disabilita funzionalità inutili su query specifiche (no_found_rows, update_*_cache) quando appropriato.
- Controlla i plugin: alcuni aggiungono join o filtri globali alle query (hook su
posts_clauses,pre_get_posts) e possono rallentare tutto.
12) Checklist finale
- Misura: individua query lente e componenti responsabili.
- Riduci: meno query e meno dati per query.
- Semplifica: evita meta_query complesse e OR inutili.
- Ottimizza paginazione e ordinamento: evita offset profondi e RAND.
- Cache: object cache persistente, transients, page cache/CDN.
- Struttura dati: quando un meta diventa “campo chiave”, valuta tabella custom/indici.
- Rivaluta dopo ogni intervento: non assumere, verifica.
Con queste tecniche puoi ottenere miglioramenti misurabili senza sacrificare la flessibilità di WordPress. L’obiettivo non è eliminare ogni query, ma rendere le interrogazioni prevedibili, selettive, cache-friendly e coerenti con la struttura dati del tuo progetto.