Параллельные планы PostgreSQL
Почему планировщик отказывается от параллелизма, какие операции должны быть parallel safe и как читать Gather в плане.
PostgreSQLEXPLAINПроизводительностьЛена задумчиво изучала монитор:
— Макс, вот вроде параллелизм должен ускорять запросы. Почему Postgres обычно его не включает, даже если таблица огромная? Я вижу свободные ядра, но запрос упорно ползет в один поток.
Макс кивнул:
— Это классика. Кажется, что Parallel Seq Scan — это кнопка «Турбо», но планировщик считает стоимости планов лучше любого бухгалтера. И у него есть как минимум три причины для отказа от параллелизма:
1. Gather и стоимость передачи строк
Вся мощь воркеров разбивается об один процесс — Gather (Leader), который собирает результаты.
-
Если воркеры жестко фильтруют данные (
WHERE status = 'ERROR') и отдают наверх лишь крупицы — это выгодно. -
Если воркеры читают всё подряд и шлют миллионы строк Лидеру — это тормоз. Лидер захлебнется, а накладные расходы на пересылку данных между процессами (IPC) съедят весь выигрыш.
Представь себе, что воркеры это Экскаваторы, а Gather — Грузовик. Несколько экскаваторов работают слаженно и быстро, но всё, что они добывают, можно увезти только одним грузовиком. Если они грузят в него весь грунт подряд — образуется пробка, машины простаивают, а результат будет почти такой же, как если бы копал один. Но если каждый из них сортирует породу на месте и отправляет в кузов лишь ценный материал — даже этот узкий выезд перестает быть проблемой.
2. Размер имеет значение
По дефолту Postgres не параллелит таблицы меньше 8 MB (min_parallel_table_scan_size). Он считает, что накладные расходы на запуск процессов для такой «мелочи» выше выгоды.
3. Стоп-факторы
Параллелизма не будет, если:
-
Уровень изоляции транзакции
SERIALIZABLE. -
В запросе есть CTE с модификацией данных (
INSERT/UPDATE/DELETE). -
Используются курсоры (
DECLARE ... CURSOR). -
В запросе есть функции, не помеченные как
PARALLEL SAFE.
— А как понять, что функция SAFE? — уточнила Лена.
— Это должен явно указать разработчик через ALTER FUNCTION. Если функция не меняет данные, не дергает сиквенсы (nextval), не лезет во временные таблицы и не имеет побочных эффектов — она может быть PARALLEL SAFE. Но если ошибешься — получишь не ускорение, а трудноуловимые баги.
Лена потерла виски:
— Слишком много «если». А что делать когда я знаю, что таблица тяжелая, знаю, что на сервере полно ресурсов? Но планировщик «стесняется» и запускает один поток, потому что по его формулам это «немного лучше». Можно как-то стукнуть кулаком по столу? Сказать: «Я босс, включай все ядра для этого отчета», но не ломая конфиг всего сервера?
Макс хитро улыбнулся и придвинулся ближе:
— Можно. Есть способ выдать конкретной таблице «VIP-пропуск».
-- Подсказать желаемое число воркеров для параллельного сканирования таблицыALTER TABLE sales_archive SET (parallel_workers = 4);Лена чуть подалась к экрану:
— А когда этот «VIP-пропуск» оправдан?
Макс задумался на секунду:
— Представь старые архивные таблицы. К ним редко обращаются, но когда обращаются — метко. Годовые отчёты, аудиты, исторические сводки. Вот там есть смысл сказать планировщику: «В этот раз — без скромности».
Он постучал по команде на экране:
— Планировщик учтёт это значение при выборе степени параллельного сканирования таблицы. Но команда не заставляет его выбрать параллельный план и не отменяет ограничения max_parallel_workers_per_gather, общего пула воркеров и требований PARALLEL SAFE.
Лена кивнула, но тут же нахмурилась:
— А если кто-нибудь пустит по этой таблице обычные запросы?
— Вот именно, — Макс повернулся к ней. — Запросы, для которых планировщик выберет параллельный scan этой таблицы, могут запросить больше воркеров. При конкурентной нагрузке это способно упереться в CPU и общий лимит процессов. Поэтому параметр нужно проверять планами и нагрузочным тестом, а при необходимости вернуть к исходному значению через ALTER TABLE sales_archive RESET (parallel_workers).
Он снова улыбнулся и откинулся на спинку кресла:
— Настоящая сила не в том, чтобы включить 32 ядра… а в том, чтобы знать, когда их лучше не трогать.