Come leggere l'output di EXPLAIN ANALYZE in PostgreSQL

EXPLAIN mostra cosa il planner intende fare; EXPLAIN ANALYZE esegue la query e mostra cosa è effettivamente successo. Saper leggere bene l'output è la singola competenza con più leva nel lavoro sulle performance di PostgreSQL — e ha qualche trappola che frega anche gli sviluppatori esperti.

Non hai voglia di fare i conti a mano? Incolla il tuo piano nel Visualizzatore EXPLAIN — calcola i tempi esclusivi per nodo e segnala automaticamente i problemi comuni, direttamente nel tuo browser.

L'anatomia di un nodo del piano

Ogni riga di un piano è un nodo in un albero. Le righe scorrono dai nodi più interni (con più indentazione) verso l'alto. Un nodo tipico si presenta così:

Index Scan using orders_customer_idx on orders
    (cost=0.43..152.80 rows=42 width=98)
    (actual time=0.031..0.512 rows=38 loops=1)

Due gruppi di numeri, e significano cose diverse:

Trappola #1: tutto è per loop

Quando un nodo si trova sul lato interno di un nested loop, può essere eseguito migliaia di volte. PostgreSQL riporta il suo actual time e le rows come una media per esecuzione, con loops che ti dice quante esecuzioni sono avvenute:

Index Scan using items_order_idx on items
    (actual time=0.005..0.021 rows=3 loops=12000)

Questo nodo non ha restituito 3 righe in 0.021 ms. Ha restituito all'incirca 36.000 righe e ha consumato circa 250 ms in totale (0.021 × 12.000). Moltiplica sempre per loops prima di decidere se un nodo è economico.

Trappola #2: i tempi sono cumulativi

L'actual time di un nodo include tutti i suoi figli. Se un Sort riporta 900 ms e il Seq Scan sotto di esso ne riporta 850 ms, il sort in sé è costato solo ~50 ms. Per scoprire dove il tempo viene davvero speso, ti serve il tempo esclusivo di ciascun nodo: il suo totale meno i totali dei figli. È esattamente il conto che diventa tedioso in un piano da 40 nodi — e la ragione principale per cui esistono i visualizzatori di piani.

Trappola #3: righe stimate contro effettive

La diagnostica più preziosa dell'intero output è lo scarto tra righe stimate ed effettive. Il piano è stato scelto sulla base della stima — se il planner si aspettava 40 righe e ne ha ottenute 400.000, ogni decisione a valle di quel nodo (strategia di join, dimensionamento della memoria, index scan contro scansione sequenziale) è stata presa su presupposti sbagliati.

Quando vedi una grande discrepanza:

Usa BUFFERS. Sempre.

EXPLAIN (ANALYZE, BUFFERS) aggiunge una riga come:

Buffers: shared hit=1520 read=8943 dirtied=12

I conteggi dei buffer spiegano la differenza tra "veloce nella mia sessione, lento in produzione": lo stesso piano è più lento di ordini di grandezza quando i suoi blocchi non sono in cache. Rendono anche visibile il bloat — una query che legge 50.000 blocchi per restituire 100 righe strette sta leggendo per lo più spazio morto o scansionando molto più di quanto dovrebbe.

Altro output che vale la pena conoscere

Una checklist di lettura

  1. Trova l'Execution Time totale in fondo — quello è il tuo budget.
  2. Percorri l'albero cercando i nodi il cui tempo esclusivo (totale meno figli, per il numero di loop) è una quota grande di esso. Di solito 1–3 nodi dominano.
  3. Per ogni nodo caldo, confronta le righe stimate ed effettive. Uno scarto di 10× è un problema del planner prima di essere un problema hardware.
  4. Controlla BUFFERS: sta leggendo molti più dati di quanto il risultato giustifichi?
  5. Solo allora pensa ai rimedi: statistiche, indici, forma della query, work_mem — all'incirca in quest'ordine di probabilità.
🔍 Letture correlate: scegliere l'indice giusto quando il piano mostra una scansione evitabile, e VACUUM & bloat quando i conteggi dei buffer sembrano gonfiati rispetto alle righe restituite.
🧯 Errori correlati: canceling statement due to statement timeout — il motivo abituale per cui si va a indagare un piano — e out of shared memory quando un piano tocca migliaia di partizioni.