Ottimizzare le query in WordPress

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() e is_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_type esplicito
  • post_status esplicito (soprattutto in contesti custom)
  • orderby e order coerenti con indici disponibili (vedi sezioni su meta e tassonomie)
  • posts_per_page contenuto (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 efficace
  • wp_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 LIKE su 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

  1. Misura: individua query lente e componenti responsabili.
  2. Riduci: meno query e meno dati per query.
  3. Semplifica: evita meta_query complesse e OR inutili.
  4. Ottimizza paginazione e ordinamento: evita offset profondi e RAND.
  5. Cache: object cache persistente, transients, page cache/CDN.
  6. Struttura dati: quando un meta diventa “campo chiave”, valuta tabella custom/indici.
  7. 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.

Torna su