BI Consult Desktop Logo BI Consult Mobile Logo
  • Russian BI Исследование российских bi
  • Перейти на Fine BI
  • Контакты
  • +7 812 334-08-01
    +7 499 608-13-06
  • Отправить сообщение
  • Главная
  • Продукты Эксперт-BI
    • Дистрибуция
    • Розничная торговля
    • Производство
    • Операторы связи
    • Страхование
    • Банки
    • Лизинг
    • Логистика
    • Нефтегазовый сектор
    • Медицина
    • Сеть ресторанов
    • E-Commerce
    • Сельское хозяйство
    • Энергетика
    • FMCG
    • Девелоперы
    • Маркетплейсы
    • Пищевая промышленность
    • Фармацевтика
    • Построение Data Platform
    • Цифровая трансформация
    • Управление по KPI
    • Финансы
    • Продажи
    • Склад
    • HR
    • Маркетинг
    • Внутренний аудит
    • Категорийный менеджмент
    • S&OP и FP&A
    • Геоаналитика
    • Цепочки поставок (SCM)
    • AutoML
    • Process Mining
    • IBP
    • ИТ (CIO)
    • Закупки
  • Платформы
    • Системы бизнес-анализа (BI)
    • Интегрированное бизнес-планирование (IBP)
    • Хранилища данных (DWH / Lakehouse)
    • Каталоги данных (Data Catalog)
    • Системы ETL и ELT
    • AI / Исскуственный интеллект
    • Шина данных (ESB)
    • Система управления мастер-данными (MDM)
    • Семантический слой
  • Услуги
    • Переход на отечественные BI и DWH системы
    • Консалтинг
    • Пилотный проект
    • Обучение и сертификация
    • Бесплатное обучение
    • Поддержка
    • Технические задания
    • Сбор требований для проекта внедрения BI-системы
    • CI/CD для DWH
    • Аудит BI приложений и DWH
    • Выделенная команда
    • Настойка и поддержка баз данных
    • Разработка BI Стратегии
    • Styleguide для BI-системы
    • Как выбрать BI-систему
  • Курсы
    • Учебный курс Информационная грамотность (Data Literacy)
    • Учебный курс для бизнес-аналитиков
    • Учебный курс для системных аналитиков
    • Учебный курс по Data Governance
    • Учебный курс Как стать CDO
    • Учебный курс Современная архитектура хранилища данных
    • Учебный курс по Fine BI
    • Учебный курс по FineReport
    • Учебный курс по DWH
    • Учебный курс по Data Science (ML, AI)
    • Учебный курс по PostgreSQL
    • Учебный курс по Greenplum
    • Учебный курс по Apache Airflow и NiFi
    • Учебный курс по Open-source BI
    • Учебный курс по ClickHouse
    • Учебный курс по DataLens
    • Учебный курс по Loginom
    • Учебный курс по Modus BI и ETL
    • Учебный курс по Visiology
    • Учебный курс по dbt (Data Build Tool)
  • Компания
    • Руководство
    • Новости
    • Клиенты
    • Карьера
    • Скачать
    • Контакты

BI

  • FineBI
  • FineReport
  • FineDataLink
  • FineChatBI (FineAI)
  • Коннекторы данных из 1С в BI
  • Airflow / Nifi
  • Visiology
  • PIX BI
  • Modus BI
  • Yandex.DataLens
  • Open-source BI: Superset/Metabase
  • Luxms BI
  • AW BI + Alpha BI
  • FlyBI + Форсайт. Аналитическая Платформа
  • Loginom
  • Триафлай
  • AI / Исскуственный интеллект
  • Optimacros
  • Навигатор BI
  • Семантический слой

СУБД

  • Arenadata
  • ClickHouse
  • Greenplum
  • Postgres Professional
  • TData

Другое

  • Построение Data Platform
    • Аналитическое хранилище данных
    • Data Lake и Data Engineering
    • Подробнее про Data Lake
    • Внедрение Lakehouse
      • Apache Doris
      • StarRocks
      • Trino
    • Миграция витрин из пропиетарных DWH на новый стек
    • Учебный курс "Современная архитектура хранилища данных"
Главная » Курсы по системам бизнес-анализа и методологии » Учебный курс по Greenplum » Greenplum FAQ: оптимизация SQL, spill, skew и ETL

Greenplum FAQ: оптимизация SQL, spill, skew и ETL

Greenplum — это MPP-СУБД для аналитических хранилищ и больших объемов данных. Она дает высокую производительность за счет параллельной обработки на сегментах, но требует аккуратного проектирования SQL-запросов, распределения данных, статистики и ETL-процессов.

В этом FAQ собраны практические кейсы по Greenplum: spill из-за тяжелых подзапросов, оптимизация GROUP BY, статистика в PL/pgSQL, риски рекурсивных запросов, non-equi join, перекос данных, измерение размера таблиц, sequence в ETL и удаление дублей через ctid и gp_segment_id. Страница полезна разработчикам хранилищ, data engineers, DBA и аналитикам, которые работают с Greenplum в продакшене.

Что внутри:

  • что такое spill в Greenplum;
  • почему пустая таблица может привести к тяжелому плану запроса;
  • как избегать OOM и massive spill;
  • оптимизация GROUP BY через CTE;
  • роль статистики в PL/pgSQL;
  • рекурсивные запросы в shared-nothing архитектуре;
  • опасность non-equi join и Nested Loop;
  • Left Join, перекос данных и union all;
  • корректное измерение размера таблиц;
  • sequence и кэширование в ETL;
  • удаление дублей через ctid и gp_segment_id;
  • принципы проектирования запросов под MPP-архитектуру.

 

 

  1. Проблема пустой таблицы и спиллов: как не попасть в ловушку
  2. Оптимизация GROUP BY: когда CTE помогает
  3. Статистика — твой лучший друг в PL/pgSQL
  4. Рекурсивные запросы и shared-nothing: как избежать катастрофы
  5. Опасность non-equi join: как Nested Loop убивает производительность
  6. Left Join и перекос (skew): спасаем запрос через union all
  7. Правильное измерение размера таблиц: cross join спасает
  8. Сиквенсы в ETL: кэш или смерть
  9. Последовательности в Greenplum: как некэшированный sequence может убить хранилище
  10. Удаление дублей: GP-style через ctid + gp_segment_id
  11. Философия Greenplum и Rubik’s cube: проектируй как собери кубик

 

Проблема пустой таблицы и спиллов в Greenplum: как не попасть в ловушку

Greenplum — это MPP-СУБД (Massively Parallel Processing), где запросы распараллеливаются и исполняются на множестве сегментов. Такая архитектура даёт огромные преимущества в обработке больших объёмов данных, но требует особого внимания к проектированию SQL-запросов. Одна из неочевидных проблем, с которой сталкиваются даже опытные разработчики, — это «ловушка пустой таблицы», приводящая к спиллам (spill).

 

Что такое spill?

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

В Greenplum спилл может возникнуть не только при агрегациях или сортировках, но даже при, казалось бы, безобидных операциях фильтрации. Особенно — при использовании подзапросов с тяжелыми таблицами.

 

Кейс: пустая таблица и тяжелый подзапрос

Рассмотрим следующую ситуацию:

  • У нас есть таблица foo, в которой содержатся данные, например, о заказах.
  • Есть таблица big_tbl, содержащая десятки или сотни миллионов строк, например, справочник всех клиентов.

 

Допустим, мы хотим выбрать из foo только те записи, у которых key отсутствует в big_tbl.pk.

 

Пример проблемного кода

SELECT t.a, t.b, t.c
FROM foo t
WHERE t.key NOT IN (SELECT pk FROM big_tbl)

 

На первый взгляд — всё логично. Но если:

  • foo пуста (или содержит мало строк),
  • а big_tbl огромна,

 

то запрос может зависнуть, вызвать massive spill или OOM (out-of-memory error).

 

Почему это происходит?

В Greenplum (и PostgreSQL) выражение NOT IN (SELECT ...) компилируется как анти-семиджойн (anti-semi join). При этом:

  • Подзапрос не может быть просто закэширован: система должна рассматривать его на каждый вызов.
  • Если big_tbl очень большая, а у foo нет статистики или она пуста, планировщик может выбрать неоптимальный план — например, скан всей big_tbl на каждом сегменте.
  • Формально запрос должен проверить, что t.key ≠ любого значения из подзапроса — а это дорогая операция.

 

Как избежать спилла: правильный подход

Решение: материализуем подзапрос через CTE с INTERSECT

WITH a AS (
    SELECT pk FROM big_tbl
    INTERSECT
    SELECT key FROM foo
)
SELECT t.a, t.b, t.c
FROM foo t
WHERE t.key NOT IN (SELECT pk FROM a)

 

Почему это работает:

  • INTERSECT позволяет сразу отбросить ненужные строки, ограничивая объем сравниваемых значений.
  • CTE (WITH a AS (...)) материализует результат, и оптимизатор может построить Hash Join, а не вложенный цикл.
  • Это избавляет от лишней передачи данных между сегментами и снижает риск спилла.

 

Пояснение терминов

Термин

Объяснение

CTE (Common Table Expression)

Временная именованная подтаблица, создаваемая с помощью WITH. Используется для упрощения и оптимизации запросов.

Spill

Ситуация, когда Greenplum выгружает данные на диск из-за нехватки оперативной памяти.

Hash Join

Метод соединения таблиц, основанный на предварительном хэшировании одной из таблиц. Эффективен при равенстве ключей.

Anti-Semi Join

Тип соединения, при котором выбираются только строки из первой таблицы, которые не имеют соответствий во второй. Используется для NOT IN.

GPORCA

Оптимизатор запросов Greenplum, который строит более эффективные планы выполнения по сравнению с Legacy Planner.

 

Практический пример: поведение на проде

На продакшене одна из ETL-функций использовала такую конструкцию:

SELECT *
FROM deals
WHERE deal_id NOT IN (SELECT deal_id FROM blacklist)

 

Когда blacklist стал весить 150 млн строк, а deals — временно был пуст (на ночь), функция неожиданно зависла. Проблема решилась после:

  1. Замены NOT IN на NOT EXISTS — более устойчивое к NULL.
  2. Материализации blacklist в отдельную временную таблицу с индексом.
  3. Добавления ANALYZE после загрузки blacklist.

 

Рекомендации

  1. Не используйте NOT IN (SELECT ...) с большими таблицами — заменяйте на NOT EXISTS или CTE.
  2. Анализируйте таблицы с ANALYZE после загрузки или изменения — иначе Greenplum не сможет построить эффективный план.
  3. Проверяйте статистику пустых таблиц — их отсутствие влияет на выбор плана.
  4. В больших подзапросах используйте INTERSECT, JOIN, LEFT JOIN ... IS NULL и другие техники с материализацией.
  5. Всегда профилируйте тяжелые запросы через EXPLAIN (ANALYZE, VERBOSE) — даже если они «должны быть лёгкими».

 

Пустые таблицы — не всегда легковесные. В условиях MPP Greenplum такие конструкции могут повлечь серьёзные проблемы с производительностью. Лучше изначально проектировать запросы с учётом поведения планировщика и особенностей распределённой архитектуры. И помнить: в MPP важен не только объём данных, но и то, как они передаются и сравниваются.

 

Как ускорить GROUP BY в Greenplum: разбивка агрегатов через CTE

В аналитических системах на базе Greenplum одной из самых ресурсоёмких операций является GROUP BY с большим числом агрегатов. Когда вам нужно сгруппировать данные по 2–3 измерениям и одновременно посчитать десятки метрик — запрос может «зависнуть», «пролить» в диск (spill) или привести к Out Of Memory.

В этой статье мы разберем:

  • Почему классический GROUP BY с множеством агрегатов неэффективен;
  • Как CTE (Common Table Expression) позволяет разгрузить систему;
  • Как устроен план исполнения;
  • И приведем практические советы, как избежать спиллов.

 

Проблема: GROUP BY с десятками агрегаций

Допустим, у нас есть таблица sales, и мы хотим сгруппировать её по трем измерениям (region, channel, product_type) и посчитать 50 метрик:

SELECT region, channel, product_type,
       SUM(m1), SUM(m2), ..., SUM(m50)
FROM sales
GROUP BY 1, 2, 3;

 

Почему такой запрос может "упасть":

  • Greenplum должен удерживать все агрегаты в памяти одновременно на каждом сегменте;
  • Под агрегаты строятся временные хэш-таблицы, и при нехватке памяти — система сбрасывает их на диск (spill);
  • Чем больше метрик, тем больше строк агрегированного состояния.

 

Термины, которые нужно знать

Термин

Объяснение

GROUP BY

SQL-конструкция для группировки строк по значениям одного или нескольких столбцов.

Агрегатные функции

Функции, которые сводят множество значений в одно: SUM, AVG, COUNT, MAX, MIN и др.

CTE (Common Table Expression)

Конструкция WITH ... AS (...), позволяющая выделить подзапрос как временную таблицу. Часто используется для разбиения логики и материализации промежуточных результатов.

Spill

Ситуация, при которой данные промежуточных операций выгружаются на диск, т.к. не помещаются в оперативную память.

Hash Aggregate

Тип агрегации, при котором строки группируются с использованием хэш-таблиц. Очень эффективно до определенного объема.

Sort Aggregate

Альтернативный тип агрегации — сортировка с последующим слиянием групп. Используется при нехватке памяти.

​

Решение: разбиваем агрегаты на подмножества

Вместо одного запроса с 50 агрегациями — делим их на 2 подзапроса с 25 метриками каждый и объединяем результат.

WITH
a AS (
  SELECT region, channel, product_type,
         SUM(m1) AS m1, SUM(m2) AS m2, ..., SUM(m25) AS m25
  FROM sales
  GROUP BY 1, 2, 3
),
b AS (
  SELECT region, channel, product_type,
         SUM(m26) AS m26, SUM(m27) AS m27, ..., SUM(m50) AS m50
  FROM sales
  GROUP BY 1, 2, 3
)
SELECT a.*, b.m26, b.m27, ..., b.m50
FROM a
JOIN b ON a.region = b.region AND a.channel = b.channel AND a.product_type = b.product_type;

 

Почему это работает

  • В каждом подзапросе в памяти находится в 2 раза меньше агрегатов.
  • Это снижает размер хэш-таблиц и уменьшает риск spill.
  • При объединении используется JOIN по группировочным ключам, который может быть эффективно реализован в MPP.
  • Если сегменты используют одинаковые ключи распределения — джойн происходит локально, без движения данных между сегментами.

 

Практический кейс: ускорение отчета

На одном из проектов клиент использовал такой запрос (упрощённо):

SELECT org_id, month,
       SUM(revenue), SUM(cost), SUM(discount), ..., SUM(var50)
FROM fct_sales
GROUP BY 1, 2;

 

В таблице — 2 миллиарда строк, 60 агрегаций. Запрос исполнялся 23 минуты.

После применения CTE с 3 подзапросами (SUM(var1..var20), SUM(var21..var40), SUM(var41..var60)) и JOIN — время снизилось до 4 минут.

 

Как понять, что у вас проблема?

  1. EXPLAIN ANALYZE показывает Spill File: true и Disk I/O.
  2. План запроса использует SortAggregate вместо HashAggregate — признак нехватки памяти.
  3. Видите в логах: Statement cancelled due to insufficient memory to run query with hash aggregation.

 

Технические детали (под капотом)

Как работает GROUP BY в Greenplum

  • Вначале сегмент собирает строки и строит хэш по ключам.
  • Для каждой группы создается слот агрегации (например, для SUM(m1) — число).
  • Чем больше агрегатов — тем больше памяти нужно на каждый слот.

 

Что происходит при спилле

  • Если памяти не хватает — временные данные сбрасываются на диск (spill).
  • Данные читаются обратно в последующих этапах.
  • Это замедляет выполнение в 10–100 раз.

 

Рекомендации

Советы

Комментарии

Разбивайте запрос с множеством агрегатов на несколько CTE

Это главный способ избежать спилла

Следите за work_mem и statement_mem

Можно увеличить их в пределах разумного

Группируйте агрегаты логически

Например, финансовые отдельно от маркетинговых

Используйте одинаковые ключи распределения в CTE

Это позволит объединить результаты без передачи данных

Проверяйте план через EXPLAIN ANALYZE

Следите за Spill, Motion, HashAggregate

 

Запросы с большим числом агрегатов — одна из основных причин спиллов и деградации производительности в Greenplum. Простая техника с использованием CTE и делением агрегаций позволяет добиться многократного ускорения без изменения логики запроса. В MPP-системах это особенно важно, т.к. цена «ошибки» в памяти на одном сегменте — это время всей кластерной операции.

 

Сбор статистики в Greenplum: почему gp_autostats_mode_in_functions может спасти ваш ETL

Одной из частых причин спиллов, зависаний и непредсказуемого поведения запросов в Greenplum является отсутствие или устаревание статистики по таблицам. Это особенно критично при использовании PL/pgSQL-функций, часто встречающихся в ETL-пайплайнах. В данной статье мы разберем:

  • Как работает сбор статистики в Greenplum;
  • Почему функции ведут себя иначе;
  • Что делает параметр gp_autostats_mode_in_functions;
  • И как избежать тяжелых ошибок на проде.

 

 

Термины и определения

Термин

Объяснение

Статистика (statistics)

Метаинформация о таблице: число строк, количество уникальных значений в столбце, селективность фильтров и т.д.

ANALYZE

Команда PostgreSQL/Greenplum, которая собирает статистику по таблице.

PL/pgSQL

Язык процедурного программирования, встроенный в PostgreSQL и Greenplum, позволяющий писать функции.

GPORCA

Новый (по сравнению с Legacy Planner) оптимизатор запросов Greenplum, использующий правила и cost-based подход для выбора плана запроса.

Spill

Сброс промежуточных данных из оперативной памяти на диск из-за нехватки ресурсов.

GUC (Grand Unified Configuration)

Система настройки параметров Greenplum (и PostgreSQL), как глобально, так и на уровне сеанса или функции.

 

Проблема: функции и "слепой" оптимизатор

Рассмотрим следующую ситуацию:

  • У вас есть ETL-функция на PL/pgSQL, которая обновляет таблицу deals_tmp.
  • После выполнения функции вы запускаете запрос с фильтрацией по этой таблице.
  • Запрос зависает, уходит в спилл, возвращает неверные результаты или выдает плохой план.

 

Почему?

Потому что функция выполнила INSERT/UPDATE, но статистика по таблице не обновилась.

 

Что делает gp_autostats_mode_in_functions

Greenplum умеет автоматически запускать ANALYZE после INSERT/UPDATE, но только если разрешено настройками. Одна из таких настроек — gp_autostats_mode_in_functions, которая определяет, будет ли собираться статистика внутри PL/pgSQL-функций.

 

Возможные значения:

Значение

Что означает

none

Не собирать статистику вообще (по умолчанию — опасно!)

on_change

Собирать, если изменено достаточно строк

on_no_stats

Собирать, если статистика отсутствует (надежный вариант)

 

Рекомендуемое решение

Устанавливайте нужный режим прямо внутри тела функции:

CREATE OR REPLACE FUNCTION public.fn_etl_stage()
RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
  SET gp_autostats_mode_in_functions = 'on_no_stats';
 
  DELETE FROM deals_tmp;
  INSERT INTO deals_tmp
  SELECT * FROM staging.deals_delta;
 
  -- Теперь статистика по deals_tmp будет собрана, если она отсутствует
END;
$$;

 

Важно:

  • Установка переменной вне тела функции не действует — нужно именно внутри.
  • Также можно добавить принудительный ANALYZE при необходимости.

 

Практический кейс: зависание запроса с пустой таблицей

На проекте с объемом 300+ TB была функция, вызывающаяся для формирования витрины:

SELECT * FROM fn_build_report('2024-06-01', '2024-06-30')
WHERE src_cd = 'CUST'
AND deal_id IN (SELECT id FROM blacklist);

 

blacklist была пуста, но без статистики. В результате запрос уходил в nested loop и зависал, несмотря на отсутствие реальных данных. Проблема ушла после:

  1. Добавления SET gp_autostats_mode_in_functions = 'on_no_stats' в тело fn_build_report.
  2. Принудительного ANALYZE по blacklist.

 

Как понять, что статистики нет?

Выполните:

SELECT relname, last_analyze
FROM pg_stat_user_tables
WHERE relname = 'название_таблицы';

 

Если last_analyze — NULL, статистики нет. Рекомендуется периодический автосбор через autovacuum или ETL-скрипт.

 

Технические детали

  • В отличие от обычного SQL-запроса, PL/pgSQL-функции — «черный ящик» для планировщика.
  • План запроса может быть сгенерирован один раз и использоваться повторно, даже если данные внутри таблиц сильно изменились.
  • gp_autostats_mode_in_functions нужен, чтобы вынудить пересбор статистики там, где иначе она не запускается.

 

Рекомендации

Что делать

Зачем

Всегда указывайте SET gp_autostats_mode_in_functions = 'on_no_stats' внутри функций, меняющих таблицы

Чтобы статистика собиралась автоматически

Анализируйте таблицы вручную, если ETL не обновляет статистику

Без этого оптимизатор будет "слепым"

Проверяйте pg_stat_user_tables перед прод-запусками

Убедитесь, что статистика актуальна

Избегайте функций с большим числом join-ов и подзапросов без GPORCA

Они плохо оптимизируются

Настройте автоматический ANALYZE по расписанию через cron или Airflow

Особенно для staging-таблиц

 

Greenplum — мощная, но требовательная MPP-система. Отсутствие статистики — одна из главных причин деградации производительности и неэффективных планов запросов. Особенно опасно это в функциях на PL/pgSQL, где по умолчанию статистика не собирается. Использование gp_autostats_mode_in_functions = 'on_no_stats' — простой и эффективный способ обеспечить корректную работу ETL.

 

Осторожно, рекурсия! Как рекурсивные запросы нарушают архитектуру Greenplum

Greenplum — это MPP (Massively Parallel Processing) СУБД, построенная по принципу shared-nothing. Это означает, что каждый сегмент кластера хранит только свою часть данных и не делится ими напрямую с другими. Такая архитектура прекрасно масштабируется, но накладывает жесткие ограничения на использование определенных SQL-конструкций.

Одним из самых опасных паттернов для производительности в Greenplum является рекурсивный запрос с самосоединением таблицы, особенно если таблица распределена только по одному из используемых в соединении ключей. В этой статье мы разберем:

  • Почему рекурсивный WITH RECURSIVE может «взорвать» кластер;
  • Как работает распределение данных в MPP;
  • Какой выход — создать «зеркальную» таблицу с другим ключом распределения;
  • И какие принципы проектирования помогут избежать фатальных ошибок.

 

Термины и определения

Термин

Объяснение

MPP (Massively Parallel Processing)

Архитектура, в которой данные обрабатываются параллельно на независимых узлах (сегментах).

Shared-nothing

Модель, при которой каждый сегмент имеет собственный диск и память, и не делится ресурсами с другими.

WITH RECURSIVE

SQL-конструкция для рекурсивного CTE. Используется, например, для построения иерархий или цепочек.

Hash-distributed table

Таблица, строки которой распределены между сегментами на основе хэш-функции по ключу.

Spill

Ситуация, при которой данные выгружаются на диск из-за нехватки оперативной памяти.

Motion

Передача данных между сегментами в Greenplum. Это дорогостоящая операция.

 

Проблема: рекурсивный join сам на себя

Допустим, у нас есть таблица agreements, в которой каждая строка — это договор, а поле previous_agreement_id указывает на предыдущую версию договора (если есть). Нужно собрать цепочку всех договоров в ретроспективе.

 

Наивное (и опасное) решение:

WITH RECURSIVE agreement_chain AS (
  SELECT agreement_id, previous_agreement_id, 1 AS level
  FROM agreements
  WHERE is_first = true
 
  UNION ALL
 
  SELECT a.agreement_id, a.previous_agreement_id, c.level + 1
  FROM agreements a
  JOIN agreement_chain c ON c.agreement_id = a.previous_agreement_id
  WHERE c.level < 10
)
SELECT * FROM agreement_chain;

 

Что здесь не так?

  • Таблица agreements распределена по agreement_id.
  • В join'е участвуют оба поля: agreement_id и previous_agreement_id.
  • Но previous_agreement_id не является ключом распределения.
  • Это значит, что Greenplum вынужден пересылать всю таблицу на каждый сегмент, потому что сопоставление не может быть выполнено локально.

 

Как это нарушает shared-nothing

При самосоединении таблицы по полю, отличному от ключа распределения:

  • Вся таблица копируется на каждый сегмент (репликация).
  • Происходит огромное количество Motion-операций.
  • Запрос может уйти в спилл или быть отменен по превышению памяти (VMEM).

 

На больших объёмах это может привести к полному отказу кластера.

 

Решение: зеркальная таблица с альтернативным распределением

Создадим копию таблицы agreements, но распределим её по previous_agreement_id:

CREATE TABLE agreements_mirror
WITH (appendonly=true, orientation=column)
AS
SELECT *
FROM agreements
DISTRIBUTED BY (previous_agreement_id);

 

Теперь используем agreements_mirror в рекурсивной части:

WITH RECURSIVE agreement_chain AS (
  SELECT agreement_id, previous_agreement_id, 1 AS level
  FROM agreements
  WHERE is_first = true
 
  UNION ALL
 
  SELECT a.agreement_id, a.previous_agreement_id, c.level + 1
  FROM agreements_mirror a
  JOIN agreement_chain c ON c.previous_agreement_id = a.agreement_id
  WHERE c.level < 10
)
SELECT * FROM agreement_chain;

 

Почему это работает:

  • В первом SELECT используется оригинальная таблица agreements, распределенная по agreement_id.
  • Во втором — agreements_mirror, распределенная по previous_agreement_id.
  • Теперь join может быть выполнен локально на каждом сегменте.
  • Исключены лишние Motion и спиллы.

 

Практический кейс: миллиард записей и краш

На одном из проектов был реализован запрос, строящий цепочку из соглашений (типичная история для договоров с пролонгацией). Таблица содержала ~800 млн строк. При запуске рекурсивного запроса:

  • Кластер начал спиллить.
  • Операции Motion достигали 2 TB в логах.
  • VMEM исчерпывался на 8 из 16 сегментов.

 

Проблема решена:

  • Создана «зеркальная» таблица, распределенная по второму плечу join-а.
  • Рекурсивная часть использовала правильные источники.
  • Запрос стал выполняться за 11 секунд вместо 24 минут и отказа.

 

Рекомендации

Что делать

Зачем

Не используйте рекурсивные join'ы таблицы самой на себя без альтернативных ключей распределения

Это приведет к Motion и спиллам

Создавайте «зеркала» таблиц для разных вариантов join-ов

Это дешевле, чем спилл или перераспределение данных

Проверьте plan запроса: если видите Broadcast Motion — уже плохо

Значит, одна из таблиц копируется на все сегменты

Если рекурсия обязательна — ограничивайте глубину (level < N)

Это снижает нагрузку

Используйте EXPLAIN ANALYZE перед запуском

Так вы увидите, как именно выполняется join

 

Рекурсивные запросы — мощный инструмент, но в MPP-среде они требуют особого внимания. Greenplum не прощает ошибок в проектировании join-ов и распределения. Использование «зеркальных» таблиц с разными ключами — простая, но эффективная техника, позволяющая сохранить преимущества параллельной архитектуры без потерь производительности.

 

Опасность non-equi join в Greenplum: почему BETWEEN в JOIN — это ловушка

При проектировании аналитических систем на базе Greenplum часто возникает необходимость соединить таблицы по диапазону значений — например, определить к какому периоду относится каждая строка по дате. Самый естественный способ — использовать BETWEEN в условии соединения. Однако в MPP-среде Greenplum такая конструкция может оказаться вредной для производительности, особенно если она реализована без ключей и по большим таблицам.

В этой статье разберем:

  • Почему BETWEEN в JOIN — это non-equi join;
  • Чем он опасен в Greenplum;
  • Как работает планировщик;
  • Как переписать запрос с сохранением логики, но с Hash Join вместо Nested Loop;
  • Практический кейс из продакшена.

 

Термины и определения

Термин

Объяснение

JOIN

Операция соединения двух таблиц по общему условию.

Equi-join

JOIN с условием вида A.key = B.key. Может быть эффективно реализован через Hash Join.

Non-equi join

JOIN, где используется >, <, BETWEEN, <> и другие неравенства. Чаще всего реализуется через Nested Loop Join.

Nested Loop Join (NLJ)

Метод соединения, при котором для каждой строки из первой таблицы перебираются все строки второй таблицы. Очень медленно при большом объеме данных.

Hash Join

Быстрый метод соединения по равенству, при котором в оперативной памяти строится хэш-таблица.

Motion

Передача данных между сегментами в Greenplum. Желательно избегать.

Spill

Ситуация, когда промежуточные данные выгружаются на диск.

 

Проблема: BETWEEN вызывает Nested Loop Join

Допустим, у нас есть:

  • Факт sales, с датой транзакции sales.dt;
  • Измерение dim_periods, содержащее границы периода: date_from, date_to.

 

Наивное решение:

SELECT s.*, p.date_from, p.date_to
FROM sales s
JOIN dim_periods p
  ON s.dt BETWEEN p.date_from AND p.date_to;

 

Почему это плохо:

  • Условие соединения — s.dt BETWEEN p.date_from AND p.date_to — это non-equi join.
  • Greenplum не может использовать Hash Join.
  • Планировщик выбирает Nested Loop Join, что приводит к O(N×M) сравнениям.
  • Даже если dim_periods содержит 100 строк, а sales — 10 миллиардов, будет произведено 10 млрд × 100 = 1 трлн сравнений.

 

Решение: эквивалентный equi-join с предобработкой

Примем во внимание, что:

  • dim_periods — маленькая таблица (календари, периоды и т.п.).
  • sales может иметь много повторяющихся дат.
  • Значит, можно предобработать таблицу sales, выделив уникальные даты.

 

Шаг 1: получаем уникальные даты

WITH distinct_dates AS (
  SELECT DISTINCT dt FROM sales
)

 

Шаг 2: находим соответствующий период для каждой даты

, matched_dates AS (
  SELECT d.dt, p.date_from, p.date_to
  FROM distinct_dates d
  JOIN dim_periods p
    ON d.dt BETWEEN p.date_from AND p.date_to
)

 

Шаг 3: соединяем обратно с фактами по dt (теперь можно использовать Hash Join!)

SELECT s.*, m.date_from, m.date_to
FROM sales s
JOIN matched_dates m ON s.dt = m.dt;

 

 

Почему это работает:

  • В подзапросе matched_dates используется BETWEEN, но крошечное число строк (уникальные даты).
  • Join sales и matched_dates теперь — equi-join (s.dt = m.dt) и будет реализован через Hash Join.
  • Массивные Nested Loop'ы заменяются на локальные, быстрые хэш-агрегации и соединения.
  • Сегменты Greenplum могут обрабатывать данные параллельно без перераспределения.

 

Практический кейс: оптимизация отчета по датам

На продакшене был запрос, соединяющий таблицу с 2 млрд строк транзакций с измерением периодов (dim_quarters) по BETWEEN.

  • Время выполнения: 37 минут, с периодическими OOM.
  • После анализа выяснилось, что JOIN реализуется как Nested Loop.

 

После переписывания с использованием DISTINCT dt + equi-join:

  • Время выполнения сократилось до 42 секунд.
  • Plan показал Hash Join без Motion.

 

Технические детали

Особенность

Что происходит

BETWEEN в JOIN

Трактуется как non-equi join → нет Hash Join

Nested Loop

Устрашающе неэффективен при больших объемах

Hash Join

Возможен только при = между ключами

Вынос BETWEEN на малом множестве

Уменьшает число сравнений и переключает план на Hash Join

 

Рекомендации

Что делать

Почему

Избегайте BETWEEN и других неравенств в JOIN

Они лишают вас преимущества Hash Join

Если не избежать — предварительно выделяйте DISTINCT значений

Это ограничит объем дорогой операции

Работайте с календарными измерениями через mapping-таблицы

Они позволяют прямой equi-join

Анализируйте план: Hash Join — хорошо, Nested Loop — сигнал беды

Используйте EXPLAIN ANALYZE

Используйте индексы на фильтруемых столбцах

Это помогает при BETWEEN в WHERE, но не спасает JOIN

 

BETWEEN в JOIN — это изящный и читаемый способ соединения таблиц по диапазону. Но в Greenplum, как и в любой MPP-СУБД, производительность критически зависит от вида соединения. Если вы не используете равенство (=) в JOIN, вы отказываетесь от Hash Join, и планировщик будет применять медленный Nested Loop.

Решение — трансформация BETWEEN в JOIN через выделение ключа, что позволяет системе эффективно распараллелить работу.

 

Пустая таблица, часть 2: как отсутствие статистики превращает пустоту в проблему

В статье №1 мы уже обсуждали, как пустая таблица может вызывать спилл и зависание запроса в Greenplum. Теперь пришло время рассмотреть ещё более опасную ситуацию: когда в запросе используется пустая таблица, но по ней отсутствует статистика. Такая комбинация способна вызывать серьезные просадки в производительности — особенно, если запрос вызывается через функцию и использует GPORCA не в полной мере.

В этой статье разберем:

  • Почему статистика важна даже для пустых таблиц;
  • Как функции и GPORCA работают с фильтрами;
  • Что делать, если у вас пустая таблица участвует в фильтрации;
  • И почему ANALYZE — это ваш лучший друг даже в “нулевых” таблицах.

 

Термины и определения

Термин

Объяснение

Статистика (statistics)

Метаинформация о таблице: количество строк, селективность по столбцам, распределение значений и др.

Spill

Сброс промежуточных данных на диск из-за нехватки оперативной памяти.

GPORCA

Оптимизатор запросов Greenplum нового поколения. Использует правило- и стоимость-ориентированный подход к построению планов.

Legacy Planner

Старый планировщик PostgreSQL, используется по умолчанию в некоторых случаях.

Функция (Function)

Код на PL/pgSQL или SQL, выполняющий несколько операций в одном вызове.

Filter pushdown

Практика, при которой фильтры применяются как можно раньше в плане выполнения, чтобы сократить объем данных.

 

Проблема: фильтрация по пустой таблице без статистики

Предположим, у вас есть функция, возвращающая набор строк за период:

SELECT deal_id
FROM fn_get_deals(p_from := '2024-09-01', p_to := '2024-09-02')
WHERE deal_type = 'IMOEX'
  AND deal_id IN (SELECT id FROM blacklist);

 

blacklist — таблица, в которой нет ни одной строки.

Кажется, что всё должно быть молниеносно, ведь фильтр по пустой таблице должен вернуть пустое множество. Но на деле:

  • Запрос висит;
  • Нагрузка на кластер резко возрастает;
  • Запрос выполняется в десятки раз дольше, чем без фильтра IN (SELECT ...).

 

Что пошло не так?

Всё дело в отсутствии статистики по пустой таблице blacklist.

 

Что делает GPORCA:

  • Оценивает селективность подзапроса IN (SELECT ...) как неизвестную;
  • Не может исключить подзапрос на стадии оптимизации;
  • Не знает, что blacklist пуст — без статистики он предполагает, что там много строк;
  • Весь результат функции fn_get_deals полностью вычисляется, прежде чем происходит фильтрация;
  • Потенциально использует Nested Loop вместо Hash Join — потому что не видит возможности оптимизации.

 

Решение: соберите статистику даже по пустым таблицам

​ANALYZE blacklist;

 

После этой простой команды:

  • Greenplum узнаёт, что таблица пуста (relpages = 0, reltuples = 0);
  • Оптимизатор может сразу отбросить подзапрос, ещё на этапе планирования;
  • Запрос становится в 1000+ раз быстрее, потому что работает с нулевым множеством.

 

Практический кейс: реальный запрос с функцией

SELECT deal_id
FROM fn_deal_report('2024-07-01', '2024-07-02')
WHERE status = 'ACTIVE'
  AND deal_id IN (SELECT risk_contract_id FROM fraud_contracts); -- таблица fraud_contracts пуста

 

Функция возвращает ~10 млн строк. Таблица fraud_contracts — пустая.

 

До:

  • Время выполнения: 3 минуты 40 секунд
  • План запроса включает: Materialize → Function Scan → Nested Loop → Filter

 

После ANALYZE fraud_contracts:

  • Время выполнения: 28 мс
  • План запроса: Function Scan → Result [без подзапроса]

 

 

GPORCA и Legacy Planner

В Greenplum GPORCA не используется, если:

  • В запросе вызывается SQL-функция, а не inline-функция;
  • Используются временные таблицы без распределения;
  • Присутствуют конструкции, несовместимые с GPORCA (например, некоторые агрегаты, window-функции, CTE).

Если вы используете функции как часть ETL, и фильтруете их результат, убедитесь, что:

  • Вы собрали статистику по всем таблицам, участвующим в фильтре;
  • Вы вынесли фильтрацию за пределы функции, если можно.

 

Рекомендации

Что делать

Зачем

Всегда запускайте ANALYZE после создания или очистки таблиц, даже если они пустые

Это позволит оптимизатору правильно оценить селективность

Используйте EXPLAIN (ANALYZE) — и смотрите, выполняется ли подзапрос на фильтрацию

Если выполняется полностью — это признак проблемы

Не фильтруйте в функции, если можно фильтровать вне

Это даёт больше контроля над планом

Проверяйте, используется ли GPORCA или Legacy Planner

SET optimizer = on; и EXPLAIN покажут это

При наличии подзапроса IN (SELECT ...) — лучше использовать JOIN с предварительным DISTINCT

Это позволяет использовать Hash Join

 

Отсутствие статистики даже в пустых таблицах может привести к многократной деградации производительности в Greenplum. Оптимизатор просто не знает, что таблица пуста, и перестраховывается, строя сложный и избыточный план. Простой ANALYZE решает проблему. Особенно это важно при использовании функций, которые итак имеют ограничения на оптимизацию.

 

Перекос данных и LEFT JOIN в Greenplum: когда 7 миллионов строк сильнее миллиарда

В системах класса MPP, таких как Greenplum, ключевым фактором производительности является равномерное распределение данных по сегментам. Даже идеально написанный SQL-запрос может "провалиться", если одна из таблиц содержит перекошенные ключи, особенно в условиях LEFT JOIN. Это приводит к непредсказуемым планам выполнения, перераспределению данных, перегрузке сегментов и даже к отказу запроса по нехватке памяти (VMEM).

В этой статье разберем:

  • Что такое перекос (data skew) и почему он так опасен;
  • Почему LEFT JOIN чувствителен к NULL-значениям;
  • Как улучшить запрос, не меняя бизнес-логику;
  • Практический кейс, где LEFT JOIN приводил к аварийному завершению запроса.

 

Термины

Термин: Data Skew
Объяснение: Нерегулярное распределение значений по ключу в таблице, из-за чего часть сегментов обрабатывает больше данных, чем остальные.

 

Термин: VMEM
Объяснение: Выделенная память сегмента Greenplum, за превышение которой запрос может быть отменен.

 

Термин: LEFT JOIN
Объяснение: Соединение, возвращающее все строки из левой таблицы и, если есть, соответствующие строки из правой таблицы. При отсутствии совпадений в правой таблице — значения NULL.

 

Термин: Distribution Key
Объяснение: Ключ, по которому Greenplum распределяет строки таблицы по сегментам.

 

 

Проблема: перекос при LEFT JOIN

Допустим, у нас есть две таблицы:

  • small_tbl — содержит 7 миллионов строк. В ней есть внешний ключ addr_fk, по которому происходит соединение.
  • big_tbl — содержит 1 миллиард строк. В ней addr_pk является первичным и ключом распределения.

 

На первый взгляд соединение выглядит безобидно:

Пример проблемного запроса:

select a.*, b.address
from small_tbl a
left join big_tbl b
on a.addr_fk = b.addr_pk

 

Запрос падает с ошибкой:

ERROR: Canceling query because of high VMEM usage.

 

Причина: перекос и NULL

Разбор:

  • В таблице small_tbl около 50 процентов значений в столбце addr_fk — NULL.
  • Это означает, что половина строк small_tbl не соединяется ни с одной строкой из big_tbl.
  • Однако при планировании LEFT JOIN, оптимизатор Greenplum:
    • Не может использовать Hash Join эффективно.
    • Пытается сместить все строки с NULL в один сегмент.
    • В результате один сегмент перегружается, а остальные простаивают.

 

Кроме того, если addr_fk — ключ распределения small_tbl, то NULL не учитывается в хэшировании. Это создает асимметрию: строки с NULL могут оказаться в одном месте, вызывая перекос нагрузки.

 

Решение: разбиение запроса

Подход: разделить LEFT JOIN на два отдельных запроса:

  1. Строки с ненулевым addr_fk обрабатываются через обычный JOIN — безопасный и предсказуемый.
  2. Строки с addr_fk IS NULL добавляются вручную с NULL-значениями справа.

 

Пример корректного запроса:

select a.*, b.address
from small_tbl a
join big_tbl b
on a.addr_fk = b.addr_pk
where a.addr_fk is not null

union all

select a.*, null as address
from small_tbl a
where a.addr_fk is null

 

Почему это работает

  • Greenplum может использовать Hash Join для первой части запроса.
  • Нет необходимости обрабатывать NULL значения внутри большого JOIN.
  • UNION ALL не требует сортировки или удаления дубликатов — работает быстро.
  • Результат полностью эквивалентен исходному LEFT JOIN.

 

Практический кейс

Проект: витрина адресов клиентов
Таблица small_tbl — отфильтрованная выборка из истории заказов
Таблица big_tbl — мастер-справочник адресов, более миллиарда строк

 

До:

  • LEFT JOIN с условиями по адресу
  • 3 из 8 сегментов перегружены
  • Время выполнения — не более 20 минут, но часто ошибка VMEM

 

После:

  • Разделение запроса на 2 части, как показано выше
  • Время выполнения — 1 минута 10 секунд
  • План запроса использует Hash Join и Index Scan
  • Уменьшение VMEM в 6 раз

 

Рекомендации

  • Никогда не делайте LEFT JOIN с ключами, содержащими NULL, если не уверены в равномерном распределении.
  • Проверяйте статистику распределения по ключу:
    select value, count(*) from table group by value order by count desc;
  • Не используйте колонку с NULL в качестве Distribution Key.
  • Разделяйте LEFT JOIN на части, особенно если есть фильтры вида IS NULL.
  • Используйте EXPLAIN ANALYZE, чтобы понять план: если видите Broadcast Motion или HashAggregate с подозрительными объемами — проверьте распределение.
  • Если VMEM превышается, разнесите обработку на несколько запросов или используйте материализацию.

 

В MPP-системах главная угроза производительности — не всегда размер таблицы, а неконтролируемое распределение данных. LEFT JOIN с NULL в ключах может создать катастрофический перекос нагрузки. Простое разбиение запроса позволяет эффективно использовать ресурсы и избежать проблем.

 

Как правильно измерить размер таблицы в Greenplum и почему pg_relation_size может врать

На первый взгляд, измерить размер таблицы в Greenplum просто — вызываешь функцию pg_relation_size и получаешь число в байтах. Но в архитектуре MPP это работает не так, как в классическом PostgreSQL. Если использовать pg_relation_size в неправильном контексте, вы можете получить ошибочные или заниженные значения, особенно при использовании вспомогательных таблиц, кросс-соединений и запросов с фильтрацией.

В этой статье разберем:

  • Как работает pg_relation_size;
  • Почему она может давать разный результат;
  • Как устроены сегменты в Greenplum;
  • Как правильно измерять размер таблицы;
  • Практический кейс и два способа измерения.

 

Термины

Термин: pg_relation_size
Объяснение: Системная функция PostgreSQL и Greenplum, возвращающая размер объекта (таблицы, индекса и т.д.) в байтах. Работает на уровне локального сегмента.

 

Термин: gp_dist_random
Объяснение: Системная функция Greenplum, возвращающая по одной строке с каждого сегмента. Часто используется для операций, которые должны выполняться распределенно.

 

Термин: cross join
Объяснение: Декартово произведение двух таблиц, соединение без условий.

 

Термин: Motion
Объяснение: Операция в Greenplum, при которой данные перемещаются между сегментами. Один из главных факторов снижения производительности.

 

Проблема: разные результаты pg_relation_size

Допустим, мы хотим узнать размер таблицы foo.

Казалось бы, достаточно выполнить:

select pg_relation_size('public.foo');

 

И действительно, результат — например, 3 209 024 байта — будет верен, если запрос выполняется напрямую и явно.

Однако в другом случае, когда имя таблицы хранится в текстовом виде в другой таблице, например:

create table foo_2 as
select 'public.foo'::text as tbl_nm;

 

и мы выполняем:

select pg_relation_size(tbl_nm) from foo_2;

 

результат будет — например, 4 640 байт, что в 700 раз меньше, чем реальный размер.

 

Почему это происходит

Функция pg_relation_size исполняется на сегменте, где находится строка, содержащая имя таблицы.
Но сама таблица foo может физически располагаться на других сегментах.

Таким образом, вы запрашиваете размер таблицы не там, где она фактически хранится. Greenplum не перемещает pg_relation_size между сегментами — она просто возвращает локальное значение. В результате:

  • При прямом вызове вы получаете агрегированный результат;
  • При вызове внутри запроса по другой таблице — частичный.

 

Решение 1: join с pg_class и pg_namespace

Это решение кажется логичным:

select pg_relation_size(c.oid)
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
join foo_2 f on f.tbl_nm = n.nspname || '.' || c.relname;

 

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

 

Решение 2: cross join с gp_dist_random

Рекомендуемый подход:

select sum(pg_relation_size(f.tbl_nm || decode(q.content, q.content, '')))
from foo_2 f
cross join gp_dist_random('gp_id') q;

 

Объяснение:

  • gp_dist_random гарантирует, что каждый сегмент примет участие в вычислении;
  • concat с decode используется как хитрость для принудительной интерпретации строки на каждом сегменте;
  • sum агрегирует результат по всем сегментам.

 

Преимущества:

  • Нет Motion;
  • Корректный размер;
  • Быстро и безопасно.

 

Практический кейс

DBA получил список таблиц, по которым нужно было вычислить объем хранения. Использовал запрос с подстановкой имени таблицы из справочника — результат оказался в 300 раз меньше ожидаемого.

После применения gp_dist_random запрос стал показывать правильный размер и был внедрен в ежедневный мониторинг.

 

Рекомендации

  • Всегда используйте gp_dist_random для распределенного вызова pg_relation_size;
  • Избегайте join с pg_class в проде — используйте только в служебных задачах;
  • Не применяйте pg_relation_size к таблице, имя которой передается как строка без привязки к сегменту;
  • При построении мониторинга размера таблиц обязательно агрегируйте по сегментам;
  • Сравнивайте результаты вручную при отладке — вызов напрямую и через обертки должен давать одинаковый ответ.

 

pg_relation_size — удобная, но потенциально опасная функция в Greenplum. В распределенной архитектуре нельзя полагаться на вызов только с одного сегмента. Использование cross join с gp_dist_random позволяет получить корректный, агрегированный результат по всем сегментам. Это особенно важно при анализе хранения, аудите таблиц и мониторинге использования ресурсов.

 

Последовательности в Greenplum: как некэшированный sequence может убить хранилище

В архитектуре хранилища данных часто требуется генерировать уникальные идентификаторы. Для этого разработчики используют sequence — механизм автонумерации, встроенный в PostgreSQL и Greenplum. Однако в MPP-среде sequence может стать источником проблем, если не использовать кэш. Некэшированные последовательности создают перегрузку на master-узел, блокируют транзакции и при интенсивной записи приводят к гигабайтам или даже терабайтам мертвых данных, которые невозможно эффективно очистить без полного переразмещения таблиц.

В этой статье разберем:

  • Как работает sequence в Greenplum
  • Почему кэш обязателен
  • Что такое bloating таблиц
  • Как sequence может блокировать сегменты
  • Как выявить проблему и исправить ее

 

Термины

Термин: sequence
Объяснение: Счетчик в базе данных, предназначенный для генерации уникальных чисел. Может использоваться через nextval или в качестве default значения столбца.

 

Термин: кэш sequence
Объяснение: Механизм, при котором значения sequence заранее резервируются пачками. Позволяет избежать блокировок и ускорить генерацию.

 

Термин: bloating
Объяснение: Раздутие таблицы — ситуация, когда физический размер таблицы многократно превышает объем логических данных. Возникает из-за частых обновлений, удалений и некорректной работы sequence.

 

Термин: master
Объяснение: Центральный управляющий узел Greenplum. Все команды, включая nextval, проходят через него.

 

Проблема: sequence без кэша на массовой вставке

По умолчанию sequence в PostgreSQL создается без явно заданного кэша:

create sequence seq_orders start with 1 increment by 1;

 

Если таблица записывает по 5000 строк в секунду с использованием nextval(seq_orders), то каждая операция требует обращения к master-узлу. При большом числе одновременных операций master начинает блокировать транзакции.

Пример:

insert into fct_orders (order_id, customer_id, ...)
select nextval('seq_orders'), customer_id, ...
from staging_table;

 

Проблема:

  • Каждая операция nextval блокирует доступ к sequence
  • Задержки достигают десятков миллисекунд на строку
  • Таблица становится точкой отказа
  • Фоновый bloating: каждая запись добавляет новые странички в таблицу, но VACUUM не может их освободить, пока sequence продолжает использоваться

 

Как выглядит bloating

Проверка размера таблицы:

select pg_size_pretty(pg_total_relation_size('fct_orders'));

Результат: 320 GB

 

Количество строк:

select count(*) from fct_orders;

Результат: 8 миллионов строк

 

Средний объем на строку — более 40 килобайт. Очевидный признак bloating.

 

Решение: sequence с кэшем

Создайте sequence с разумным значением кэша:

create sequence seq_orders cache 10000;

Теперь каждый сегмент или транзакция будет резервировать блок значений и обращаться к master только раз на 10000 вставок. Это:

  • Снижает нагрузку на master
  • Убирает блокировки
  • Уменьшает логическую фрагментацию
  • Повышает производительность ETL

 

Дополнительно: кешированные sequence в insert через generate_series

При массовой генерации данных:

insert into fct_orders (order_id, ...)
select nextval('seq_orders'), ...
from generate_series(1, 100000);

 

Если кэш не задан, каждый nextval пройдет через master.

 

Практический кейс

На проекте с объемом 50 миллиардов строк sequence использовался без кэша в staging-загрузках.

Последствия:

  • Таблица выросла до 3 терабайт при реальном объеме данных 70 ГБ
  • VACUUM не помог
  • После анализа выяснено: запись шла с некэшированным nextval, каждый insert блокировал следующий
  • Проблема решена через:
    1. Удаление sequence
    2. Создание новой с cache 10000
    3. Полная пересозданная таблица

 

Как обнаружить проблему

  • Таблицы с автонумерацией неожиданно большие
  • VACUUM и VACUUM FULL не освобождают место
  • EXPLAIN ANALYZE показывает задержки при insert
  • Таблица сильно тормозит при INSERT, даже при маленьком объеме

 

Проверьте параметры:

select * from pg_sequences where schemaname = 'public';

 

Проверьте флаг cache_size. Если он равен 1 — sequence не оптимизирован.

 

Рекомендации

  • Всегда указывайте cache при создании sequence: от 1000 до 100000 в зависимости от нагрузки
  • Не используйте nextval без кэша в массовых загрузках
  • Периодически проверяйте bloating таблиц: pgstattuple, pg_total_relation_size
  • Для часто изменяемых таблиц с sequence — настройте autovacuum с повышенной агрессивностью
  • Вынесите генерацию идентификаторов за пределы insert — через staging и materialized insert

 

Sequence — это не просто счетчик, а критический элемент архитектуры записи. В Greenplum неправильное использование некэшированных sequence может привести к блокировкам, замедлению запросов и гигантским потерям в дисковом пространстве. Простой параметр cache в create sequence позволяет избежать всех этих проблем.

 

Удаление дублей в Greenplum: через ctid и gp_segment_id — быстро, безопасно, без оконных функций

Введение

Удаление дублей — типовая задача для любой системы обработки данных. В классическом SQL это решается через оконные функции row_number() и удаление строк с номером больше одного. Но в Greenplum такой подход часто приводит к Motion, Spill, перераспределению данных и неэффективному плану, особенно если таблица большая.

В этой статье мы разберем GP-style метод — удаление дублей по ctid и gp_segment_id. Это наиболее производительный и безопасный способ удаления дубликатов без оконных функций и сортировок.

 

Термины

Термин: ctid
Объяснение: Системный столбец PostgreSQL и Greenplum, представляющий физическое местоположение строки внутри страницы таблицы. Уникален на уровне сегмента.

 

Термин: gp_segment_id
Объяснение: Системный столбец Greenplum, обозначающий номер сегмента, на котором физически хранится строка. Помогает различать одинаковые строки на разных сегментах.

 

Термин: row_number()
Объяснение: Оконная функция, присваивающая каждой строке уникальный порядковый номер в рамках партиции. Используется для поиска дублей.

 

Термин: Motion
Объяснение: Операция перемещения данных между сегментами. Может резко снизить производительность.

 

Проблема: удаление дублей через оконные функции

Классический способ:

with marked as (
select *,
row_number() over (partition by key1, key2 order by id) as rn
from my_table
)
delete from marked where rn > 1;

 

Проблемы:

  • Требуется сортировка — дорого для больших таблиц.
  • Используется оконная функция — часто вызывает Motion.
  • Сложный план выполнения.
  • Удаление по CTE не всегда поддерживается в Greenplum напрямую.

 

Решение: ctid плюс gp_segment_id

Шаг 1. Выбираем дубль, который оставим

create table deduped as
select min(ctid) as keep_ctid, gp_segment_id, key1, key2
from my_table
group by gp_segment_id, key1, key2;

 

Шаг 2. Удаляем все строки, которые не попали в deduped

delete from my_table
where (gp_segment_id, ctid) not in (
select gp_segment_id, keep_ctid from deduped
);

 

Почему это работает

  • ctid уникален только внутри сегмента, поэтому нужно использовать gp_segment_id.
  • Мы сохраняем только одну строку для каждой уникальной комбинации ключей.
  • Нет оконных функций.
  • Нет Motion.
  • Удаление локально на сегментах.

 

Практический кейс

Таблица: my_table, 900 миллионов строк
Ключи: client_id, contract_id

До:

  • row_number() → spill на 5 сегментах
  • delete по CTE не поддерживается
  • итоговое время выполнения: 47 минут

 

После:

  • через ctid и gp_segment_id
  • материализация deduped: 1 минута 40 секунд
  • delete: 3 минуты 15 секунд
  • итоговое время: 5 минут, без spill

 

 

Проверка результата

После удаления можно проверить на наличие дублей:

select client_id, contract_id, count()
from my_table
group by 1, 2
having count() > 1;

 

Если результат пустой — дублей нет.

 

Альтернатива: insert into with distinct on

Можно использовать insert into с distinct on:

create table deduped as
select distinct on (key1, key2) *
from my_table;

 

Но:

  • distinct on требует order by
  • тоже может вызывать Motion
  • не удаляет строки, а копирует в новую таблицу

 

Метод через ctid и gp_segment_id дает возможность удалить дубли на месте, не создавая копий.

 

Рекомендации

  • Для удаления дублей всегда используйте ctid вместе с gp_segment_id
  • Удаляйте только строки, явно не попавшие в подвыборку min(ctid)
  • Избегайте row_number() и оконных функций при больших объемах данных
  • Проверяйте планы выполнения через explain analyze
  • Удаляйте дубли в staging до загрузки в витрины

 

Greenplum как MPP-СУБД требует особого подхода к операциям с данными. Стандартные SQL-решения, вроде row_number, часто не масштабируются. Использование системных столбцов ctid и gp_segment_id — это простая, быстрая и надежная техника для удаления дубликатов в духе Greenplum.

 

Философия проектирования в Greenplum: как собирать хранилище, как кубик Рубика

​Greenplum — это не просто MPP-база, это целая инженерная платформа, где успех запроса зависит не только от SQL, но от архитектуры. Чтобы получить производительность, масштабируемость и предсказуемость, нужно проектировать модели и запросы так, как будто ты собираешь кубик Рубика: у каждого элемента должно быть свое место, своя ориентация, и правильная последовательность действий.

В этой статье мы рассмотрим:

  • Принцип проектирования данных под Greenplum
  • Почему хаотичная архитектура убивает масштабирование
  • Как упорядоченность приводит к производительности
  • И как проектировать, вдохновляясь логикой кубика Рубика

 

Термины

Термин: кубик Рубика
Объяснение: Головоломка с шестью гранями и 54 клетками, которую можно собрать только при правильной последовательности вращений и ориентации элементов.

 

Термин: shared-nothing
Объяснение: Архитектура, в которой каждый сегмент Greenplum работает независимо, без общего доступа к дискам и памяти.

 

Термин: модель данных
Объяснение: Структура представления бизнес-данных в виде таблиц, связей, ключей и правил распределения.

 

Термин: colocation
Объяснение: Совместное распределение таблиц по одному ключу, чтобы join выполнялся локально без передачи данных между сегментами.

 

Термин: hash distribution
Объяснение: Метод распределения строк таблицы по сегментам на основе хеш-функции от ключа.

 

Главный принцип: проектируй, чтобы все совпадало

В кубике Рубика правильная сборка требует, чтобы:

  • Цвета были на своих гранях
  • Центры соответствовали сторонам
  • Последовательность действий была продумана

 

То же самое в Greenplum:

  • Таблицы должны быть распределены согласованно
  • Все ключи должны быть определены
  • Join должен быть локальным
  • Данные должны быть подготовлены заранее, а не на лету

 

Пример плохого проектирования

  • Таблица заказов распределена по заказу_id
  • Таблица клиентов распределена по клиенту_id
  • Запрос делает join заказов и клиентов по клиенту_id

 

Результат:

  • Не совпадают ключи распределения
  • Выполняется Redistribute Motion
  • Потеря параллельности
  • Увеличение времени выполнения в 5–10 раз

 

Как проектировать правильно: философия Rubik's cube

Шаг 1. Выбери ось модели
Как в кубике всегда есть фиксированные центры — так и в модели данных должна быть сквозная ось (например, клиент или заказ), по которой будет идти основное распределение.

 

Шаг 2. Собери грань — сделай colocation
Если ты собираешь красную грань — все элементы должны быть красные. Так и в Greenplum — если ты собираешь витрину по заказу, все таблицы должны быть распределены по заказу_id.

 

Шаг 3. Не вращай, пока не готов
В кубике неправильный поворот разрушает собранную грань. В Greenplum не делай join без предварительной подготовки данных — сначала создай staging, согласуй ключи, проверь статистику.

 

Шаг 4. Фиксируй правильные позиции
Собрал грань — сохрани ее в материализованной таблице. Не пересчитывай каждый раз. Это сэкономит десятки минут при повторных запросах.

 

Практический кейс

Проект: отчет по сегментам покупателей
Ошибка: таблицы по продажам, клиентам, регионам распределены по разным ключам
Результат: каждый join вызывал Motion, таблица в 300 миллионов строк собиралась за 15 минут
Решение:

  • Все таблицы приведены к распределению по клиенту_id
  • Все join стали локальными
  • Время выполнения — 1 минута 10 секунд
  • План запроса без Motion, чистые Hash Join

 

Практические советы

  1. Никогда не используй разные ключи распределения, если таблицы логически связаны
  2. При проектировании витрины всегда определяй, по какому ключу будет строиться отчет
  3. Используй hash distribution по числовым ID, не по строкам
  4. Если colocation невозможен — сделай предварительное агрегирование в staging
  5. Всегда проверяй план через explain analyze — если видишь Motion, иди и пересобирай кубик

 

Проектирование в Greenplum — это инженерия на грани архитектуры и математики. Как в кубике Рубика, ты не можешь просто вращать в случайном порядке — каждое действие должно быть обоснованным. Только так можно построить масштабируемое и стабильное хранилище, где join не превращается в redistribute, а анализ данных — в ожидание загрузки CPU.

Проектируй, как собираешь кубик: слой за слоем, с пониманием структуры и цели.

 

Сиквенсы в ETL: кэш или смерть

Sequence — это простой и удобный способ генерировать уникальные идентификаторы в БД. В Greenplum они тоже есть, но в условиях параллельной архитектуры они становятся бутылочным горлышком, если используются неправильно. Если ты вызываешь nextval без кэша в массовой ETL-нагрузке — ты тормозишь весь pipeline.

 

Термины

Термин: sequence
Описание: Счетчик, генерирующий последовательные значения. Используется для ID и surrogate key.

 

Термин: nextval
Описание: Функция получения следующего значения из sequence.

 

Термин: cache
Описание: Количество значений, которое sequence резервирует за один вызов. Уменьшает частоту обращений к master-узлу.

 

Термин: bloating
Описание: Раздутие таблицы за счет неэффективных операций записи, когда физический объем становится больше логического.

 

Проблема: sequence без cache

Пример:

create sequence seq_deals;
insert into fct_deals (deal_id, ...)
select nextval('seq_deals'), ... from staging;

 

Если кэш не указан, Greenplum:

  • Делает nextval на master-узле
  • Блокирует доступ к sequence для параллельных операций
  • Создает очередь
  • Снижает throughput записи
  • Увеличивает bloating таблицы

 

Решение: укажи cache

Правильно:

create sequence seq_deals cache 10000;

 

Теперь:

  • Значения nextval выделяются пачками
  • Сегменты работают независимо
  • Запись становится в 10–100 раз быстрее
  • Bloating снижается

 

Практический кейс

ETL записывал 5000 строк в секунду. Таблица раздулась до 1.2 терабайта при логических 50 гигабайтах. Причина — массовые вставки через некэшированный sequence. После смены sequence с cache 10000:

  • запись ускорилась на 8 раз
  • bloating ушел после пересоздания
  • нагрузка на master упала

 

Рекомендации

  1. Всегда используй cache при создании sequence в ETL
  2. Не вызывай nextval в цикле без буферизации
  3. Следи за bloating через pg_total_relation_size
  4. Используй staging и сериализацию идентификаторов отдельно от записи
  5. Не оставляй sequence без контроля — это тихий убийца производительности

 

Sequence в Greenplum — это мощный инструмент, но только если использовать его с кэшем. В условиях параллельной загрузки без этого параметра ты получаешь блокировки, потери в скорости и гигабайты мертвых данных. Настрой cache один раз — и избавь себя от огромных проблем.

 

FAQ

Вопрос: Что такое spill в Greenplum?

Ответ: Spill — это ситуация, когда сегменту не хватает памяти для промежуточных данных, и Greenplum начинает выгружать их на диск. Это может резко замедлить запрос и привести к проблемам с диском.

 

Вопрос: Почему пустая таблица может вызвать тяжелый запрос?

Ответ: Если основная таблица пуста или маленькая, а подзапрос обращается к большой таблице, планировщик может выбрать неудачный план. В Greenplum это особенно опасно из-за параллельного выполнения на сегментах и возможных spill.

 

Вопрос: Чем опасен non-equi join в Greenplum?

Ответ: Non-equi join может привести к Nested Loop и многократному перебору строк, особенно на больших таблицах. В MPP-СУБД это быстро превращается в тяжелый запрос с большим расходом памяти и времени.

 

Вопрос: Что такое skew в Greenplum?

Ответ: Skew — это перекос распределения данных между сегментами. Если часть сегментов получает намного больше строк, запрос выполняется медленнее, потому что один или несколько сегментов становятся узким местом.

 

Вопрос: Почему sequence важны в ETL?

Ответ: Некэшированные sequence могут создавать сильную нагрузку и тормозить ETL-процессы. Для больших загрузок важно правильно настраивать кэширование и понимать, как последовательности работают в распределенной архитектуре.

 

  • ClickHouse FAQ: бэкапы, кластеры и MergeTree
  • ClickHouse для новичков
  • PostgreSQL: WAL и журнал предзаписи
  • PostgreSQL: изоляция транзакций
  • DWH: зачем компании хранилище данных

 

Узнать стоимость решенияЗапросить видео презентацию

← Предыдущая статья
Автоматизированная непрерывная репликация Greenplum в PostgreSQL
Следующая статья →
Разделение вычислений и хранения в Greenplum: опыт использования S3 и проекта Yezzey
Запросить видео презентацию Запросить доступ к демо стенду online Узнать стоимость лицензий

Задать вопрос

loading...

Решения

Анализировать ФинансыУвеличивайте ПродажиОптимальный Склад и ЛогистикаМаркетинговые Метрики

Клиенты
  • Группа компаний "Дёке" производит товары для внешней отделки загородных домов. Ассортимент включает виниловый сайдинг, фасадные панели, водосточные системы, чердачные лестницы и гибкую битумную черепицу. Продукция Дёке вызывает гордость у сотрудников и партнеров компании.

  • СберКорус (Группа компаний Сбербанка) – это ИТ‑компания, ИТ‑интегратор, SaaS-провайдер. Является разработчиком цифровых сервисов и услуг для автоматизации широкого диапазона бизнес-процессов юридических лиц. В 2004 году компания стала первым в России оператором электронного документооборота, а в 2012 году вошла в экосистему Сбера. 

  • Novikov group – первый российский ресторанный холдинг, основанный в 1991 году. Это команда профессионалов под управлением Аркадия Новикова, реализующая широкий спектр услуг в сфере гостеприимства: от проведения event-мероприятия до управления рестораном, от установления стандартов сервиса до контроля качества готовой продукции, от построения бизнес-плана проекта до реализации франшизы.

  • ООО "Уральская транспортная компания" — это транспортно-логистическая компания, специализирующаяся на железнодорожных перевозках грузов, создана в 2009 году.

  • Решения
    • Дистрибуция
    • Розничная торговля
    • Производство
    • Операторы связи
    • Страхование
    • Банки
    • Лизинг
    • Логистика
    • Нефтегазовый сектор
    • Медицина
    • Сеть ресторанов
    • E-Commerce
    • Энергетика
    • Фармацевтика
  • Услуги
    • Переход на отечественные BI и DWH
    • Консалтинг
    • Пилотный проект
    • Обучение и сертификация
    • Бесплатное обучение
    • Техническая поддержка
    • Технические задания
    • Сбор требований для проекта внедрения BI-системы
    • CI/CD для DWH
    • Аудит BI приложений
    • Выделенная команда
    • Настойка и поддержка баз данных
    • Разработка BI Стратегии
    • Styleguide для BI-системы
    • Как выбрать BI-систему
  • Платформы
    • FineBI
    • FineReport
    • FineDataLink
    • Коннекторы данных из 1С в BI
    • Airflow + NiFi
    • Visiology
    • Luxms BI
    • Modus BI
    • PIX BI
    • Arenadata
    • ClickHouse
    • Greenplum
    • Postgres Professional
    • Open-source BI: Superset/Metabase
    • Loginom
    • Yandex.DataLens
    • AI / Исскуственный интеллект
    • Optimacros
    • Шины данных
  • Курсы
    • Учебный курс Информационная грамотность
    • Учебный курс для бизнес-аналитиков
    • Учебный курс для системных аналитиков
    • Учебный курс по Data Governance
    • Учебный курс Как стать CDO
    • Учебный курс Современная архитектура хранилища данных
    • Учебный курс по Fine BI
    • Учебный курс по FineReport
    • Учебный курс по DWH
    • Учебный курс по Data Science (ML, AI)
    • Учебный курс по PostgreSQL
    • Учебный курс по Apache Airflow и NiFi
    • Учебный курс по Open-source BI
    • Учебный курс по ClickHouse
    • Учебный курс по DataLens
    • Учебный курс по Loginom
    • Учебный курс по Modus BI и ETL
    • Учебный курс по Visiology
    • Учебный курс по dbt
  • Функциональные решения
    • Создание Data Lake
    • Цифровая трансформация
    • Управление по KPI
    • Финансы
    • Продажи
    • Склад
    • HR
    • Маркетинг
    • Внутренний аудит
    • Категорийный менеджмент
    • S&OP и прогнозная аналитика
    • Геоаналитика
    • Цепочки поставок (SCM)
    • AutoML
    • Process Mining
    • Сквозная аналитика
  • Компания
    • О нас
    • Руководство
    • Новости
    • Клиенты
    • Скачать
    • Контакты
    • Политика конфиденциальности
RutubeVkontakteLinkedInYouTube
ООО "Би Ай Консалт",
ИНН: 7811437757,
ОГРН: 1097847154184
199178, Россия,
Санкт-Петербург,
6-ая линия В.О., Д. 63, 4 этаж
Тел: +7 (812) 334-08-01
Тел: +7 (499) 608-13-06
E-mail: info@biconsult.ru

 

 

 

 

 

×

Пользуясь сайтом, вы соглашаетесь с использованием cookies и политикой конфиденциальности.