Параллельные запросы и партиционирование

Рейтинг: 70.2% · 15 голосов
Подробный курс по PostgreSQL: SQL по демобазе, типы данных и JSONB, индексы, транзакции и MVCC, VACUUM, EXPLAIN и планировщик, роли и привилегии, репликация, бэкап, настройка под нагрузку и под 1С. Актуально на 2026.
Ответить
Аватара пользователя
Egor_DBA
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Параллельные запросы и партиционирование

Сообщение Egor_DBA »

Оглавление курса (47)
  1. Что такое PostgreSQL: история, философия, где применяется
  2. Урок 1. Что нового в PostgreSQL 15 и далее (15 -> 16 -> 17)
  3. Установка и первый запуск: Linux, Windows, Docker
  4. psql детально: метакоманды и работа в консоли
  5. Демобаза Авиаперевозки Postgres Pro: структура и развёртывание
  6. SELECT: проекция, фильтрация, сортировка
  7. Транзакции и ACID
  8. Уровни изоляции транзакций: Read Committed, Repeatable Read, Serializable
  9. Соединения таблиц (JOIN)
  10. Агрегация: GROUP BY, HAVING, GROUPING SETS
  11. Подзапросы и CTE (WITH), рекурсия
  12. Оконные функции
  13. Изменение данных: INSERT/UPDATE/DELETE, UPSERT, MERGE
  14. Числовые и булевы типы, NULL и трёхзначная логика
  15. Строки, текст, дата и время, таймзоны
  16. Массивы, диапазоны, enum, составные типы
  17. JSON и JSONB: операторы и индексация
  18. DDL: CREATE TABLE, схемы, ALTER
  19. Ограничения целостности
  20. Представления и материализованные представления
  21. Последовательности, IDENTITY, генерируемые столбцы
  22. Индексы: B-tree и когда индекс не используется
  23. Типы индексов: Hash, GiST, SP-GiST, GIN, BRIN
  24. Продвинутые индексы: частичные, по выражению, покрывающие
  25. Обслуживание индексов и таблиц: bloat, REINDEX, CONCURRENTLY
  26. Полнотекстовый поиск
  27. Роли и пользователи
  28. Привилегии: GRANT/REVOKE
  29. Row-Level Security и безопасность данных
  30. Подключение приложения: pg_hba.conf, SSL, пулы соединений
  31. Серверное программирование: функции и процедуры
  32. Триггеры и события
  33. Архитектура PostgreSQL: процессы и память
  34. MVCC: версии строк и видимость
  35. VACUUM, autovacuum, freeze и wraparound
  36. WAL, контрольные точки и долговечность
  37. Блокировки и взаимоблокировки
  38. Планировщик запросов
  39. EXPLAIN и EXPLAIN ANALYZE: чтение планов
  40. Конфигурация сервера: ключевые параметры
  41. Настройка под нагрузку и под 1С
  42. Параллельные запросы и партиционирование (вы здесь)
  43. Резервное копирование и восстановление
  44. Репликация: потоковая и hot standby
  45. Логическая репликация и кластерные решения
  46. Внешние данные, расширения и сертификация
  47. Разбор планов запросов на explain.tensor.ru: глубокое чтение EXPLAIN ANALYZE
Урок 41. Параллельные запросы и партиционирование

Когда таблица в postgresql разрастается до десятков и сотен миллионов строк, два привычных инструмента перестают спасать: один процессор медленно перемалывает сканирование, а одна гигантская таблица одинаково тяжела для любого запроса. В этом уроке разберём, как заставить сервер считать запрос несколькими ядрами сразу (параллельное выполнение) и как физически разрезать большую таблицу на куски (декларативное партиционирование), чтобы планировщик читал только нужное. Это две ортогональные техники масштабирования, и они отлично работают вместе.

Изображение

Как это работает

Параллельное выполнение - это когда ведущий процесс (leader) запускает несколько фоновых рабочих процессов (parallel workers), раздаёт им части работы, а потом собирает результаты. В плане запроса это видно по узлу Gather (или Gather Merge, если нужен сохранённый порядок): ниже него идёт параллельная часть, выше - последовательная сборка. Каждый воркер - это отдельный процесс ОС, поэтому параллелизм у PostgreSQL не бесплатный: на запуск воркеров и пересылку строк через очередь в разделяемой памяти тратится время.

Сколько воркеров дадут запросу, решают три ограничителя. max_worker_processes - общий лимит фоновых процессов на весь кластер. max_parallel_workers - сколько из них вообще можно отдать под параллельные запросы. max_parallel_workers_per_gather - потолок воркеров на один узел Gather. Если свободных слотов в пуле нет, запрос спокойно отработает с меньшим числом воркеров или вовсе последовательно - параллелизм всегда опционален.

Планировщик не включает параллелизм просто так. Таблица должна быть достаточно большой (порог задаёт min_parallel_table_scan_size, по умолчанию 8 МБ) и стоимость должна оправдывать накладные расходы. Параллельными бывают Seq Scan, Index Scan, агрегаты (частичная агрегация в воркерах плюс финальная сборка), хэш-джойны и nested loop. А вот изменения данных и многие конструкции параллелить нельзя.

Партиционирование - совсем про другое. Декларативное секционирование (с PARTITION BY, появилось в PG 10 и доведено до ума к 15) физически делит одну логическую таблицу на много дочерних таблиц-секций по значению ключа. Главный выигрыш - partition pruning: планировщик по условию WHERE понимает, какие секции точно не содержат нужных строк, и вообще их не трогает. Запрос по одному дню из таблицы за пять лет читает одну секцию вместо всей истории.

Есть три стратегии. RANGE - по диапазонам (типично по дате: месяц или год на секцию). LIST - по списку значений (например, регион или статус). HASH - по остатку хэша ключа, чтобы равномерно размазать строки по N секций, когда естественного диапазона нет. Pruning работает и на этапе планирования, и во время выполнения (runtime pruning) - последнее важно, когда значение приходит как параметр или из подзапроса.

SQL и примеры

Посмотрим, дают ли запросу воркеров. Тяжёлый агрегат по таблице перелётов с билетами обычно уходит в параллель:

Код: Выделить всё

EXPLAIN (ANALYZE, VERBOSE)
SELECT count(*), sum(amount)
FROM bookings.ticket_flights;
В плане ищем строку Gather и под ней Partial Aggregate с пометкой Workers Planned/Launched. Это и есть тот самый explain analyze, по которому видно реальное число запущенных воркеров.

Управляем параллелизмом на лету в рамках сессии. Полезно для замеров:

Код: Выделить всё

SET max_parallel_workers_per_gather = 4;
SET min_parallel_table_scan_size = '1MB';

EXPLAIN ANALYZE
SELECT f.flight_no, count(*)
FROM bookings.flights f
JOIN bookings.ticket_flights tf ON tf.flight_id = f.flight_id
GROUP BY f.flight_no;
Теперь декларативное партиционирование. Сделаем учебную секционированную таблицу бронирований по дате, по диапазону:

Код: Выделить всё

CREATE TABLE book_part (
    book_ref  char(6)        NOT NULL,
    book_date timestamptz     NOT NULL,
    total_amount numeric(10,2) NOT NULL
) PARTITION BY RANGE (book_date);

CREATE TABLE book_2017_q1 PARTITION OF book_part
    FOR VALUES FROM ('2017-01-01') TO ('2017-04-01');
CREATE TABLE book_2017_q2 PARTITION OF book_part
    FOR VALUES FROM ('2017-04-01') TO ('2017-07-01');
Заполним из демобазы Авиаперевозки и проверим pruning:

Код: Выделить всё

INSERT INTO book_part
SELECT book_ref, book_date, total_amount FROM bookings.bookings;

EXPLAIN
SELECT sum(total_amount)
FROM book_part
WHERE book_date >= '2017-05-01' AND book_date < '2017-06-01';
В плане должна остаться только секция book_2017_q2 - остальные отсечены. Это и есть partition pruning в действии.

Удобная мелочь PG 11+: секция по умолчанию ловит всё, что не попало в диапазоны, чтобы INSERT не падал:

Код: Выделить всё

CREATE TABLE book_default PARTITION OF book_part DEFAULT;
Частые грабли
  • Ждут параллель, а её нет: таблица меньше порога, или закончились слоты max_parallel_workers, или запрос содержит непараллелизуемую конструкцию. Смотрите Workers Launched, а не Planned - они различаются при нехватке слотов.
  • Считают, что больше воркеров - всегда быстрее. На пересылку строк и сборку в Gather уходит время; для лёгких запросов параллелизм только вредит. Подбирайте per_gather под реальное число ядер и нагрузку.
  • Уникальный ключ или первичный ключ на секционированной таблице обязан включать столбец партиционирования - иначе СУБД не гарантирует уникальность между секциями.
  • Pruning не срабатывает, если в условии WHERE по ключу стоит функция или приведение типа, мешающее планировщику. Сравнивайте ключ напрямую с константой того же типа.
  • Слишком мелкие секции (тысячи штук) раздувают время планирования и память. Гонитесь за разумным числом - десятки-сотни, не тысячи.
  • Забывают про default-секцию: INSERT со значением вне диапазонов падает с ошибкой. И помните - наличие default-секции замедляет добавление новых секций.
  • Глобального индекса по всем секциям нет: индекс на партиционированной таблице создаётся на каждой секции отдельно.
В PG 16 параллельное выполнение расширили (например, параллельный FULL и правый хэш-джойн), а в PG 17 заметно ускорили загрузку метаданных при тысячах секций и улучшили runtime pruning. База остаётся та же, но на больших масштабах свежая версия ощутимо выигрывает.

Мини-лаба
  • Шаг 1. Подключитесь к демобазе: psql -d demo. Замерьте тяжёлый агрегат: EXPLAIN ANALYZE SELECT count(*), sum(amount) FROM bookings.ticket_flights;
  • Шаг 2. Запретите параллель: SET max_parallel_workers_per_gather = 0; повторите запрос. Сравните время и наличие узла Gather.
  • Шаг 3. Верните 4 воркера и снизьте min_parallel_table_scan_size до 1MB. Повторите. Найдите Workers Planned и Workers Launched.
  • Шаг 4. Создайте секционированную по RANGE таблицу book_part с двумя квартальными секциями и default-секцией (код из урока).
  • Шаг 5. Залейте данные из bookings.bookings и выполните EXPLAIN запроса за один месяц. Убедитесь, что читается одна секция.
  • Шаг 6. Добавьте условие с приведением типа (например, book_date::date = '2017-05-15') и сравните план - сохранился ли pruning.
  • Шаг 7. Загляните в bookings.flights: эта таблица уже сделана партиционированной по дате вылета. Выполните \d+ bookings.flights и найдите список секций.
Контрольные вопросы
  • Чем отличаются узлы Gather и Gather Merge в плане и когда выбирается второй?
  • Какие три параметра ограничивают число параллельных воркеров и за что отвечает каждый?
  • Почему параллельное выполнение не всегда ускоряет запрос и от чего это зависит?
  • В чём разница между partition pruning на этапе планирования и runtime pruning?
  • Когда выбрать HASH-партиционирование вместо RANGE или LIST?
  • Почему первичный ключ секционированной таблицы должен включать ключ партиционирования?
👍3 ❤️2 🔥3 😄 🤔1
Аватара пользователя
Krash13
Сообщения: 1
Зарегистрирован: 30 май 2026, 05:03

Re: Параллельные запросы и партиционирование

Сообщение Krash13 »

А почему у меня count(*) по большой таблице идёт без Gather? Поставил per_gather=4, а воркеров ноль. Оказалось min_parallel_table_scan_size большой и таблица в кэше летит и так.
👍 ❤️ 🔥1 😄 🤔2
Аватара пользователя
raspberry13
Сообщения: 1
Зарегистрирован: 14 май 2026, 11:14

Re: Параллельные запросы и партиционирование

Сообщение raspberry13 »

На проде нарезал секции по дням за 3 года - планировщик начал тупить на ровном месте. Согласен с уроком: лучше по месяцам, иначе тысячи секций душат планирование.
👍2 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Настройка под нагрузку и под 1С
Следующая глава →
Резервное копирование и восстановление

Все главы курса «PostgreSQL: от первого запроса до продакшена»

Поделиться темой: ✈ Telegram VK
Похожие запросы: партиционирование таблиц postgresql по дате

Вернуться в «PostgreSQL: от первого запроса до продакшена»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 2 гостя