Por muito tempo, uma das deficiências mais conhecidas do PostgreSQL foi a capacidade de paralelizar consultas. Com o lançamento da versão 9.6, isso não será mais um problema. Um grande trabalho tem sido feito neste assunto, começando pelo commit 80558c1, a introdução do scan sequencial paralelo, que veremos no decorrer deste artigo.
Primeiro, você deve tomar nota:o desenvolvimento deste recurso tem sido contínuo e alguns parâmetros mudaram de nome entre um commit e outro. Este artigo foi escrito usando um checkout realizado em 17 de junho e alguns recursos aqui ilustrados estarão presentes apenas na versão 9.6 beta2.
Comparado com a versão 9.5, novos parâmetros foram introduzidos dentro do arquivo de configuração. Esses são:
- max_parallel_workers_per_gather :o número de trabalhadores que podem auxiliar uma varredura sequencial de uma tabela;
- min_parallel_relation_size :o tamanho mínimo que uma relação deve ter para que o planejador considere o uso de trabalhadores adicionais;
- parallel_setup_cost :o parâmetro do planejador que estima o custo de instanciar um trabalhador;
- parallel_tuple_cost :o parâmetro do planejador que estima o custo de transferência de uma tupla de um trabalhador para outro;
- force_parallel_mode :parâmetro útil para testes, paralelismo forte e também uma consulta na qual o planejador operaria de outras maneiras.
Vamos ver como os trabalhadores adicionais podem ser usados para acelerar nossas consultas. Criamos uma tabela de teste com um campo INT e cem milhões de registros:
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 tem
max_parallel_workers_per_gather
definido como 2 por padrão, para o qual dois trabalhadores serão ativados durante uma varredura sequencial. Uma simples varredura sequencial não apresenta nenhuma novidade:
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)
Na verdade, a presença de um
WHERE
cláusula é necessária para paralelização: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)
Podemos voltar para a ação anterior e observar a configuração de diferenças
max_parallel_workers_per_gather
para 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)
Um tempo 2,5 vezes maior.
O planejador nem sempre considera uma varredura sequencial paralela como a melhor opção. Se uma consulta não for seletiva o suficiente e houver muitas tuplas para transferir de trabalhador para trabalhador, ela pode preferir uma varredura sequencial “clássica”:
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)
De fato, se tentarmos forçar uma varredura sequencial paralela, obteremos um resultado pior:
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)
O número de trabalhadores pode ser aumentado até
max_worker_processes
(padrão:8). Restauramos o valor de parallel_tuple_cost
e vemos o que acontece 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)
Embora o PostgreSQL pudesse usar até 8 trabalhadores, ele instancia apenas seis. Isso ocorre porque o Postgres também otimiza o número de trabalhadores de acordo com o tamanho da tabela e o
min_parallel_relation_size
. O número de trabalhadores disponibilizados pelo postgres é baseado em uma progressão geométrica com 3 como razão comum 3 e min_parallel_relation_size
como fator de escala. Aqui está um exemplo. Considerando os 8 MB de parâmetro padrão:Tamanho | Trabalhador |
---|---|
<8MB | 0 |
<24MB | 1 |
<72MB | 2 |
<216MB | 3 |
<648 MB | 4 |
<1944MB | 5 |
<5822MB | 6 |
… | … |
O tamanho da nossa tabela é 3458 MB, então 6 é o número máximo de trabalhadores disponíveis.
postgres=# \dt+ test
List of relations
Schema | Name | Type | Owner | Size | Description
--------+------+-------+----------+---------+-------------
public | test | table | postgres | 3458 MB |
(1 row)
Por fim, farei uma breve demonstração das melhorias alcançadas por meio deste patch. Executando nossa consulta com um número crescente de trabalhadores em crescimento, obtemos os seguintes resultados:
Trabalhadores | Hora |
---|---|
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 |
Podemos ver que os tempos melhoram drasticamente, até chegar a um terço do valor inicial. Também é simples explicar o fato de que não vemos melhorias entre o uso de 3 e 6 trabalhadores:a máquina em que o teste foi executado possui 4 CPUs, portanto, os resultados são estáveis após adicionar mais 3 trabalhadores ao processo original .
Finalmente, o PostgreSQL 9.6 preparou o cenário para a paralelização de consultas, na qual a varredura sequencial paralela é apenas o primeiro grande resultado. Também veremos que na 9.6 as agregações foram paralelizadas, mas isso é informação para outro artigo que será lançado nas próximas semanas!