Learning Platform
Урок 07.04 · 24 мин
Продвинутый
PlannerParallel queryGatherWorkersOLAP

До Postgres 9.6 одна сессия = один процесс = одно ядро. Если у тебя 32 ядра и таблица 100 GiB, SELECT count(*) всё равно поедет одним потоком. С 9.6 это поменялось: появился parallel query — planner может добавить в план узлы Gather/Gather Merge, которые порождают worker-процессы, и работа делится между ними.

В этом уроке — как это устроено внутри, когда полезно, и почему в OLTP его обычно отключают.

Структура параллельного плана

Идея проста: главный (leader) процесс получает запрос, планирует, видит, что параллелизм окупится, и запускает N worker-процессов. Каждый делает кусок работы — например, читает свою долю страниц таблицы. Затем результаты «склеиваются» через узел Gather.

Параллельный план: leader + workers

Узел Gather на корне получает потоки из workers. Каждый worker — отдельный процесс, со своим memory context. Координацию делает Dynamic Shared Memory.

Aggregate (sum)на leader
Gather Merge / Gatherсобирает потоки от workers
Parallel Seq Scanworker #0
блоки 0..N/4
Parallel Seq Scanworker #1
блоки N/4..N/2
Parallel Seq Scanworker #2
блоки N/2..3N/4
Parallel Seq Scanleader (3N/4..N)
ещё и Gather
DSM (Dynamic Shared Memory)как они общаются: parallel context, query plan, partial aggregates

Ключевые компоненты:

  • Leader — изначальный процесс. Запускает workers, координирует, собирает результат, иногда сам участвует в работе.
  • Worker — отдельный backend-процесс, форкнутый специально для запроса. Имеет свою private memory, но shared buffers общий с leader.
  • DSM (Dynamic Shared Memory) — механизм связи: leader пишет туда план и параметры, workers читают, пишут
    partial aggregates
    , leader читает.
  • 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.

PostgreSQL

Когда 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 секунд.

PostgreSQL

Принудительный параллелизм: снижаем parallel_setup_cost и тривиализируем минимальный размер. В реальной БД с 8 ядрами увидишь Gather + 4 workers в плане.

PostgreSQL

В реальной системе оптимальная настройка зависит от природы 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 склеивает.

Проверка знанийKnowledge check
В продакшене ты заметил, что один аналитический запрос на 16-ядерной машине использует только 3 ядра (видно через top). EXPLAIN показывает «Workers Planned: 2, Workers Launched: 2». Что проверить, чтобы поднять до 8?
ОтветAnswer
Сначала проверь max_parallel_workers_per_gather (вероятно 2 — стандартный default). Подними до 8 (SET или ALTER ROLE). Затем — max_parallel_workers (default 8) и max_worker_processes (default 8): если у тебя несколько параллельных запросов одновременно, потолок может быть исчерпан другими сессиями. Подними оба до 16. Если "Workers Launched" всё равно меньше Planned — значит, при выполнении не хватило слотов в max_parallel_workers. Также проверь, что parallel_setup_cost не слишком высокий: на запросе с разумной cost он может урезать число workers'ов сам. Наконец: убедись что нет ни одной non-parallel-safe функции в запросе — она блокирует параллелизм целиком.

Чек-лист

  • 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.
Поток vs процесс: что общего, что разного Векторизованное выполнение в ClickHouse

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Какой узел в плане отвечает за сбор результатов от parallel workers'ов, **сохраняя их упорядоченность** (например, для ORDER BY)?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5