Оптимизация PostgreSQL для OLAP: параллельная агрегация через shared memory

Если вы работаете с PostgreSQL в аналитических сценариях, то наверняка сталкивались с проблемой: запросы с GROUP BY на больших таблицах выполняются медленно, особенно при высокой нагрузке. Инженеры Tantor Labs предложили решение, которое может ускорить такие запросы в 2,5 раза без перехода на колоно

Оптимизация PostgreSQL для OLAP: параллельная агрегация через shared memory

Если вы работаете с PostgreSQL в аналитических сценариях, то наверняка сталкивались с проблемой: запросы с GROUP BY на больших таблицах выполняются медленно, особенно при высокой нагрузке. Инженеры Tantor Labs предложили решение, которое может ускорить такие запросы в 2,5 раза без перехода на колоночные СУБД. Они использовали общую хэш-таблицу в shared memory с механизмом «тикетов», что позволяет параллельным воркерам эффективно агрегировать данные, избегая блокировок. Эксперимент, основанный на статье Xu&Marcus из PVLDB 2025, показал впечатляющие результаты, но есть нюансы, которые стоит учитывать перед внедрением в продакшен.

Как ускорить PostgreSQL в OLAP-сценариях

Традиционный подход к параллельной агрегации в PostgreSQL предполагает, что каждый воркер обрабатывает свою часть данных, строит локальную хэш-таблицу, а затем результаты объединяются. Это хорошо масштабируется для операций чтения, но при группировке возникает узкое место: объединение частичных агрегатов требует дополнительных затрат, особенно когда число групп невелико относительно числа строк. В таких случаях производительность падает, и пользователи вынуждены искать альтернативы — например, переходить на специализированные аналитические СУБД.

Однако команда Tantor Labs решила проверить гипотезу, которая может изменить ситуацию. Вместо локальных хэш-таблиц они предложили использовать одну общую таблицу, размещенную в shared memory. Идея почерпнута из работы Xu&Marcus, где описывается механизм «тикетирования»: он разделяет операции поиска группы и обновления агрегата, что снижает конкуренцию за доступ к данным. Такой подход уже применяется в ClickHouse и Greenplum, но в PostgreSQL считался неэффективным из-за блокировок. Однако с помощью атомарных операций и умного управления слотами удалось обойти это ограничение.

Эксперимент: как проверяли гипотезу

Авторы эксперимента — инженеры Tantor Labs, компании, развивающей сертифицированный ФСТЭК дистрибутив PostgreSQL, — не ограничились теоретическими выкладками. Они собрали прототип на основе стандартной хэш-агрегации, но с ключевым отличием: вместо локальных таблиц использовалась общая хэш-таблица в shared memory. Для теста была выбрана стандартная схема TPC-H с масштабом 10, запрос с группировкой по нескольким колонкам и агрегацией SUM. Замеры проводились на машине с 16 ядрами и 64 ГБ RAM, под управлением PostgreSQL 16 с патчами.

Сравнивались три варианта: стандартный параллельный план (как в ванильном PostgreSQL), план с общей хэш-таблицей без тикетов и план с общей хэш-таблицей с тикетами. Результаты оказались обнадеживающими: при 4 воркерах прирост составил около 30%, при 8 — 80%, а при 16 — 250% (2,5 раза) по сравнению с базовым планом. При этом тикеты давали дополнительный выигрыш примерно в 15–20% при высокой конкурентной нагрузке, но на малом числе воркеров разница была незначительной. Эти цифры подтверждают, что общая хэш-таблица — рабочий способ ускорить агрегацию, но только при достаточном количестве параллельных процессов.

Предыстория и контекст

Проблема параллельной агрегации в PostgreSQL известна давно. Стандартный подход — разделить данные на части, каждый воркер строит свою хэш-таблицу, затем результаты объединяются. Это хорошо масштабируется на чтение, но плохо — на группировку, особенно когда число групп невелико относительно числа строк. Узким местом становится объединение частичных агрегатов.

Идея общей хэш-таблицы не нова: она используется в ClickHouse, Greenplum и других OLAP-системах. Но в PostgreSQL она считалась неэффективной из-за блокировок при доступе к общей памяти. Статья Xu&Marcus предлагает элегантный обход: вместо того чтобы блокировать всю таблицу, используется тикетирование — каждый воркер получает «билет» на поиск группы, а обновление агрегата происходит без блокировок на основе атомарных операций.

Этот подход перекликается с современными тенденциями в разработке СУБД: отказ от глобальных блокировок в пользу оптимистичного управления конкурентностью. В PostgreSQL уже есть похожие механизмы — например, параллельный seq scan и hash join, но для агрегации они пока не применялись.

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

Суть тикетинга в следующем: когда воркер хочет найти группу для очередной строки, он обращается к общей хэш-таблице, но не блокирует её целиком. Вместо этого он получает тикет — номер слота, который резервируется для него. После того как слот найден, воркер обновляет агрегат с помощью атомарных инструкций (CAS — compare-and-swap). Это позволяет нескольким воркерам одновременно работать с разными слотами, не мешая друг другу.

В прототипе Tantor использовали стандартную хэш-функцию из PostgreSQL, но добавили массив тикетов в shared memory. Каждый тикет содержит указатель на слот и состояние (занят/свободен). При вставке новой группы слот резервируется, а при обновлении существующей — используется уже зарезервированный.

Важно отметить, что этот метод требует, чтобы хэш-таблица полностью помещалась в shared memory, иначе придется использовать внешние сортировки, что сведет на нет выигрыш. Для больших таблиц с миллионами групп это может стать ограничением.

Технические подробности и ограничения

Разработчики выделили несколько ключевых моментов, которые влияют на производительность. Во-первых, размер shared memory должен быть достаточным для хранения всей хэш-таблицы. В эксперименте использовалась таблица с 1 миллионом групп, что заняло около 200 МБ — это укладывается в стандартные настройки PostgreSQL (sharedbuffers обычно 25% от RAM).

Во-вторых, тикеты требуют дополнительной памяти: для каждого слота нужно хранить состояние. В прототипе использовали 8 байт на слот, что для миллиона групп — 8 МБ, приемлемо.

В-третьих, важна настройка планировщика. Чтобы PostgreSQL выбрал новый план, пришлось модифицировать стоимость операций в планировщике. В прототипе использовали GUC-переменную, которая включает новый путь выполнения, но для реального внедрения потребуется более глубокая интеграция.

Сравнение с конкурентами: ClickHouse показывает прирост до 5 раз на подобных запросах, но использует колоночное хранение и векторные инструкции. PostgreSQL с общим хэшем не дотягивает до этого, но для транзакционной системы это неплохой результат.

Кого затронет и как

Для разработчиков, работающих с PostgreSQL в аналитических сценариях, это потенциальный способ ускорить запросы с GROUP BY без перехода на отдельную OLAP-систему. Особенно это актуально для компаний, которые используют PostgreSQL как основное хранилище и не хотят плодить лишние компоненты.

Для бизнеса это означает более быстрые отчеты и возможность строить аналитику на тех же данных, что и OLTP. Например, ритейлеры смогут агрегировать продажи по товарам за день без создания отдельного хранилища.

В России и СНГ это особенно интересно, так как Tantor Labs развивает сертифицированный ФСТЭК дистрибутив PostgreSQL, и внедрение таких оптимизаций может быть включено в будущие релизы. Это дает преимущество перед западными форками, которые могут не учитывать специфику локальных требований.

Что будет дальше

Сейчас прототип существует в виде патчей к PostgreSQL 16. Tantor Labs планирует подготовить полноценный патч для включения в следующий мажорный релиз, вероятно, PostgreSQL 18 (2025 год). Однако есть вопросы, которые предстоит решить: как вести себя при нехватке shared memory, как оптимизировать тикеты для разных типов агрегатов (не только SUM, но и AVG, COUNT, а также более сложных).

Также ведутся эксперименты с гибридным подходом: использовать общую хэш-таблицу для небольших групп, а для больших — переключаться на стандартный метод. Это позволит избежать деградации при большом числе групп.

Итог

Эксперимент Tantor Labs показывает, что общая хэш-таблица с тикетами — рабочий способ ускорить параллельную агрегацию в PostgreSQL, дающий до 2,5 раз прироста на 16 ядрах. Это не серебряная пуля, но важный шаг к тому, чтобы PostgreSQL стал более конкурентоспособным в OLAP-сценариях. Следите за развитием проекта: возможно, в ближайших релизах мы увидим эту оптимизацию в стандартной поставке.