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-архитектуру.
- Проблема пустой таблицы и спиллов: как не попасть в ловушку
- Оптимизация GROUP BY: когда CTE помогает
- Статистика — твой лучший друг в PL/pgSQL
- Рекурсивные запросы и shared-nothing: как избежать катастрофы
- Опасность non-equi join: как Nested Loop убивает производительность
- Left Join и перекос (skew): спасаем запрос через union all
- Правильное измерение размера таблиц: cross join спасает
- Сиквенсы в ETL: кэш или смерть
- Последовательности в Greenplum: как некэшированный sequence может убить хранилище
- Удаление дублей: GP-style через ctid + gp_segment_id
- Философия Greenplum и Rubik’s cube: проектируй как собери кубик
Проблема пустой таблицы и спиллов в Greenplum: как не попасть в ловушку
Greenplum — это MPP-СУБД (Massively Parallel Processing), где запросы распараллеливаются и исполняются на множестве сегментов. Такая архитектура даёт огромные преимущества в обработке больших объёмов данных, но требует особого внимания к проектированию SQL-запросов. Одна из неочевидных проблем, с которой сталкиваются даже опытные разработчики, — это «ловушка пустой таблицы», приводящая к спиллам (spill).
Что такое spill?
Spill — это ситуация, при которой из-за нехватки памяти на сегменте промежуточные данные начинают выгружаться на диск. Это резко замедляет выполнение запроса (в десятки или сотни раз) и может привести к отказу в обслуживании, если диск переполнится.
В Greenplum спилл может возникнуть не только при агрегациях или сортировках, но даже при, казалось бы, безобидных операциях фильтрации. Особенно — при использовании подзапросов с тяжелыми таблицами.
Кейс: пустая таблица и тяжелый подзапрос
Рассмотрим следующую ситуацию:
-
У нас есть таблица
foo, в которой содержатся данные, например, о заказах. -
Есть таблица
big_tbl, содержащая десятки или сотни миллионов строк, например, справочник всех клиентов.
Допустим, мы хотим выбрать из foo только те записи, у которых key отсутствует в big_tbl.pk.
Пример проблемного кода
SELECTt.a, t.b, t.cFROMfoo tWHEREt.keyNOT IN(SELECTpkFROMbig_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
WITHaAS(SELECTpkFROMbig_tblINTERSECT SELECTkeyFROMfoo)SELECTt.a, t.b, t.cFROMfoo tWHEREt.keyNOT IN(SELECTpkFROMa)
Почему это работает:
-
INTERSECTпозволяет сразу отбросить ненужные строки, ограничивая объем сравниваемых значений. -
CTE (
WITH a AS (...)) материализует результат, и оптимизатор может построить Hash Join, а не вложенный цикл. - Это избавляет от лишней передачи данных между сегментами и снижает риск спилла.
Пояснение терминов
|
Термин |
Объяснение |
|---|---|
|
CTE (Common Table Expression) |
Временная именованная подтаблица, создаваемая с помощью |
|
Spill |
Ситуация, когда Greenplum выгружает данные на диск из-за нехватки оперативной памяти. |
|
Hash Join |
Метод соединения таблиц, основанный на предварительном хэшировании одной из таблиц. Эффективен при равенстве ключей. |
|
Anti-Semi Join |
Тип соединения, при котором выбираются только строки из первой таблицы, которые не имеют соответствий во второй. Используется для |
|
GPORCA |
Оптимизатор запросов Greenplum, который строит более эффективные планы выполнения по сравнению с Legacy Planner. |
Практический пример: поведение на проде
На продакшене одна из ETL-функций использовала такую конструкцию:
SELECT * FROMdealsWHEREdeal_idNOT IN(SELECTdeal_idFROMblacklist)
Когда blacklist стал весить 150 млн строк, а deals — временно был пуст (на ночь), функция неожиданно зависла. Проблема решилась после:
-
Замены
NOT INнаNOT EXISTS— более устойчивое к NULL. -
Материализации
blacklistв отдельную временную таблицу с индексом. -
Добавления
ANALYZEпосле загрузкиblacklist.
Рекомендации
-
Не используйте
NOT IN (SELECT ...)с большими таблицами — заменяйте наNOT EXISTSили CTE. -
Анализируйте таблицы с
ANALYZEпосле загрузки или изменения — иначе Greenplum не сможет построить эффективный план. - Проверяйте статистику пустых таблиц — их отсутствие влияет на выбор плана.
-
В больших подзапросах используйте
INTERSECT,JOIN,LEFT JOIN ... IS NULLи другие техники с материализацией. -
Всегда профилируйте тяжелые запросы через
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 метрик:
SELECTregion, channel, product_type,SUM(m1),SUM(m2), ...,SUM(m50)FROMsalesGROUP BY 1,2,3;
Почему такой запрос может "упасть":
- Greenplum должен удерживать все агрегаты в памяти одновременно на каждом сегменте;
- Под агрегаты строятся временные хэш-таблицы, и при нехватке памяти — система сбрасывает их на диск (spill);
- Чем больше метрик, тем больше строк агрегированного состояния.
Термины, которые нужно знать
|
Термин |
Объяснение |
|---|---|
|
GROUP BY |
SQL-конструкция для группировки строк по значениям одного или нескольких столбцов. |
|
Агрегатные функции |
Функции, которые сводят множество значений в одно: |
|
CTE (Common Table Expression) |
Конструкция |
|
Spill |
Ситуация, при которой данные промежуточных операций выгружаются на диск, т.к. не помещаются в оперативную память. |
|
Hash Aggregate |
Тип агрегации, при котором строки группируются с использованием хэш-таблиц. Очень эффективно до определенного объема. |
|
Sort Aggregate |
Альтернативный тип агрегации — сортировка с последующим слиянием групп. Используется при нехватке памяти. |
Решение: разбиваем агрегаты на подмножества
Вместо одного запроса с 50 агрегациями — делим их на 2 подзапроса с 25 метриками каждый и объединяем результат.
WITHaAS(SELECTregion, channel, product_type,SUM(m1)ASm1,SUM(m2)ASm2, ...,SUM(m25)ASm25FROMsalesGROUP BY 1,2,3),bAS(SELECTregion, channel, product_type,SUM(m26)ASm26,SUM(m27)ASm27, ...,SUM(m50)ASm50FROMsalesGROUP BY 1,2,3)SELECTa.*, b.m26, b.m27, ..., b.m50FROMaJOINbONa.region=b.regionANDa.channel=b.channelANDa.product_type=b.product_type;
Почему это работает
- В каждом подзапросе в памяти находится в 2 раза меньше агрегатов.
- Это снижает размер хэш-таблиц и уменьшает риск spill.
- При объединении используется JOIN по группировочным ключам, который может быть эффективно реализован в MPP.
- Если сегменты используют одинаковые ключи распределения — джойн происходит локально, без движения данных между сегментами.
Практический кейс: ускорение отчета
На одном из проектов клиент использовал такой запрос (упрощённо):
SELECTorg_id,month,SUM(revenue),SUM(cost),SUM(discount), ...,SUM(var50)FROMfct_salesGROUP BY 1,2;
В таблице — 2 миллиарда строк, 60 агрегаций. Запрос исполнялся 23 минуты.
После применения CTE с 3 подзапросами (SUM(var1..var20), SUM(var21..var40), SUM(var41..var60)) и JOIN — время снизилось до 4 минут.
Как понять, что у вас проблема?
-
EXPLAIN ANALYZE показывает
Spill File: trueиDisk I/O. -
План запроса использует
SortAggregateвместоHashAggregate— признак нехватки памяти. -
Видите в логах:
Statement cancelled due to insufficient memory to run query with hash aggregation.
Технические детали (под капотом)
Как работает GROUP BY в Greenplum
- Вначале сегмент собирает строки и строит хэш по ключам.
-
Для каждой группы создается слот агрегации (например, для
SUM(m1)— число). - Чем больше агрегатов — тем больше памяти нужно на каждый слот.
Что происходит при спилле
-
Если памяти не хватает — временные данные сбрасываются на диск (
spill). - Данные читаются обратно в последующих этапах.
- Это замедляет выполнение в 10–100 раз.
Рекомендации
|
Советы |
Комментарии |
|---|---|
|
Разбивайте запрос с множеством агрегатов на несколько CTE |
Это главный способ избежать спилла |
|
Следите за |
Можно увеличить их в пределах разумного |
|
Группируйте агрегаты логически |
Например, финансовые отдельно от маркетинговых |
|
Используйте одинаковые ключи распределения в CTE |
Это позволит объединить результаты без передачи данных |
|
Проверяйте план через |
Следите за |
Запросы с большим числом агрегатов — одна из основных причин спиллов и деградации производительности в 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-функций.
Возможные значения:
|
Значение |
Что означает |
|---|---|
|
|
Не собирать статистику вообще (по умолчанию — опасно!) |
|
|
Собирать, если изменено достаточно строк |
|
|
Собирать, если статистика отсутствует (надежный вариант) |
Рекомендуемое решение
Устанавливайте нужный режим прямо внутри тела функции:
CREATE ORREPLACEFUNCTIONpublic.fn_etl_stage()RETURNSvoidLANGUAGEplpgsqlAS$$BEGIN SETgp_autostats_mode_in_functions= 'on_no_stats';DELETE FROMdeals_tmp;INSERT INTOdeals_tmpSELECT * FROMstaging.deals_delta;-- Теперь статистика по deals_tmp будет собрана, если она отсутствует END;$$;
Важно:
- Установка переменной вне тела функции не действует — нужно именно внутри.
-
Также можно добавить принудительный
ANALYZEпри необходимости.
Практический кейс: зависание запроса с пустой таблицей
На проекте с объемом 300+ TB была функция, вызывающаяся для формирования витрины:
SELECT * FROMfn_build_report('2024-06-01','2024-06-30')WHEREsrc_cd= 'CUST' ANDdeal_idIN(SELECTidFROMblacklist);
blacklist была пуста, но без статистики. В результате запрос уходил в nested loop и зависал, несмотря на отсутствие реальных данных. Проблема ушла после:
-
Добавления
SET gp_autostats_mode_in_functions = 'on_no_stats'в телоfn_build_report. -
Принудительного
ANALYZEпоblacklist.
Как понять, что статистики нет?
Выполните:
SELECTrelname, last_analyzeFROMpg_stat_user_tablesWHERErelname= 'название_таблицы';
Если last_analyze — NULL, статистики нет. Рекомендуется периодический автосбор через autovacuum или ETL-скрипт.
Технические детали
- В отличие от обычного SQL-запроса, PL/pgSQL-функции — «черный ящик» для планировщика.
- План запроса может быть сгенерирован один раз и использоваться повторно, даже если данные внутри таблиц сильно изменились.
-
gp_autostats_mode_in_functionsнужен, чтобы вынудить пересбор статистики там, где иначе она не запускается.
Рекомендации
|
Что делать |
Зачем |
|---|---|
|
Всегда указывайте |
Чтобы статистика собиралась автоматически |
|
Анализируйте таблицы вручную, если ETL не обновляет статистику |
Без этого оптимизатор будет "слепым" |
|
Проверяйте |
Убедитесь, что статистика актуальна |
|
Избегайте функций с большим числом join-ов и подзапросов без GPORCA |
Они плохо оптимизируются |
|
Настройте автоматический |
Особенно для 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 RECURSIVEagreement_chainAS(SELECTagreement_id, previous_agreement_id,1 ASlevelFROMagreementsWHEREis_first= true UNION ALL SELECTa.agreement_id, a.previous_agreement_id, c.level+ 1 FROMagreements aJOINagreement_chain cONc.agreement_id=a.previous_agreement_idWHEREc.level< 10)SELECT * FROMagreement_chain;
Что здесь не так?
-
Таблица
agreementsраспределена поagreement_id. -
В join'е участвуют оба поля:
agreement_idиprevious_agreement_id. -
Но
previous_agreement_idне является ключом распределения. - Это значит, что Greenplum вынужден пересылать всю таблицу на каждый сегмент, потому что сопоставление не может быть выполнено локально.
Как это нарушает shared-nothing
При самосоединении таблицы по полю, отличному от ключа распределения:
- Вся таблица копируется на каждый сегмент (репликация).
-
Происходит огромное количество
Motion-операций. - Запрос может уйти в спилл или быть отменен по превышению памяти (VMEM).
На больших объёмах это может привести к полному отказу кластера.
Решение: зеркальная таблица с альтернативным распределением
Создадим копию таблицы agreements, но распределим её по previous_agreement_id:
CREATE TABLEagreements_mirrorWITH(appendonly=true, orientation=column)AS SELECT * FROMagreementsDISTRIBUTEDBY(previous_agreement_id);
Теперь используем agreements_mirror в рекурсивной части:
WITH RECURSIVEagreement_chainAS(SELECTagreement_id, previous_agreement_id,1 ASlevelFROMagreementsWHEREis_first= true UNION ALL SELECTa.agreement_id, a.previous_agreement_id, c.level+ 1 FROMagreements_mirror aJOINagreement_chain cONc.previous_agreement_id=a.agreement_idWHEREc.level< 10)SELECT * FROMagreement_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 запроса: если видите |
Значит, одна из таблиц копируется на все сегменты |
|
Если рекурсия обязательна — ограничивайте глубину ( |
Это снижает нагрузку |
|
Используйте |
Так вы увидите, как именно выполняется 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 с условием вида |
|
Non-equi join |
JOIN, где используется |
|
Nested Loop Join (NLJ) |
Метод соединения, при котором для каждой строки из первой таблицы перебираются все строки второй таблицы. Очень медленно при большом объеме данных. |
|
Hash Join |
Быстрый метод соединения по равенству, при котором в оперативной памяти строится хэш-таблица. |
|
Motion |
Передача данных между сегментами в Greenplum. Желательно избегать. |
|
Spill |
Ситуация, когда промежуточные данные выгружаются на диск. |
Проблема: BETWEEN вызывает Nested Loop Join
Допустим, у нас есть:
-
Факт
sales, с датой транзакцииsales.dt; -
Измерение
dim_periods, содержащее границы периода:date_from,date_to.
Наивное решение:
SELECTs.*, p.date_from, p.date_toFROMsales sJOINdim_periods pONs.dtBETWEENp.date_fromANDp.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: получаем уникальные даты
WITHdistinct_datesAS(SELECT DISTINCTdtFROMsales)
Шаг 2: находим соответствующий период для каждой даты
, matched_datesAS(SELECTd.dt, p.date_from, p.date_toFROMdistinct_dates dJOINdim_periods pONd.dtBETWEENp.date_fromANDp.date_to)
Шаг 3: соединяем обратно с фактами по dt (теперь можно использовать Hash Join!)
SELECTs.*, m.date_from, m.date_toFROMsales sJOINmatched_dates mONs.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.
Технические детали
|
Особенность |
Что происходит |
|---|---|
|
|
Трактуется как non-equi join → нет Hash Join |
|
|
Устрашающе неэффективен при больших объемах |
|
|
Возможен только при |
|
Вынос |
Уменьшает число сравнений и переключает план на Hash Join |
Рекомендации
|
Что делать |
Почему |
|---|---|
|
Избегайте |
Они лишают вас преимущества Hash Join |
|
Если не избежать — предварительно выделяйте |
Это ограничит объем дорогой операции |
|
Работайте с календарными измерениями через mapping-таблицы |
Они позволяют прямой equi-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 |
Практика, при которой фильтры применяются как можно раньше в плане выполнения, чтобы сократить объем данных. |
Проблема: фильтрация по пустой таблице без статистики
Предположим, у вас есть функция, возвращающая набор строк за период:
SELECTdeal_idFROMfn_get_deals(p_from := '2024-09-01', p_to := '2024-09-02')WHEREdeal_type= 'IMOEX' ANDdeal_idIN(SELECTidFROMblacklist);
blacklist — таблица, в которой нет ни одной строки.
Кажется, что всё должно быть молниеносно, ведь фильтр по пустой таблице должен вернуть пустое множество. Но на деле:
- Запрос висит;
- Нагрузка на кластер резко возрастает;
-
Запрос выполняется в десятки раз дольше, чем без фильтра
IN (SELECT ...).
Что пошло не так?
Всё дело в отсутствии статистики по пустой таблице blacklist.
Что делает GPORCA:
-
Оценивает селективность подзапроса
IN (SELECT ...)как неизвестную; - Не может исключить подзапрос на стадии оптимизации;
-
Не знает, что
blacklistпуст — без статистики он предполагает, что там много строк; -
Весь результат функции
fn_get_dealsполностью вычисляется, прежде чем происходит фильтрация; - Потенциально использует Nested Loop вместо Hash Join — потому что не видит возможности оптимизации.
Решение: соберите статистику даже по пустым таблицам
ANALYZE blacklist;
После этой простой команды:
-
Greenplum узнаёт, что таблица пуста (
relpages = 0,reltuples = 0); - Оптимизатор может сразу отбросить подзапрос, ещё на этапе планирования;
- Запрос становится в 1000+ раз быстрее, потому что работает с нулевым множеством.
Практический кейс: реальный запрос с функцией
SELECTdeal_idFROMfn_deal_report('2024-07-01','2024-07-02')WHEREstatus= 'ACTIVE' ANDdeal_idIN(SELECTrisk_contract_idFROMfraud_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, и фильтруете их результат, убедитесь, что:
- Вы собрали статистику по всем таблицам, участвующим в фильтре;
- Вы вынесли фильтрацию за пределы функции, если можно.
Рекомендации
|
Что делать |
Зачем |
|---|---|
|
Всегда запускайте |
Это позволит оптимизатору правильно оценить селективность |
|
Используйте |
Если выполняется полностью — это признак проблемы |
|
Не фильтруйте в функции, если можно фильтровать вне |
Это даёт больше контроля над планом |
|
Проверяйте, используется ли GPORCA или Legacy Planner |
|
|
При наличии подзапроса |
Это позволяет использовать |
Отсутствие статистики даже в пустых таблицах может привести к многократной деградации производительности в 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 на два отдельных запроса:
-
Строки с ненулевым addr_fk обрабатываются через обычный
JOIN— безопасный и предсказуемый. - Строки с 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 блокировал следующий
-
Проблема решена через:
- Удаление sequence
- Создание новой с cache 10000
- Полная пересозданная таблица
Как обнаружить проблему
- Таблицы с автонумерацией неожиданно большие
- 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
Практические советы
- Никогда не используй разные ключи распределения, если таблицы логически связаны
- При проектировании витрины всегда определяй, по какому ключу будет строиться отчет
- Используй hash distribution по числовым ID, не по строкам
- Если colocation невозможен — сделай предварительное агрегирование в staging
- Всегда проверяй план через 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 упала
Рекомендации
- Всегда используй cache при создании sequence в ETL
- Не вызывай nextval в цикле без буферизации
- Следи за bloating через pg_total_relation_size
- Используй staging и сериализацию идентификаторов отдельно от записи
- Не оставляй 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: зачем компании хранилище данных




