Przez długi czas jednym z najbardziej znanych mankamentów PostgreSQL była możliwość zrównoleglania zapytań. Wraz z wydaniem wersji 9.6 nie będzie to już problemem. Wykonano w tym temacie świetną robotę, począwszy od zatwierdzenia 80558c1, wprowadzenia równoległego skanowania sekwencyjnego, które zobaczymy w tym artykule.
Po pierwsze, musisz wziąć pod uwagę:rozwój tej funkcji był ciągły, a niektóre parametry zmieniły nazwy między zatwierdzeniem a innym. Ten artykuł został napisany przy kasie wykonanej 17 czerwca, a niektóre z przedstawionych tu funkcji będą dostępne tylko w wersji 9.6 beta2.
W porównaniu do wersji 9.5 w pliku konfiguracyjnym wprowadzono nowe parametry. Są to:
- max_parallel_workers_per_gather :liczba pracowników, którzy mogą asystować przy sekwencyjnym skanowaniu tabeli;
- min_parallel_relation_size :minimalny rozmiar, jaki musi mieć relacja, aby planista rozważył wykorzystanie dodatkowych pracowników;
- parallel_setup_cost :parametr planisty, który szacuje koszt wystąpienia pracownika;
- parallel_tuple_cost :parametr planisty, który szacuje koszt przeniesienia krotki od jednego pracownika do drugiego;
- force_parallel_mode :parametr przydatny do testowania, silna równoległość, a także zapytanie, w którym planista działałby w inny sposób.
Zobaczmy, jak można wykorzystać dodatkowych pracowników do przyspieszenia naszych zapytań. Tworzymy tabelę testową z polem INT i stu milionami rekordów:
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 ma max_parallel_workers_per_gather
domyślnie ustawione na 2, dla którego dwóch pracowników zostanie aktywowanych podczas skanowania sekwencyjnego.
Proste skanowanie sekwencyjne nie przedstawia żadnych nowości:
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)
W rzeczywistości obecność WHERE
klauzula jest wymagana do zrównoleglania:
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)
Możemy wrócić do poprzedniej akcji i zaobserwować ustawienie różnic max_parallel_workers_per_gather
do 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)
Czas 2,5 razy większy.
Planista nie zawsze uważa, że równoległe skanowanie sekwencyjne jest najlepszą opcją. Jeśli zapytanie nie jest wystarczająco selektywne i istnieje wiele krotek do przeniesienia od pracownika do pracownika, może preferować „klasyczne” skanowanie sekwencyjne:
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)
W rzeczywistości, jeśli spróbujemy wymusić równoległe skanowanie sekwencyjne, uzyskamy gorszy wynik:
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)
Liczbę pracowników można zwiększyć do max_worker_processes
(domyślnie:8). Przywracamy wartość parallel_tuple_cost
i widzimy, co się dzieje, zwiększając max_parallel_workers_per_gather
do 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)
Mimo że PostgreSQL może używać do 8 procesów roboczych, utworzył tylko sześć instancji. Dzieje się tak, ponieważ Postgres optymalizuje również liczbę pracowników zgodnie z rozmiarem tabeli i min_parallel_relation_size
. Liczba pracowników udostępnionych przez postgres opiera się na postępie geometrycznym z 3 jako wspólnym stosunkiem 3 i min_parallel_relation_size
jako współczynnik skali. Oto przykład. Biorąc pod uwagę 8MB domyślnego parametru:
Rozmiar | Pracownik |
---|---|
<8 MB | 0 |
<24 MB | 1 |
<72 MB | 2 |
<216 MB | 3 |
<648 MB | 4 |
<1944 MB | 5 |
<5822 MB | 6 |
… | … |
Rozmiar naszego stołu wynosi 3458 MB, więc 6 to maksymalna liczba dostępnych pracowników.
postgres=# \dt+ test
List of relations
Schema | Name | Type | Owner | Size | Description
--------+------+-------+----------+---------+-------------
public | test | table | postgres | 3458 MB |
(1 row)
Na koniec przedstawię krótką prezentację ulepszeń osiągniętych dzięki tej łatce. Przeprowadzając nasze zapytanie z rosnącą liczbą rosnących pracowników, otrzymujemy następujące wyniki:
Pracownicy | Czas |
---|---|
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 |
Widzimy, że czasy dramatycznie się poprawiają, aż do osiągnięcia jednej trzeciej wartości początkowej. Można również łatwo wyjaśnić fakt, że nie widzimy poprawy między wykorzystaniem 3 i 6 pracowników:maszyna, na której przeprowadzono test, ma 4 procesory, więc wyniki są stabilne po dodaniu 3 dodatkowych pracowników do pierwotnego procesu .
Wreszcie, PostgreSQL 9.6 przygotował grunt pod zrównoleglanie zapytań, w którym równoległe skanowanie sekwencyjne jest tylko pierwszym świetnym wynikiem. Zobaczymy również, że w wersji 9.6 agregacje zostały zrównoleglone, ale jest to informacja do innego artykułu, który ukaże się w nadchodzących tygodniach!