Параллелизм В PostgreSQL CTAS С Пользовательским Агрегатом: Решение Проблем
Всем привет! Сегодня мы поговорим о любопытном поведении PostgreSQL, которое я обнаружил при работе с параллелизмом, CTAS (CREATE TABLE AS SELECT) и пользовательскими агрегатами. В частности, мы рассмотрим ситуацию, когда созданный пользовательский агрегат, помеченный как parallel = restricted
, неожиданно приводит к параллельному плану выполнения запроса. Это может показаться странным, ведь мы явно указали, что агрегат не должен выполняться параллельно. Давайте разберемся, что происходит и как с этим бороться.
Создание пользовательского агрегата
Первым делом, давайте создадим пользовательский агрегат. Для примера возьмем простой агрегат, который суммирует значения. Мы пометим его как parallel = restricted
, чтобы запретить параллельное выполнение. Это важный момент, на котором мы сфокусируемся.
CREATE AGGREGATE my_sum (numeric) (
SFUNC = numeric_add,
STYPE = numeric,
INITCOND = 0,
PARALLEL = RESTRICTED
);
В этом коде мы создаем агрегат my_sum
, который принимает числовой аргумент. Функция numeric_add
используется для сложения значений, numeric
- это тип данных для хранения промежуточного результата, INITCOND
- начальное значение, и PARALLEL = RESTRICTED
указывает на ограничение параллельного выполнения. Очень важно понимать, что именно этот параметр должен предотвратить распараллеливание агрегата.
Принудительный выбор параллельного плана
Теперь, чтобы воспроизвести проблему, нам нужно заставить PostgreSQL выбрать параллельный план выполнения запроса. Это можно сделать, увеличив значение parallel_setup_cost
и parallel_tuple_cost
, чтобы оптимизатор решил, что параллельное выполнение будет дешевле. Мы также установим min_parallel_table_scan_size
и min_parallel_index_scan_size
в 0, чтобы разрешить параллельное сканирование даже для небольших таблиц.
SET parallel_setup_cost = 0;
SET parallel_tuple_cost = 0;
SET min_parallel_table_scan_size = 0;
SET min_parallel_index_scan_size = 0;
Эти настройки заставляют PostgreSQL активнее рассматривать параллельные планы, даже если они не являются оптимальными в обычных условиях. Это полезно для тестирования и отладки, но в реальной среде следует использовать эти параметры с осторожностью.
Запрос с CTAS и агрегатом
Теперь давайте напишем запрос с использованием CTAS и нашего пользовательского агрегата. Мы создадим таблицу test_table
с некоторыми данными и выполним агрегацию с помощью my_sum
.
CREATE TABLE test_table AS
SELECT generate_series(1, 100000) AS id,
random() AS value;
CREATE TABLE result_table AS
SELECT sum(id), my_sum(value)
FROM test_table;
Здесь мы создаем таблицу test_table
с 100 000 строк, содержащих идентификатор и случайное значение. Затем мы создаем result_table
, используя CTAS, и вычисляем сумму идентификаторов и агрегированное значение с помощью нашего пользовательского агрегата my_sum
. Вот тут-то и начинается самое интересное: несмотря на PARALLEL = RESTRICTED
, PostgreSQL может выбрать параллельный план.
Анализ плана выполнения
Чтобы убедиться, что запрос выполняется параллельно, мы можем использовать EXPLAIN
.
EXPLAIN (ANALYZE, BUFFERS)
CREATE TABLE result_table AS
SELECT sum(id), my_sum(value)
FROM test_table;
Если в плане выполнения вы видите узлы Gather
или Parallel Aggregate
, это означает, что запрос выполняется параллельно. В моем случае, я обнаружил, что PostgreSQL действительно выбирает параллельный план, несмотря на ограничение в агрегате. Это выглядит как противоречие, и важно понять, почему это происходит.
Почему это происходит?
Причина такого поведения кроется в том, как PostgreSQL обрабатывает параллельные запросы и пользовательские агрегаты. Оптимизатор может решить распараллелить запрос на более высоком уровне, например, при сканировании таблицы или выполнении других операций, предшествующих агрегации. В этом случае, даже если сам агрегат помечен как RESTRICTED
, параллельное выполнение других частей запроса может привести к параллельному плану в целом. Это важный нюанс, который следует учитывать при проектировании запросов и агрегатов.
Решение проблемы
Как же решить эту проблему и заставить PostgreSQL уважать ограничение PARALLEL = RESTRICTED
? Есть несколько подходов:
-
Отключение параллельных запросов: Самый простой способ - временно отключить параллельные запросы на уровне сессии. Это можно сделать, установив
max_parallel_workers_per_gather
в 0.SET max_parallel_workers_per_gather = 0;
Этот способ гарантированно предотвратит параллельное выполнение, но он также отключит параллелизм для всех других запросов в сессии. Это может быть нежелательно, если вы хотите использовать параллелизм в других частях вашего приложения.
-
Использование
PARALLEL = SAFE
: Если ваш агрегат действительно может выполняться параллельно, но требует специальной обработки, вы можете использоватьPARALLEL = SAFE
. В этом случае вам нужно будет предоставить функции объединения (combine function) для агрегата.CREATE AGGREGATE my_safe_sum (numeric) ( SFUNC = numeric_add, STYPE = numeric, INITCOND = 0, PARALLEL = SAFE, COMBINEFUNC = numeric_add );
Этот подход требует дополнительной работы, но позволяет использовать параллелизм там, где это возможно. Функция
COMBINEFUNC
используется для объединения промежуточных результатов, полученных разными параллельными процессами. -
Переписывание запроса: Иногда можно переписать запрос, чтобы избежать параллельного выполнения агрегата. Например, можно выполнить агрегацию в подзапросе, а затем использовать результаты в основном запросе.
CREATE TABLE result_table AS SELECT sum_id, my_sum_value FROM ( SELECT sum(id) AS sum_id, my_sum(value) AS my_sum_value FROM test_table ) AS subquery;
Этот метод может быть более сложным, но иногда он позволяет добиться желаемого результата без отключения параллелизма. Важно понимать, что переписывание запроса может повлиять на производительность, поэтому необходимо тщательно протестировать разные варианты.
Вывод
Параллелизм в PostgreSQL - мощный инструмент, но он может приводить к неожиданному поведению, особенно при работе с пользовательскими агрегатами. Важно понимать, как PostgreSQL обрабатывает параллельные запросы и как ограничения PARALLEL
влияют на выбор плана выполнения. Если вы столкнулись с подобной проблемой, попробуйте использовать один из предложенных способов решения, чтобы гарантировать правильное выполнение ваших запросов.
Дополнительные советы и best practices
- Тщательно тестируйте свои агрегаты: Перед использованием пользовательских агрегатов в production, убедитесь, что они работают правильно в различных условиях, включая параллельное выполнение. Используйте
EXPLAIN
для анализа планов выполнения и выявления потенциальных проблем. - Используйте
PARALLEL = SAFE
с осторожностью: Если вы используетеPARALLEL = SAFE
, убедитесь, что ваша функция объединения работает правильно и не приводит к некорректным результатам. Неправильная функция объединения может привести к серьезным ошибкам в данных. - Мониторьте производительность: После внесения изменений в запросы или настройки параллелизма, обязательно мониторьте производительность системы. Убедитесь, что ваши изменения действительно улучшают производительность, а не ухудшают ее.
- Обращайтесь к документации: Документация PostgreSQL - отличный ресурс для понимания тонкостей работы параллелизма и агрегатов. Не стесняйтесь обращаться к ней за помощью.
Надеюсь, эта статья помогла вам разобраться с параллелизмом в PostgreSQL и пользовательскими агрегатами. Удачи в ваших проектах!