До Postgres 9.6 одна сессия = один процесс = одно ядро. Если у тебя 32 ядра и таблица 100 GiB, SELECT count(*) всё равно поедет одним потоком. С 9.6 это поменялось: появился parallel query — planner может добавить в план узлы Gather/Gather Merge, которые порождают worker-процессы, и работа делится между ними.
В этом уроке — как это устроено внутри, когда полезно, и почему в OLTP его обычно отключают.
Структура параллельного плана
Идея проста: главный (leader) процесс получает запрос, планирует, видит, что параллелизм окупится, и запускает N worker-процессов. Каждый делает кусок работы — например, читает свою долю страниц таблицы. Затем результаты «склеиваются» через узел Gather.
Узел Gather на корне получает потоки из workers. Каждый worker — отдельный процесс, со своим memory context. Координацию делает Dynamic Shared Memory.
Ключевые компоненты:
- Leader — изначальный процесс. Запускает workers, координирует, собирает результат, иногда сам участвует в работе.
- Worker — отдельный backend-процесс, форкнутый специально для запроса. Имеет свою private memory, но shared buffers общий с leader.
- DSM (Dynamic Shared Memory) — механизм связи: leader пишет туда план и параметры, workers читают, пишут , leader читает.partial aggregates
- Gather / Gather Merge — узел плана на корне. Gather просто склеивает потоки в любом порядке; Gather Merge сохраняет упорядоченность (для запросов с
ORDER BY).
Что бывает параллельным
Не каждый узел планируется параллельно. На сегодня:
- Да:
Parallel Seq Scan— почти всегда. - Да:
Parallel Index Scan/Parallel Index Only Scan— с PG 10+. - Да:
Parallel Bitmap Heap Scan— bitmap строится одним процессом, heap читается параллельно. - Да:
Parallel Hash Join— workers совместно строят hash table в shared memory. - Да:
Parallel Aggregate— partial aggregates → final aggregate. - Нет: любая user-defined function с пометкой не
PARALLEL SAFE— блокирует параллелизм. - Нет:
SELECT FOR UPDATE, CTE сMATERIALIZED, некоторые типы subqueries.
Проверить парallel-safety функции: SELECT proname, proparallel FROM pg_proc WHERE proname = 'my_func'; (s = safe, r = restricted, u = unsafe).
Параметры
Главные GUC:
max_parallel_workers_per_gather(default 2) — сколько workers’ов на один Gather. Это per query, не total.max_parallel_workers(default 8) — общий лимит worker’ов в кластере.max_worker_processes(default 8) — самый верхний потолок (включает logical replication, autovacuum workers и пр.). Менять требует restart.parallel_setup_cost(default 1000) — оценка стоимости порождения workers’ов; чем выше, тем менее охотно planner идёт в параллелизм.parallel_tuple_cost(default 0.1) — стоимость передачи одной строки через Gather.min_parallel_table_scan_size(default 8MB) — таблицы меньше этого размера не сканируются параллельно никогда.
Смотрим, как настроен параллелизм в текущей сессии. В pglite это работает по конфигу WebAssembly-сборки; в реальной БД на 8 ядрах ты увидишь default 2 и сможешь поднять до 6-8.
Когда planner идёт в параллелизм
Грубо: когда оценочная стоимость последовательного плана достаточно велика, чтобы превысить parallel_setup_cost + parallel_tuple_cost × rows, и есть парallel-safe план, planner добавит Gather.
Типичные сценарии «да, параллелизм оправдан»:
- Seq Scan большой таблицы (10M+ строк). Чтение делится между workers, потом partial aggregates → final.
- Hash Join большой и маленькой: одна сторона маленькая → hash table, вторая (большая) сканируется параллельно.
- Aggregation без GROUP BY или с малым числом групп: partial sums легко комбинировать.
- CREATE INDEX (с PG 11+) — индекс строится параллельно на больших таблицах.
И «нет, не нужно»:
- OLTP-запросы — короткие, на одну-несколько строк.
parallel_setup_cost = 1000означает, что запрос сcost < 1000точно не получит workers. На индексных point-lookup’ах cost обычно 5-50. - Маленькие таблицы (меньше 8 MB по умолчанию).
- Запросы с не-parallel-safe функциями.
- Высококонкурентные нагрузки, где у каждой сессии и так свой backend, а ядер мало.
Посмотрим в EXPLAIN
В pglite реального параллелизма нет (single-threaded WebAssembly среда), но планировщик можно «обмануть», и он покажет Gather в плане, если таблица достаточно велика. В реальной БД с 8 ядрами ты увидишь нечто похожее.
Большой Seq Scan на orders (500K строк). В реальной БД planner может добавить Gather с 2-4 workers; в pglite — реализация single-thread, но cost-модель работает. Дата-сет инициализируется ~10-15 секунд.
Принудительный параллелизм: снижаем parallel_setup_cost и тривиализируем минимальный размер. В реальной БД с 8 ядрами увидишь Gather + 4 workers в плане.
В реальной системе оптимальная настройка зависит от природы workload:
- OLAP / analytics / reporting: подними
max_parallel_workers_per_gatherдо количества физических ядер / 2 (на 16-core — 6-8). Хорошо живёт сparallel_setup_costравным дефолту. - OLTP: оставь default или даже снизь
max_parallel_workers_per_gather = 0, чтобы исключить unexpected behavior на тяжёлом long-running запросе. - Mixed: разделяй на пользователей или роли через
ALTER ROLE analytics SET max_parallel_workers_per_gather = 8.
Когда параллелизм вредит
- Маленькие, частые запросы. Setup overhead (~5-30 ms) больше самого запроса.
- Точечные индексные lookups — даже на огромных таблицах. Index Scan возвращает 1 строку, делить нечего.
- Высокая конкуренция: если у тебя одновременно 100 сессий, и каждая запросит 4 worker’а —
max_worker_processesмгновенно исчерпается, новые запросы будут падать в режим without parallelism, неравномерно. - Когда узкое место — диск, а не CPU: параллельный Seq Scan просто упирается в IOPS дальше. Параллелизм даёт CPU-выигрыш, не I/O.
Gather Merge vs Gather
Тонкий момент. Когда есть ORDER BY сверху, workers возвращают частично отсортированные потоки. Если использовать обычный Gather, leader получит несортированный микс — придётся пересортировать. Поэтому planner выбирает Gather Merge: он делает k-way merge из k отсортированных потоков, сохраняя order.
Поэтому EXPLAIN на запросе с ORDER BY часто покажет Gather Merge над Parallel Index Scan — каждый worker сканирует свой кусок индекса (в order), Gather Merge склеивает.
Чек-лист
- Parallel query с PG 9.6: Gather/Gather Merge на корне плана + N worker-процессов.
- Workers общаются через DSM (Dynamic Shared Memory).
- Параллельные узлы: Seq Scan, Index Scan, Bitmap Scan, Hash Join, Aggregate.
- Главный GUC:
max_parallel_workers_per_gather(default 2),max_parallel_workers(default 8). - Planner выбирает параллелизм, когда
parallel_setup_cost + tuple_cost × rows < sequential_cost. - Gather склеивает потоки в любом порядке, Gather Merge сохраняет order.
- Не используется: OLTP, маленькие таблицы (меньше 8 MB), non-parallel-safe функции, точечные lookups.
- В OLAP подними
max_parallel_workers_per_gatherдо ядер/2; в OLTP оставь default или 0.