[Перевод] PostgreSQL 9.6: Параллельное последовательное сканирование
Во-первых, следует принять к сведению: разработка этого функционала велась непрерывно и некоторые параметры изменили свои имена между коммитами. Данная статья была написана после чекаута, совершенного 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. Количество воркеров доступных постгресу основано на геометрической прогрессии со знаменателем 3 и min_parallel_relation_size в качестве масштабирующего фактора. Вот пример. Учитывая что 8Мб является параметром по умолчанию:
Size | Worker |
<8Мб | 0 |
<24Мб | 1 |
<72Мб | 2 |
<216Мб | 3 |
<648Мб | 4 |
<1944Мб | 5 |
<5822Мб | 6 |
… | … |
Размер нашей таблицы 3548Мб, соответственно 6 является максимальным количеством доступных воркеров.
postgres=# \dt+ test
List of relations
Schema | Name | Type | Owner | Size | Description
--------+------+-------+----------+---------+-------------
public | test | table | postgres | 3458 MB |
(1 row)
Наконец, я дам краткую демонстрацию улучшений, достигнутых с помощью этого патча. Запуская наш запрос с растущим числом воркеров, мы получим следующие результаты:
Size | Worker |
<0 | 24767.848 мс |
<1 | 14855.961 мс |
<2 | 10415.661 мс |
<3 | 8041.187 мс |
<4 | 8090.855 мс |
<5 | 8082.937 мс |
<6 | 8061.939 мс |
Можно видеть, что время выполнения значительно улучшается, пока не достигнет одной трети от исходного значения. Также легко объяснить тот факт, что мы не видим улучшений при использовании 6 воркеров вместо 3: машина, на которой выполнялись тесты имеет 4 процессора, так что результаты стабильны после добавления 3 дополнительных воркеров к оригинальному процессу.
Наконец, PostgreSQL 9.6 вышел на новый этап параллелизации запросов, в котором параллельное последовательное сканирование это только первый отличный результат. Кроме того, в 9.6 версии были распараллелино аггрегирование, но это уже тема для другой статьи, которая выйдет в ближайшие недели!
Комментарии (1)
14 июля 2016 в 12:37
+1↑
↓
Это приятное дополнение к функциональности Postgres, но использовать его нужно с умом. Если на машине с Postgres подсистема ввода-вывода является узким местом, то параллельное сканирование может только усугубить картину, ухудшив общую производительность системы.К тому же, параллельное сканирование — это движение в сторону OLAP, и тут стоит вспомнить, что на одной машине совмещать OLAP и OLTP далеко не лучшее решение, т.к. несколько параллельных запросов аналитиков в 8 воркеров каждый создадут такую нагрузку на IO, что с SLA транзакционной части придется попрощаться