Een van de meest bekende tekortkomingen van PostgreSQL was lange tijd de mogelijkheid om queries te parallelliseren. Met de release van versie 9.6 zal dit geen probleem meer zijn. Er is geweldig werk verricht over dit onderwerp, te beginnen met de commit 80558c1, de introductie van parallelle sequentiële scan, die we in de loop van dit artikel zullen zien.
Ten eerste moet je er rekening mee houden:de ontwikkeling van deze functie is continu geweest en sommige parameters hebben namen veranderd tussen een commit en een andere. Dit artikel is geschreven met behulp van een checkout op 17 juni en sommige functies die hier worden geïllustreerd, zijn alleen aanwezig in versie 9.6 beta2.
In vergelijking met release 9.5 zijn er nieuwe parameters in het configuratiebestand geïntroduceerd. Dit zijn:
- max_parallel_workers_per_gather :het aantal werkers dat kan helpen bij een opeenvolgende scan van een tafel;
- min_parallel_relation_size :de minimale grootte die een relatie moet hebben opdat de planner rekening houdt met het gebruik van extra werknemers;
- parallel_setup_cost :de plannerparameter die de kosten schat van het instantiëren van een werknemer;
- parallel_tuple_cost :de plannerparameter die de kosten schat van het overbrengen van een tupel van de ene werknemer naar de andere;
- force_parallel_mode :parameter nuttig voor testen, sterk parallellisme en ook een query waarin de planner op andere manieren zou werken.
Laten we eens kijken hoe de extra werknemers kunnen worden gebruikt om onze zoekopdrachten te versnellen. We maken een testtabel met een INT-veld en honderd miljoen records:
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 heeft max_parallel_workers_per_gather
standaard ingesteld op 2, waarvoor twee werknemers worden geactiveerd tijdens een sequentiële scan.
Een eenvoudige sequentiële scan levert geen nieuwigheden op:
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)
In feite is de aanwezigheid van een WHERE
clausule is vereist voor parallellisatie:
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)
We kunnen teruggaan naar de vorige actie en de verschillen observeren instelling max_parallel_workers_per_gather
naar 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)
Een tijd die 2,5 keer zo groot is.
De planner vindt een parallelle sequentiële scan niet altijd de beste optie. Als een zoekopdracht niet selectief genoeg is en er veel tuples zijn om van werknemer naar werknemer over te dragen, kan deze de voorkeur geven aan een "klassieke" sequentiële scan:
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)
Als we een parallelle sequentiële scan proberen te forceren, krijgen we zelfs een slechter resultaat:
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)
Het aantal werkers kan worden verhoogd tot max_worker_processes
(standaard:8). We herstellen de waarde van parallel_tuple_cost
en we zien wat er gebeurt door max_parallel_workers_per_gather
. te vergroten tot 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)
Hoewel PostgreSQL tot 8 werkers kon gebruiken, heeft het er slechts zes geïnstantieerd. Dit komt omdat Postgres ook het aantal werkers optimaliseert op basis van de grootte van de tabel en de min_parallel_relation_size
. Het aantal werkers dat door postgres beschikbaar wordt gesteld, is gebaseerd op een geometrische progressie met 3 als gemeenschappelijke verhouding 3 en min_parallel_relation_size
als schaalfactor. Hier is een voorbeeld. Gezien de 8 MB standaardparameter:
Maat | Werknemer |
---|---|
<8MB | 0 |
<24MB | 1 |
<72MB | 2 |
<216MB | 3 |
<648MB | 4 |
<1944MB | 5 |
<5822MB | 6 |
… | … |
Onze tabelgrootte is 3458 MB, dus 6 is het maximale aantal beschikbare werkers.
postgres=# \dt+ test
List of relations
Schema | Name | Type | Owner | Size | Description
--------+------+-------+----------+---------+-------------
public | test | table | postgres | 3458 MB |
(1 row)
Ten slotte zal ik een korte demonstratie geven van de verbeteringen die door deze patch zijn bereikt. Als we onze zoekopdracht uitvoeren met een groeiend aantal groeiende werknemers, krijgen we de volgende resultaten:
Werknemers | Tijd |
---|---|
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 |
We kunnen zien dat de tijden dramatisch verbeteren, totdat u een derde van de oorspronkelijke waarde bereikt. Het is ook eenvoudig uit te leggen dat we geen verbeteringen zien tussen het gebruik van 3 en 6 werkers:de machine waarop de test is uitgevoerd heeft 4 CPU's, dus de resultaten zijn stabiel nadat er nog 3 werkers aan het oorspronkelijke proces zijn toegevoegd .
Ten slotte heeft PostgreSQL 9.6 de weg geëffend voor parallellisatie van query's, waarbij parallelle sequentiële scan slechts het eerste goede resultaat is. We zullen ook zien dat in 9.6 aggregaties zijn geparalleliseerd, maar dat is informatie voor een ander artikel dat in de komende weken zal worden gepubliceerd!