Capire il costo SPIEGAZIONE di Postgres
EXPLAIN
è molto utile per comprendere le prestazioni di una query Postgres. Restituisce il piano di esecuzione generato dal pianificatore di query PostgreSQL per una determinata istruzione. Il EXPLAIN
comando specifica se le tabelle a cui si fa riferimento in un'istruzione verranno ricercate utilizzando una scansione dell'indice o una scansione sequenziale.
Alcune delle prime cose che noterai durante la revisione dell'output di un EXPLAIN
comando sono le statistiche sui costi, quindi è naturale chiedersi cosa significano, come vengono calcolati e come vengono utilizzati.
In breve, il pianificatore di query di PostgreSQL stima quanto tempo impiegherà la query (in un'unità arbitraria), con un costo di avvio e un costo totale per ciascuna operazione. Ne parleremo più avanti. Quando dispone di più opzioni per eseguire una query, utilizza questi costi per scegliere l'opzione più economica e quindi, si spera, più veloce.
In che unità sono i costi?
I costi sono in un'unità arbitraria. Un comune malinteso è che siano in millisecondi o in qualche altra unità di tempo, ma non è così.
Le unità di costo sono ancorate (per impostazione predefinita) a una singola pagina sequenziale letta che costa 1,0 unità (seq_page_cost
). Ogni riga elaborata aggiunge 0,01 (cpu_tuple_cost
), e ogni pagina letta non sequenziale aggiunge 4.0 (random_page_cost
). Ci sono molte altre costanti come questa, tutte configurabili. Quest'ultimo è un candidato particolarmente comune, almeno su hardware moderno. Ne esamineremo di più tra un po'.
Costi di avvio
I primi numeri che vedi dopo cost=
sono conosciuti come il "costo di avvio". Questa è una stima del tempo necessario per recuperare la prima riga . In quanto tale, il costo di avvio di un'operazione include il costo dei suoi figli.
Per una scansione sequenziale, il costo di avvio sarà generalmente vicino a zero, poiché può iniziare a recuperare le righe immediatamente. Per un'operazione di ordinamento, sarà maggiore perché è necessario eseguire gran parte del lavoro prima che le righe possano iniziare a essere restituite.
Per vedere un esempio, creiamo una semplice tabella di test con 1000 nomi utente:
CREATE TABLE users ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, username text NOT NULL); INSERT INTO users (username) SELECT 'person' || n FROM generate_series(1, 1000) AS n; ANALYZE users;
Diamo un'occhiata a un semplice piano di query, con un paio di operazioni:
EXPLAIN SELECT * FROM users ORDER BY username; QUERY PLAN | --------------------------------------------------------------+ Sort (cost=66.83..69.33 rows=1000 width=17) | Sort Key: username | -> Seq Scan on users (cost=0.00..17.00 rows=1000 width=17)|
Nel piano di query sopra, come previsto, il costo di esecuzione dell'istruzione stimato per il Seq Scan
è 0.00
e per Sort
è 66.83
.
Costi totali
La seconda statistica dei costi, dopo il costo di avvio ei due punti, è nota come "costo totale". Questa è una stima del tempo necessario per restituire tutte le righe .
Esaminiamo di nuovo quel piano di query di esempio:
QUERY PLAN | --------------------------------------------------------------+ Sort (cost=66.83..69.33 rows=1000 width=17) | Sort Key: username | -> Seq Scan on users (cost=0.00..17.00 rows=1000 width=17)|
Possiamo vedere che il costo totale del Seq Scan
l'operazione è 17.00
. Per il Sort
l'operazione è 69,33, che non è molto più del suo costo di avvio (come previsto).
I costi totali generalmente includono il costo delle operazioni precedenti. Ad esempio, il costo totale dell'operazione di ordinamento di cui sopra include quello della scansione sequenziale.
Un'importante eccezione è LIMIT
clausole, che il pianificatore utilizza per stimare se può interrompere anticipatamente. Se necessita solo di un numero ridotto di righe, le cui condizioni sono comuni, può calcolare che una scelta di scansione più semplice sia più economica (probabilmente più veloce).
Ad esempio:
EXPLAIN SELECT * FROM users LIMIT 1; QUERY PLAN | --------------------------------------------------------------+ Limit (cost=0.00..0.02 rows=1 width=17) | -> Seq Scan on users (cost=0.00..17.00 rows=1000 width=17)|
Come puoi vedere, il costo totale riportato sul nodo Seq Scan è ancora 17,00, ma il costo completo dell'operazione Limit è riportato essere 0,02. Questo perché il pianificatore prevede di dover elaborare solo 1 riga su 1000, quindi il costo, in questo caso, è stimato a 1000° del totale.
Come vengono calcolati i costi
Per calcolare questi costi, il pianificatore di query di Postgres utilizza sia le costanti (alcune delle quali abbiamo già visto) sia i metadati sui contenuti del database. I metadati sono spesso chiamati "statistiche".
Le statistiche vengono raccolte tramite ANALYZE
(da non confondere con EXPLAIN
parametro con lo stesso nome) e memorizzato in pg_statistic
. Vengono inoltre aggiornati automaticamente come parte dell'autovacuum.
Queste statistiche includono una serie di cose molto utili, come approssimativamente il numero di righe di ogni tabella e quali sono i valori più comuni in ogni colonna.
Diamo un'occhiata a un semplice esempio, utilizzando gli stessi dati di query di prima:
EXPLAIN SELECT count(*) FROM users; QUERY PLAN | -------------------------------------------------------------+ Aggregate (cost=19.50..19.51 rows=1 width=8) | -> Seq Scan on users (cost=0.00..17.00 rows=1000 width=0)|
Nel nostro caso, le statistiche del pianificatore suggerivano che i dati per la tabella fossero archiviati entro 7 pagine (o blocchi) e che sarebbero state restituite 1000 righe. I parametri di costo seq_page_cost
, cpu_tuple_cost
e cpu_operator_cost
sono stati lasciati ai valori predefiniti di 1
, 0.01
e 0.0025
rispettivamente.
Pertanto, il costo totale di Seq Scan è stato calcolato come:
Total cost of Seq Scan = (estimated sequential page reads * seq_page_cost) + (estimated rows returned * cpu_tuple_cost) = (7 * 1) + (1000 * 0.01) = 7 + 10.00 = 17.00
E per l'Aggregato come:
Total cost of Aggregate = (cost of Seq Scan) + (estimated rows processed * cpu_operator_cost) + (estimated rows returned * cpu_tuple_cost) = (17.00) + (1000 * 0.0025) + (1 * 0.01) = 17.00 + 2.50 + 0.01 = 19.51
Come il pianificatore utilizza i costi
Poiché sappiamo che Postgres sceglierà il piano di query con il costo totale più basso, possiamo usarlo per cercare di capire le scelte che ha fatto. Ad esempio, se una query non utilizza un indice che ti aspetti, puoi utilizzare impostazioni come enable_seqscan
per scoraggiare in modo massiccio alcune scelte del piano di query. A questo punto, non dovresti essere sorpreso di sapere che impostazioni come questa funzionano aumentando i costi!
I numeri di riga sono una parte estremamente importante della stima dei costi. Vengono utilizzati per calcolare le stime per diversi ordini di unione, algoritmi di unione, tipi di scansione e altro ancora. Le stime dei costi in linea che sono superate di molto possono portare a una stima dei costi superata di molto, il che alla fine può comportare una scelta del piano non ottimale.
Utilizzo di EXPLAIN ANALYZE per ottenere un piano di query
Quando scrivi istruzioni SQL in PostgreSQL, ANALYZE
command è la chiave per ottimizzare le query, rendendole più veloci ed efficienti. Oltre a visualizzare il piano di query e le stime PostgreSQL, EXPLAIN ANALYZE
l'opzione esegue la query (fai attenzione con UPDATE
e DELETE
!), e mostra il tempo di esecuzione effettivo e il numero di righe per ogni passaggio del processo di esecuzione. Ciò è necessario per monitorare le prestazioni SQL.
Puoi usare EXPLAIN ANALYZE
per confrontare il numero stimato di righe con le righe effettive restituite da ciascuna operazione.
Diamo un'occhiata a un esempio, utilizzando di nuovo gli stessi dati:
QUERY PLAN | -----------------------------------------------------------------------------------------------------------+ Sort (cost=66.83..69.33 rows=1000 width=17) (actual time=20.569..20.684 rows=1000 loops=1) | Sort Key: username | Sort Method: quicksort Memory: 102kB | -> Seq Scan on users (cost=0.00..17.00 rows=1000 width=17) (actual time=0.048..0.596 rows=1000 loops=1)| Planning Time: 0.171 ms | Execution Time: 20.793 ms |
Possiamo vedere che il costo di esecuzione totale è ancora 69,33, con la maggior parte di ciò che è l'operazione di ordinamento e 17,00 proveniente dalla scansione sequenziale. Tieni presente che il tempo di esecuzione della query è di poco inferiore a 21 ms.
Scansione sequenziale vs. Scansione indice
Ora aggiungiamo un indice per cercare di evitare quella sorta di costoso dell'intera tabella:
CREATE INDEX people_username_idx ON users (username); EXPLAIN ANALYZE SELECT * FROM users ORDER BY username; QUERY PLAN | ---------------------------------------------------------------------------------------------------------------------------------+ Index Scan using people_username_idx on users (cost=0.28..28.27 rows=1000 width=17) (actual time=0.052..1.494 rows=1000 loops=1)| Planning Time: 0.186 ms | Execution Time: 1.686 ms |
Come puoi vedere, il pianificatore di query ha ora scelto una scansione dell'indice, poiché il costo totale di quel piano è 28,27 (inferiore a 69,33). Sembra che la scansione dell'indice sia stata più efficiente della scansione sequenziale, poiché il tempo di esecuzione della query è ora di poco inferiore a 2 ms.
Aiutare il pianificatore a stimare in modo più accurato
Possiamo aiutare il pianificatore a stimare in modo più accurato in due modi:
- Aiutalo a raccogliere statistiche migliori
- Ottimizza le costanti che usa per i calcoli
Le statistiche possono essere particolarmente negative dopo una grande modifica ai dati in una tabella. Pertanto, quando carichi molti dati in una tabella, puoi aiutare Postgres eseguendo un manuale ANALYZE
su di essa. Inoltre, le statistiche non persistono su un aggiornamento di una versione principale, quindi è un altro momento importante per farlo.
Naturalmente, anche le tabelle cambiano nel tempo, quindi può essere molto utile regolare le impostazioni di autovacuum per assicurarsi che venga eseguito abbastanza frequentemente per il tuo carico di lavoro.
Se hai problemi con stime errate per una colonna con una distribuzione asimmetrica, potresti trarre vantaggio dall'aumentare la quantità di informazioni raccolte da Postgres utilizzando ALTER TABLE SET STATISTICS
comando, o anche il default_statistics_target
per l'intero database.
Un'altra causa comune di stime errate è che, per impostazione predefinita, Postgres presumerà che due colonne siano indipendenti. Puoi risolvere il problema chiedendogli di raccogliere i dati di correlazione su due colonne della stessa tabella tramite statistiche estese.
Sul fronte dell'ottimizzazione costante, ci sono molti parametri che puoi regolare per adattarli al tuo hardware. Supponendo che tu stia utilizzando SSD, probabilmente vorrai almeno regolare le tue impostazioni di random_page_cost
. Il valore predefinito è 4, che è 4 volte più costoso del seq_page_cost
abbiamo guardato prima. Questo rapporto aveva senso sui dischi rotanti, ma sugli SSD tende a penalizzare troppo l'I/O casuale. Pertanto, un'impostazione più vicina a 1, o tra 1 e 2, potrebbe avere più senso. In ScaleGrid, il valore predefinito è 1.
Posso rimuovere i costi dai piani di query?
Per molti dei motivi sopra menzionati, la maggior parte delle persone lascia i costi durante l'esecuzione di EXPLAIN
. Tuttavia, se lo desideri, puoi disattivarli utilizzando il COSTS
parametro.
EXPLAIN (COSTS OFF) SELECT * FROM users LIMIT 1; QUERY PLAN | -----------------------+ Limit | -> Seq Scan on users|
Conclusione
Ricapitolando, i costi nei piani di query sono le stime di Postgres per quanto tempo impiegherà una query SQL, in un'unità arbitraria.
Sceglie il piano con il costo complessivo più basso, in base ad alcune costanti configurabili e ad alcune statistiche raccolte.
Aiutarlo a stimare questi costi in modo più accurato è molto importante per aiutarlo a fare buone scelte e mantenere le tue query efficienti.
|