Durante mucho tiempo, una de las deficiencias más conocidas de PostgreSQL fue la capacidad de paralelizar consultas. Con el lanzamiento de la versión 9.6, esto ya no será un problema. Se ha hecho un gran trabajo en este tema, a partir de la confirmación 80558c1, la introducción del escaneo secuencial paralelo, que veremos en el transcurso de este artículo.
En primer lugar, debes tomar nota:el desarrollo de esta característica ha sido continuo y algunos parámetros han cambiado de nombre entre un commit y otro. Este artículo se ha escrito usando un pago realizado el 17 de junio y algunas características aquí ilustradas estarán presentes solo en la versión 9.6 beta2.
En comparación con la versión 9.5, se han introducido nuevos parámetros dentro del archivo de configuración. Estos son:
- max_parallel_workers_per_gather :el número de trabajadores que pueden ayudar en un escaneo secuencial de una tabla;
- min_parallel_relation_size :el tamaño mínimo que debe tener una relación para que el planificador considere el uso de trabajadores adicionales;
- costo_de_configuración_paralela :el parámetro del planificador que estima el costo de instanciar un trabajador;
- coste_tuple_paralelo :el parámetro del planificador que estima el costo de transferir una tupla de un trabajador a otro;
- modo_forzado_paralelo :parámetro útil para pruebas, fuerte paralelismo y también una consulta en la que el planificador operaría de otras formas.
Veamos cómo se pueden utilizar los trabajadores adicionales para acelerar nuestras consultas. Creamos una tabla de prueba con un campo INT y cien millones 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 tiene max_parallel_workers_per_gather
establecido en 2 de forma predeterminada, para lo cual se activarán dos trabajadores durante un análisis secuencial.
Un simple escaneo secuencial no presenta ninguna novedad:
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)
De hecho, la presencia de un WHERE
se requiere una cláusula para la paralelización:
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 volver a la acción anterior y observar las diferencias configurando 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 tiempo 2,5 veces mayor.
El planificador no siempre considera que un escaneo secuencial paralelo sea la mejor opción. Si una consulta no es lo suficientemente selectiva y hay muchas tuplas para transferir de un trabajador a otro, puede preferir un escaneo secuencial "clásico":
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 hecho, si intentamos forzar un escaneo secuencial paralelo, obtenemos un peor resultado:
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)
El número de trabajadores se puede aumentar hasta max_worker_processes
(predeterminado:8). Restauramos el valor de parallel_tuple_cost
y vemos lo que sucede al aumentar 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)
Aunque PostgreSQL podría usar hasta 8 trabajadores, solo ha instanciado seis. Esto se debe a que Postgres también optimiza la cantidad de trabajadores según el tamaño de la tabla y el min_parallel_relation_size
. El número de trabajadores que postgres pone a disposición se basa en una progresión geométrica con 3 como proporción común 3 y min_parallel_relation_size
como factor de escala. Aquí hay un ejemplo. Teniendo en cuenta los 8 MB de parámetro predeterminado:
Tamaño | Trabajador |
---|---|
<8MB | 0 |
<24MB | 1 |
<72 MB | 2 |
<216 MB | 3 |
<648 MB | 4 |
<1944MB | 5 |
<5822MB | 6 |
… | … |
El tamaño de nuestra tabla es de 3458 MB, por lo que 6 es el número máximo de trabajadores disponibles.
postgres=# \dt+ test
List of relations
Schema | Name | Type | Owner | Size | Description
--------+------+-------+----------+---------+-------------
public | test | table | postgres | 3458 MB |
(1 row)
Finalmente, daré una breve demostración de las mejoras logradas a través de este parche. Ejecutando nuestra consulta con un número creciente de trabajadores en crecimiento, obtenemos los siguientes resultados:
Trabajadores | Tiempo |
---|---|
0 | 24767.848ms |
1 | 14855,961ms |
2 | 10415,661ms |
3 | 8041,187ms |
4 | 8090.855ms |
5 | 8082,937ms |
6 | 8061,939ms |
Podemos ver que los tiempos mejoran notablemente, hasta llegar a un tercio del valor inicial. También es simple de explicar el hecho de que no vemos mejoras entre el uso de 3 y 6 trabajadores:la máquina en la que se ejecutó la prueba tiene 4 CPU, por lo que los resultados son estables después de haber agregado 3 trabajadores más al proceso original. .
Finalmente, PostgreSQL 9.6 ha preparado el escenario para la paralelización de consultas, en la que el análisis secuencial paralelo es solo el primer gran resultado. También veremos que en 9.6, las agregaciones se han paralelizado, ¡pero esa es información para otro artículo que se publicará en las próximas semanas!