PostgreSQL
Параллельные запросы в PostgreSQL
Аналитический запрос по таблице в 200 GB идёт 3 минуты. Один CPU-ядро скучает. Ещё 15 ядер простаивают. PostgreSQL умеет задействовать все - нужно только разрешить.
- **Cloudflare** использует Parallel Seq Scan для hourly aggregation логов (~500 млн строк/час) - 8 workers сокращают время с 4 мин до 40 сек на том же железе
- **Zalando** перевела аналитические отчёты inventory на параллельные запросы: таблица stock_movements (80 GB) теперь агрегируется за 12 сек вместо 70 сек
- **GitLab** для внутренней аналитики (таблица events, 2 млрд строк) настроила max_parallel_workers_per_gather=6 для reporting-роли и отдельно =0 для app-роли - разные профили нагрузки
Как работает параллелизм в PG
PostgreSQL делит тяжёлую работу между несколькими процессами-workers. Один процесс - leader - координирует, остальные - workers - параллельно сканируют куски таблицы или вычисляют агрегаты. Результаты собираются через Gather-узел в план-дереве.
Параллелизм включается автоматически, если размер таблицы превышает `min_parallel_table_scan_size` (8 MB по умолчанию) и planner оценивает достаточно строк. Принудительно проверить: `SET max_parallel_workers_per_gather = 4; EXPLAIN SELECT ...`
Что делает узел Gather в параллельном плане?
Parallel Seq Scan
Parallel Sequential Scan делит heap-файл таблицы на блоки (по 8 KB) и раздаёт их workers динамически. Каждый worker берёт следующую группу блоков через shared memory-очередь. На таблице orders в 50 млн строк Uber сократил время аналитических выборок с 18 с до 4 с, добавив 4 workers.
Workers Planned: 4 означает запрошено 4 дополнительных process + 1 leader = 5 CPU. Workers Launched (в ANALYZE) показывает реально запущенных - если меньше, значит `max_parallel_workers` на сервере ограничивает.
Workers Planned: 3 в EXPLAIN. Что означает Workers Launched: 2 в EXPLAIN ANALYZE?
Параллельные JOIN-ы
PG умеет распараллеливать Hash Join и Merge Join. При Parallel Hash Join каждый worker строит свою часть хэш-таблицы из inner relation, затем все вместе сканируют outer relation. Это работает особенно эффективно для OLAP-запросов с большими join-ами.
Nested Loop Join не поддерживает параллелизм - inner relation сканируется per-row. Если planner выбирает NL join для большого outer, добавьте `enable_nestloop = off` в сессии и проверьте альтернативный план.
При Parallel Hash Join workers строят хэш-таблицу из inner relation. Как они избегают конфликтов при записи?
Parallel Aggregation
Агрегация разбивается на два этапа: каждый worker вычисляет частичный агрегат (Partial Aggregate) над своим куском данных, лидер финально объединяет через Finalize Aggregate. Для count(*) это прямолинейно - суммирование частичных count-ов. Для percentile_cont параллелизм не поддерживается.
Агрегатные функции делятся на combinable (sum, count, min, max, avg - можно partial) и non-combinable (percentile, array_agg с ORDER BY, jsonb_agg). Non-combinable автоматически отключают параллелизм для всего запроса.
Partial Aggregate + Finalize Aggregate - зачем два шага вместо одного?
Настройка parallel workers
Три ключевых параметра управляют параллелизмом. `max_parallel_workers` - сколько worker процессов можно держать суммарно на весь сервер. `max_parallel_workers_per_gather` - максимум на один запрос. `parallel_setup_cost` и `parallel_tuple_cost` управляют порогом, при котором planner предпочитает параллельный план.
На OLTP-сервере с 100+ одновременных коротких запросов параллелизм может навредить: 100 запросов × 4 workers = 400 процессов, контекстные переключения бьют по latency. Для OLAP с 10 тяжёлых запросов - наоборот, ускорение ×3-4.
Больше workers = всегда быстрее
Параллелизм имеет overhead на синхронизацию и IPC. После определённого числа workers прирост останавливается из-за Amdahl's law - последовательная часть (Finalize Aggregate, Gather) становится узким местом.
На практике 4-8 workers дают 70-85% ускорения от теоретического максимума. 16 workers дают лишь 90% - линейного масштабирования нет.
Почему для OLTP рекомендуют снижать max_parallel_workers_per_gather до 0-1?
Ключевые идеи
- Gather-узел собирает результаты от workers - всё выше Gather работает в одном процессе
- Parallel Aggregation: каждый worker считает Partial Aggregate, лидер делает Finalize - без конкуренции за общую структуру
- OLTP: workers вредят из-за overhead запуска; OLAP: gains от 2x до 5x на 4-8 workers
Связанные темы
Параллельные запросы взаимодействуют с другими механизмами PG:
- EXPLAIN и план запроса — Parallel-узлы видны в EXPLAIN - Workers Planned и Workers Launched позволяют диагностировать проблемы с параллелизмом
- Индексы и сканирование — Parallel Index Scan и Parallel Bitmap Heap Scan дополняют Parallel Seq Scan для запросов с фильтрами
- Настройка памяти PG — work_mem выделяется на каждый worker отдельно - при 4 workers запрос может использовать 4x work_mem
Вопросы для размышления
- Если запрос содержит ORDER BY без LIMIT, как это влияет на параллелизм - Gather или Gather Merge?
- При parallel_setup_cost = 0 PG будет распараллеливать даже запрос к таблице в 100 строк. Почему это может замедлить, а не ускорить?
- Как убедиться, что увеличение max_parallel_workers не вызвало деградацию OLTP-запросов на том же сервере?