Дълго време един от най-известните недостатъци на PostgreSQL беше способността за паралелизиране на заявки. С пускането на версия 9.6 това вече няма да е проблем. Беше свършена страхотна работа по този въпрос, като се започне от комит 80558c1, въвеждането на паралелно последователно сканиране, което ще видим в хода на тази статия.
Първо, трябва да вземете под внимание:развитието на тази функция е непрекъснато и някои параметри са променили имената между комит и друг. Тази статия е написана чрез плащане, направено на 17 юни, и някои функции, илюстрирани тук, ще присъстват само във версия 9.6 beta2.
В сравнение с версията 9.5, в конфигурационния файл са въведени нови параметри. Това са:
- max_parallel_workers_per_gather :броят на работниците, които могат да помогнат за последователно сканиране на таблица;
- min_parallel_relation_size :минималният размер, който трябва да има една връзка, за да може планиращият да обмисли използването на допълнителни работници;
- parallel_setup_cost :параметърът за планиране, който оценява разходите за създаване на екземпляр на работник;
- parallel_tuple_cost :параметърът за планиране, който оценява разходите за прехвърляне на кортеж от един работник към друг;
- force_parallel_mode :параметър, полезен за тестване, силен паралелизъм и също така заявка, в която плановникът би работил по други начини.
Нека видим как допълнителните работници могат да бъдат използвани за ускоряване на нашите заявки. Създаваме тестова таблица с поле INT и сто милиона записа:
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 има max_parallel_workers_per_gather
зададено на 2 по подразбиране, за което двама работници ще бъдат активирани по време на последователно сканиране.
Обикновеното последователно сканиране не представлява никакви новости:
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)
Всъщност наличието на WHERE
за паралелизиране е необходима клауза:
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)
Можем да се върнем към предишното действие и да наблюдаваме настройката на разликите max_parallel_workers_per_gather
до 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)
Време 2,5 пъти по-голямо.
Програмистът не винаги смята, че паралелното последователно сканиране е най-добрият вариант. Ако заявката не е достатъчно селективна и има много кортежи за прехвърляне от работник на работник, тя може да предпочете „класическо“ последователно сканиране:
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)
Всъщност, ако се опитаме да принудим паралелно последователно сканиране, получаваме по-лош резултат:
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)
Броят на работниците може да бъде увеличен до max_worker_processes
(по подразбиране:8). Възстановяваме стойността на parallel_tuple_cost
и виждаме какво се случва чрез увеличаване на max_parallel_workers_per_gather
до 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)
Въпреки че PostgreSQL може да използва до 8 работници, той е инстанцирал само шест. Това е така, защото Postgres също оптимизира броя на работниците според размера на таблицата и min_parallel_relation_size
. Броят на работниците, предоставени от postgres, се основава на геометрична прогресия с 3 като общо съотношение 3 и min_parallel_relation_size
като мащабен фактор. Ето един пример. Като се има предвид 8MB параметър по подразбиране:
Размер | Работник |
---|---|
<8MB | 0 |
<24MB | 1 |
<72MB | 2 |
<216MB | 3 |
<648MB | 4 |
<1944MB | 5 |
<5822MB | 6 |
… | … |
Размерът на нашата таблица е 3458 MB, така че 6 е максималният брой налични работници.
postgres=# \dt+ test
List of relations
Schema | Name | Type | Owner | Size | Description
--------+------+-------+----------+---------+-------------
public | test | table | postgres | 3458 MB |
(1 row)
Накрая ще дам кратка демонстрация на подобренията, постигнати чрез този пластир. Изпълнявайки нашата заявка с нарастващ брой нарастващи работници, получаваме следните резултати:
Работници | Време |
---|---|
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 |
Виждаме, че времената драстично се подобряват, докато достигнете една трета от първоначалната стойност. Също така е лесно да се обясни фактът, че не виждаме подобрения между използването на 3 и 6 работници:машината, на която е извършен тестът, има 4 CPU, така че резултатите са стабилни след добавяне на още 3 работници към първоначалния процес .
И накрая, PostgreSQL 9.6 постави началото на паралелизиране на заявки, при което паралелното последователно сканиране е само първият страхотен резултат. Ще видим също, че в 9.6 агрегиранията са успоредни, но това е информация за друга статия, която ще бъде пусната през следващите седмици!