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.
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:
cost=0.43..152.80— la stima del planner, in unità di costo arbitrarie (non millisecondi). Il primo numero è il costo di avvio prima che si possa produrre la prima riga, il secondo il costo totale per tutte le righe.rows=42(nel gruppo dei costi) — quante righe il planner si aspettava.actual time=0.031..0.512— millisecondi misurati, di nuovo avvio..totale, per loop.rows=38 loops=1(nel gruppo actual) — righe effettivamente restituite, mediate per loop.
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:
- Esegui
ANALYZEsulle tabelle coinvolte — le statistiche potrebbero semplicemente essere vecchie. - Se l'errore di stima persiste su una colonna specifica, aumenta il dettaglio del campione:
ALTER TABLE t ALTER COLUMN c SET STATISTICS 1000; ANALYZE t; - Se la condizione combina colonne correlate (città + CAP, categoria + marca), il planner moltiplica le loro selettività come se fossero indipendenti.
CREATE STATISTICScon il tipodependenciesesiste esattamente per questo caso.
Usa BUFFERS. Sempre.
EXPLAIN (ANALYZE, BUFFERS) aggiunge una riga come:
Buffers: shared hit=1520 read=8943 dirtied=12
hit— blocchi da 8 KB trovati nella shared buffer cache di PostgreSQL.read— blocchi che hanno dovuto arrivare da fuori i shared buffer (cache del SO o disco).dirtied/written— blocchi modificati o sfrattati mentre la query girava.
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
Rows Removed by Filter— righe recuperate e poi scartate. Un numero alto significa che il percorso di accesso non è selettivo: un indice (o un indice migliore) potrebbe evitare la maggior parte di quelle letture.Sort Method: external merge Disk: 210400kB— il sort non è entrato inwork_memed è spillato su disco. Stessa storia per gli hash join che riportanoBatches: 4(più di un batch = spill).(never executed)— il nodo non è mai stato eseguito (per esempio, l'altro lato del join ha prodotto zero righe). I suoi numeri sono privi di significato; non ottimizzarlo.loopscon i worker paralleli — un nodo parallelo mostra un loop per worker; i numeri per loop sono medie per worker.
Una checklist di lettura
- Trova l'
Execution Timetotale in fondo — quello è il tuo budget. - 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.
- Per ogni nodo caldo, confronta le righe stimate ed effettive. Uno scarto di 10× è un problema del planner prima di essere un problema hardware.
- Controlla
BUFFERS: sta leggendo molti più dati di quanto il risultato giustifichi? - Solo allora pensa ai rimedi: statistiche, indici, forma della query,
work_mem— all'incirca in quest'ordine di probabilità.
