Per molto tempo, una delle carenze più note di PostgreSQL è stata la capacità di parallelizzare le query. Con il rilascio della versione 9.6, questo non sarà più un problema. Su questo argomento è stato fatto un ottimo lavoro, a partire dal commit 80558c1, l'introduzione della scansione sequenziale parallela, che vedremo nel corso di questo articolo.
Innanzitutto, devi prendere nota:lo sviluppo di questa funzionalità è stato continuo e alcuni parametri hanno cambiato nome tra un commit e l'altro. Questo articolo è stato scritto utilizzando un checkout effettuato il 17 giugno e alcune funzionalità qui illustrate saranno presenti solo nella versione 9.6 beta2.
Rispetto alla release 9.5 sono stati introdotti nuovi parametri all'interno del file di configurazione. Questi sono:
- max_parallel_workers_per_gather :il numero di lavoratori che possono assistere a una scansione sequenziale di una tabella;
- min_parallel_relation_size :la dimensione minima che una relazione deve avere affinché il progettista possa considerare l'utilizzo di lavoratori aggiuntivi;
- costo_impostazione_parallela :il parametro del pianificatore che stima il costo di istanziare un lavoratore;
- costo_tuple_parallelo :il parametro del pianificatore che stima il costo del trasferimento di una tupla da un lavoratore all'altro;
- force_parallel_mode :parametro utile per il test, forte parallelismo e anche una query in cui il pianificatore opererebbe in altri modi.
Vediamo come utilizzare i lavoratori aggiuntivi per velocizzare le nostre query. Creiamo una tabella di test con un campo INT e cento milioni di record:
postgres=# CREATE TABLE test (i int);
CREATE TABLE
postgres=# INSERT INTO test SELECT generate_series(1,100000000);
INSERT 0 100000000
postgres=# ANALYSE test;
ANALYZE
PostgreSQL ha max_parallel_workers_per_gather
impostato a 2 per impostazione predefinita, per il quale verranno attivati due lavoratori durante una scansione sequenziale.
Una semplice scansione sequenziale non presenta nessuna novità:
postgres=# EXPLAIN ANALYSE SELECT * FROM test;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
Seq Scan on test (cost=0.00..1442478.32 rows=100000032 width=4) (actual time=0.081..21051.918 rows=100000000 loops=1)
Planning time: 0.077 ms
Execution time: 28055.993 ms
(3 rows)
Infatti la presenza di un WHERE
per la parallelizzazione è richiesta una clausola:
postgres=# EXPLAIN ANALYZE SELECT * FROM test WHERE i=1;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
Gather (cost=1000.00..964311.60 rows=1 width=4) (actual time=3.381..9799.942 rows=1 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Seq Scan on test (cost=0.00..963311.50 rows=0 width=4) (actual time=6525.595..9791.066 rows=0 loops=3)
Filter: (i = 1)
Rows Removed by Filter: 33333333
Planning time: 0.130 ms
Execution time: 9804.484 ms
(8 rows)
Possiamo tornare all'azione precedente e osservare le differenze impostando max_parallel_workers_per_gather
a 0:
postgres=# SET max_parallel_workers_per_gather TO 0;
SET
postgres=# EXPLAIN ANALYZE SELECT * FROM test WHERE i=1;
QUERY PLAN
--------------------------------------------------------------------------------------------------------
Seq Scan on test (cost=0.00..1692478.40 rows=1 width=4) (actual time=0.123..25003.221 rows=1 loops=1)
Filter: (i = 1)
Rows Removed by Filter: 99999999
Planning time: 0.105 ms
Execution time: 25003.263 ms
(5 rows)
Un tempo 2,5 volte maggiore.
Il pianificatore non sempre considera una scansione sequenziale parallela l'opzione migliore. Se una query non è sufficientemente selettiva e ci sono molte tuple da trasferire da lavoratore a lavoratore, potrebbe preferire una scansione sequenziale "classica":
postgres=# SET max_parallel_workers_per_gather TO 2;
SET
postgres=# EXPLAIN ANALYZE SELECT * FROM test WHERE i<90000000;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------
Seq Scan on test (cost=0.00..1692478.40 rows=90116088 width=4) (actual time=0.073..31410.276 rows=89999999 loops=1)
Filter: (i < 90000000)
Rows Removed by Filter: 10000001
Planning time: 0.133 ms
Execution time: 37939.401 ms
(5 rows)
Infatti, se proviamo a forzare una scansione sequenziale parallela, otteniamo un risultato peggiore:
postgres=# SET parallel_tuple_cost TO 0;
SET
postgres=# EXPLAIN ANALYZE SELECT * FROM test WHERE i<90000000;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------
Gather (cost=1000.00..964311.50 rows=90116088 width=4) (actual time=0.454..75546.078 rows=89999999 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Seq Scan on test (cost=0.00..1338795.20 rows=37548370 width=4) (actual time=0.088..20294.670 rows=30000000 loops=3)
Filter: (i < 90000000)
Rows Removed by Filter: 3333334
Planning time: 0.128 ms
Execution time: 83423.577 ms
(8 rows)
Il numero di lavoratori può essere aumentato fino a max_worker_processes
(predefinito:8). Ripristiniamo il valore di parallel_tuple_cost
e vediamo cosa succede aumentando max_parallel_workers_per_gather
a 8.
postgres=# SET parallel_tuple_cost TO DEFAULT ;
SET
postgres=# SET max_parallel_workers_per_gather TO 8;
SET
postgres=# EXPLAIN ANALYZE SELECT * FROM test WHERE i=1;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
Gather (cost=1000.00..651811.50 rows=1 width=4) (actual time=3.684..8248.307 rows=1 loops=1)
Workers Planned: 6
Workers Launched: 6
-> Parallel Seq Scan on test (cost=0.00..650811.40 rows=0 width=4) (actual time=7053.761..8231.174 rows=0 loops=7)
Filter: (i = 1)
Rows Removed by Filter: 14285714
Planning time: 0.124 ms
Execution time: 8250.461 ms
(8 rows)
Anche se PostgreSQL può utilizzare fino a 8 worker, ne ha istanziati solo sei. Questo perché Postgres ottimizza anche il numero di lavoratori in base alle dimensioni del tavolo e al min_parallel_relation_size
. Il numero dei lavoratori messi a disposizione da postgres si basa su una progressione geometrica con 3 come rapporto comune 3 e min_parallel_relation_size
come fattore di scala. Ecco un esempio. Considerando gli 8 MB di parametro predefinito:
Dimensione | Lavoratore |
---|---|
<8MB | 0 |
<24MB | 1 |
<72MB | 2 |
<216MB | 3 |
<648MB | 4 |
<1944MB | 5 |
<5822MB | 6 |
… | … |
La dimensione della nostra tabella è 3458 MB, quindi 6 è il numero massimo di lavoratori disponibili.
postgres=# \dt+ test
List of relations
Schema | Name | Type | Owner | Size | Description
--------+------+-------+----------+---------+-------------
public | test | table | postgres | 3458 MB |
(1 row)
Infine, darò una breve dimostrazione dei miglioramenti ottenuti attraverso questa patch. Eseguendo la nostra query con un numero crescente di lavoratori in crescita, otteniamo i seguenti risultati:
Lavoratori | Ora |
---|---|
0 | 24767,848 ms |
1 | 14855,961 ms |
2 | 10415,661 ms |
3 | 8041,187 ms |
4 | 8090,855 ms |
5 | 8082,937 ms |
6 | 8061,939 ms |
Si vede che i tempi migliorano notevolmente, fino a raggiungere un terzo del valore iniziale. È anche semplice spiegare il fatto che non vediamo miglioramenti tra l'utilizzo di 3 e 6 lavoratori:la macchina su cui è stato eseguito il test ha 4 CPU, quindi i risultati sono stabili dopo aver aggiunto 3 lavoratori in più al processo originale .
Infine, PostgreSQL 9.6 ha posto le basi per la parallelizzazione delle query, in cui la scansione sequenziale parallela è solo il primo grande risultato. Vedremo anche che nella 9.6 le aggregazioni sono state parallelizzate, ma queste sono informazioni per un altro articolo che uscirà nelle prossime settimane!