PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

PostgreSQL 9.6:Scansione sequenziale parallela

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!