[Перевод] PostgreSQL 9.6: Параллельное последовательное сканирование

В течении долгого времени, одним из самых известных несовершенств PostgreSQL была возможность распараллеливания запросов. С выходом версии 9.6 это перестанет быть проблемой. Большая работа была проделана по этому вопросу, и уже начиная с коммита 80558c1, появляется параллельное последовательное сканирование, с которым мы и познакомимся по ходу этой статьи.
image


Во-первых, следует принять к сведению: разработка этого функционала велась непрерывно и некоторые параметры изменили свои имена между коммитами. Данная статья была написана после чекаута, совершенного 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 транзакционной части придется попрощаться

© Habrahabr.ru