Greenplum secrets🎩
698 subscribers
38 photos
8 videos
10 files
67 links
The channel about best practice coding for Greenplum / Канал о том как писать оптимальный код в Greenplum. by @smartyru
Download Telegram
Greenplum secrets🎩
Как вы проверяете наличие данных в таблице ?
How do you check if data exists in a table?
Думаю, что вариант с select count(*) лучше.
Как мы знаем из плана запроса, о чем писал выше, в случае собранной по табл-е статистике, что как правило имеет место,
count(*) делает агрегацию пред агрегатов, посчитанных на сегментах,
а как работает LIMIT для меня до сих пор загадка.

Вывод также подтверждается собранной мной статистикой запуска 5000 различных потоков AirFlow - т.к. у нас было на проме оба варианта, была возможность сравнить.
Разница заметная, если говорить о среднем времени выполнения обеих версий кода в интервале 2х недель.

I think that the option with select count(*) is better.
As we know from the query plan, which I wrote about above, in the case of statistics collected from the table, which usually takes place,
count(*) makes an aggregation of pre-aggregates calculated on segments,
and how LIMIT works is still a mystery to me.

The conclusion is also confirmed by the statistics I collected from running 5000 different AirFlow flows - since we had both options in production, there was an opportunity to compare.

The difference is noticeable, if we talk about the average run time of both versions of the code in the interval of 2 weeks.
👍4
Greenplum secrets🎩
Думаю, что вариант с select count(*) лучше. Как мы знаем из плана запроса, о чем писал выше, в случае собранной по табл-е статистике, что как правило имеет место, count(*) делает агрегацию пред агрегатов, посчитанных на сегментах, а как работает LIMIT для…
Опровержение!
Оказывается, не все так просто.
Поставил эксперимент.
Взял топ 98 самых больших таблиц на проме: 38 партицированных, 60 - нет. Общий вес 78 TB.
Проверил наличие данных для каждой в цикле,
результат - безоговорочная победа метода LIMIT 1, который показал суммарное время = 2.8 ceк, тогда как COUNT(*) дал 1455 сек.

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

А теперь самое интересное.
Я решил проверить, а можно ли ускорить получение первой произвольной строки через LIMIT 1, добавив отсечку по gp_segment_id.

Запилил такую ф-ю:

CREATE OR REPLACE FUNCTION is_tbl_empty(p_tbl_nm text, p_segments_cnt int, p_start_segment_id int)
RETURNS boolean
LANGUAGE plpgsql
AS $function$
declare
v_ int;
begin
for c in 0 .. p_segments_cnt - 1
loop
execute 'select 1 from ' || p_tbl_nm || ' where gp_segment_id = $1 limit 1' into v_ using mod( coalesce(p_start_segment_id,0)+c, p_segments_cnt) ;
if v_ = 1 then
return false;
end if;
end loop;
return true;
end;
$function$


которая возвращает true для пустой табл-ы ( иначе false ), читая первый сегмент, заданный параметром p_start_segment_id
Если табл-а пустая, придется прочитать все p_segments_cnt сегментов.
Но как правило, в реальности имеем дело с новыми даныыми каждый день, поэтому вероятность, что в каждом сегменте будут данные почти 1.

Читать по порядку от 0 до p_segments_cnt - 1 не стоит, т.к. повышаем вероятность нагрузки на сегменты с наименьшими номерами, поэтому функции в
p_start_segment_id лучше передавать random(0, p_segments_cnt).

Неожиданно, данный метод показал лучший результат лишь для 91 из 98 таблиц списка - суммарно затратив 0.9 сек против 4.8 с на все табл-ы .
Для остальных 7 таблиц ( из которых 1 оказалась партицированной) ф-я дала рез-т сильно хуже, в итоге суммарно ф-я затратила 18.7 с против 5 сек у LIMIT 1.

Я проверил для отдельной табли-ы без партиций из этих 7, нет ли в ней перекоса - особо не выявлено.
Вместо p_start_segment_id = 0 я задал для функции номер сегмента с наименьшим числом запиесей ( 87 ) рез-т почти не изменился.
Рез-т воспроизводится. Эти 7 таблиц раскиданы случайно в общем наборе, упорядоченном по убыванию размера таблиц.
Обяснения у меня нет. Но, надеюсь, это не деградация дисков.
🔥5
Секрет 34 ( Порядок бьет класс, v/2.0 или кэшбэк от 11 до 45% на ровном месте )

Как мы знаем из https://t.iss.one/mpp_secrets/52 размер таблицы зависит от выбранного ключа дистрибуции.
Оказывается, размер таблицы зависит и от порядка строк в табл-е
и это можно использовать в нашу пользу, если отсортировать данные по ключу дистрибуции.

Проведем тест.
Для краткости отмечу, что все таблицы ниже создаются колоночными со сжатием zstd1, т.е.
WITH (appendonly=true,orientation=column,compresstype=zstd,compresslevel=1),

🔸Тест 1: 101 млн записей, низкая энтропия данных
Синтетика:
create table t1 as
select a.n, current_date + a.n dt
from (select generate_series(1, 1000000) n) a distributed by(n);

insert into t1
select a.n + trunc(random() * 1000), a.dt
from t1 a
join (select generate_series(1, 100) n) b on 1 = 1;

Сортировка:
create table t1_ordered as
select *
from t1
order by 1 distributed by(n);

select count(*) from t1 -- 101 000 000

Рез-т:
select pg_relation_size('t1') -- 468 795 552
select pg_relation_size('t1_ordered') -- 258 197 512
Экономия места: 45%

🔸Тест 2: 101 млн записей, высокая энтропия данных
Синтетика:
create table t2 as
select a.n, current_date + a.n dt, random() rnd
from (select generate_series(1, 1000000) n) a distributed by(n);

insert into t2
select a.n + trunc(random() * 100000000), a.dt, random() rnd
from t3 a
join (select generate_series(1, 100) n) b on 1 = 1;

Сортировка:
create table t2_ordered as
select *
from t2
order by 1 distributed by(n);

Чек идентичности данных:
select count(*) from t2 -- 101 000 000
select h, sum(cnt) from (
select textin(record_out(t2)) as h, 1 as cnt from t3
union all
select textin(record_out(t2_ordered)) as h, -1 as cnt from t3_ordered) foo
group by h
having sum(cnt) <> 0;

=> 0 строк ( сеты совпадают )

Рез-т:
select pg_relation_size('t2') -- 1 206 135 536
select pg_relation_size('t2_ordered') -- 1 075 361 608
Экономия места: 11%

Напомню, энтропия R - мера хаоса системы, а в данном контексте - % уникальных значений в столбце, т.е.
t2.rnd имеет экстремальную кардинальность ввиду его определения ( 101 млн случайных чисел ),
у dt - R в 100 раз ниже,
R ключа дистрибуции почти такая же как у rnd( ~100 млн случайных чисел) в тесте №2 и в 100 раз меньше - в 1-м тесте ( не более 1.001 млн случайных чисел )

🔸Кого заинтересовал рецепт, делитесь своими результатами - спасем планету от лишнего мусора!
Рассмотрены крайние случаи.
🤔3👍2❤‍🔥1
Запись в партицированную таблицу P через корневую( insert into P as select from S where ) лочит всю таблицу
или запись в P доступна в параллельной сессии ?
Do you think that writing to partitioned table P via root table locks the whole table for writing?
Anonymous Poll
44%
Y(лочится вся P//all P is locked)
56%
N(не лочится вся P//all P is not locked)
Greenplum secrets🎩
Запись в партицированную таблицу P через корневую( insert into P as select from S where ) лочит всю таблицу
или запись в P доступна в параллельной сессии ?
Do you think that writing to partitioned table P via root table locks the whole table for writing?
📌Корректный ответ - N, т.е. вся P не блокируется
Не скрою, сам не поверил, но факты - вещь упрямая

Обоснование:
Сразу скажу, что тест проводился на табл-е P (AOCO zstd1) с двухуровневым партицированием,
где по src_cd(text) было 8 парт--й по document_date(date) - 194. Всего 1552 партиции.
В S было 6 млн строк, по 1 млн для каждого src_cd, где document_date - случайная дата в интервале 2000 - 2025 г
2 сессии запущены почти одновременнно вручную в порядке 1,2

Рез-т: секции не блокируются для записи в параллельной сессии, что видно по вложенности интервалов выполнения операции INSERT - интервал 2й сессии вложен в интервал первой сессии

📌The correct answer is N, i.e. the whole P is not blocked
It's incredible, but facts are stubborn things
Justification:
I will say right away that the test was conducted on the P table(AOCO zstd1) with two-level partitioning,
where there were 8 partitions with src_cd(text) key and 194 with document_date(date). Total is 1552 partitions
In S there were 6 million rows, 1 million for each src_cd, where document_date is a random date in the range of 2000 - 2025
2 sessions were started almost simultaneously manually in the order 1,2

Result: sections are not blocked for writing in a parallel session, which is evident from the nesting of the intervals for executing the INSERT operation - the interval of the 2nd session is nested in the interval of the first session


🔸Тест с разным src_cd:
сессия 1:
Test with different src_cd:
session 1:

DO $$
BEGIN
raise notice 'bef ins:%', clock_timestamp();
insert into P
select * from S
where src_cd = 'S0';
raise notice 'aft ins:%', clock_timestamp();
perform pg_sleep(15);
END$$;
bef ins:2025-04-15 20:19:36.138579+00
aft ins:2025-04-15 20:19:41.516063+00
completed in 20 s 438 ms

сессия 2:
session 2:
DO $$
BEGIN
raise notice 'bef ins:%', clock_timestamp();
insert into P
select * from S
where src_cd = 'S1'
limit 10000;
raise notice 'aft ins:%', clock_timestamp();
END$$;
bef ins:2025-04-15 20:19:37.226968+00
aft ins:2025-04-15 20:19:41.202767+00
completed in 4 s 17 ms


🔸Тест с одинаковым src_cd:
сессия 1:
Test with same src_cd
session 1:

DO $$
BEGIN
raise notice 'bef ins:%', clock_timestamp();
insert into P
select * from S
where src_cd = 'S0';
raise notice 'aft ins:%', clock_timestamp();
perform pg_sleep(15);
END$$;
bef ins:2025-04-15 20:27:57.471849+00
aft ins:2025-04-15 20:28:02.630457+00
completed in 20 s 218 ms

сессия 2:
session 1:
DO $$
BEGIN
raise notice 'bef ins:%', clock_timestamp();
insert into P
select * from S
where src_cd = 'S0'
limit 10000;
raise notice 'aft ins:%', clock_timestamp();
END$$;
bef ins:2025-04-15 20:27:59.406034+00
aft ins:2025-04-15 20:28:02.326422+00
completed in 2 s 970 ms


📌Вывод: В обоих случаях insert в 1й сессии не блокирует insert в 2й сессии.
Задержка 15 сек в 1й сессии дает необходимую паузу для начала теста в рамках 2й сессии, а
limit 10000 в сессии 2 сокращает время выполнения insert дабы исключить потенциальную возможность реализации сценария,
когда временные интервалы insert обеих сессий пересекаются так, что 1-я закончилась пока работает 2-я.

👆Интересно отметить:
Если в качестве приемника P использовать таблицу без партиций, она блокируется на запись сессией 1, и сессия 2 ждет завершения операции insert в сессии 1, но не окончания транзакции.
📌Conclusion: In both cases, insert in session 1 does not block insert in session 2
The 15 sec delay in the 1st session gives the necessary pause to start the test within the 2nd session, and the 10000 limit in session 2 reduces the insert execution time in order to eliminate the potential possibility of a scenario where the insert time intervals of both sessions overlap so that the 1st session ends while the 2nd is running.

👆It is interesting to note:
If you use a table without partitions as a receiver, it is locked for writing by session 1, and session 2 waits for the insert operation in session 1 to complete, but not for the transaction to end.
👍2
Вопрос из зала: **Нужна помощь. Начали упираться в лимиты по открытым на запись AO таблицам.
Не знаешь, как получить список открытых на запись в данный момент таблиц?

Надо отмониторить состояние кластера и понять какие процессы открывают такое количество таблиц.**
Я решил это подсветить здесь для повышения вероятности получить более содержательный ответ.

Мой комментарий : 1) Логи БД должны содержать сам запрос,вызвавший известную ошибку 2) Данную ошибку можно словить и на одной партицированной таблице в одной сессии при отсутствии других партицированных объектов в БД, если постоянно писать в нее данные, например через цепочку операций drop,create,insert. Такой странный сценарий я использовал именно тогда, чтобы найти границы применимости партицированных таблиц в GP. Иными словами, похоже,что drop не обнуляет счётчик заблокированных AO таблиц, который сравнивается с параметром БД max_append_only
Translation of last published secret
Secret 34 (Order beats class, v/2.0 or cashback from 11 to 45% out of the blue)

As we know from https://t.iss.one/mpp_secrets/52, the table size depends on the selected distribution key.
It turns out that the table size also depends on the order of the rows in the table
and this can be used to our advantage if we sort the data by distribution key.

Let's run a test.
For brevity, I note that all tables below are created as columnar with zstd1 compression, i.e.
WITH (appendonly=true,orientation=column,compresstype=zstd,compresslevel=1),
🔸Test 1: 101 million records, low data entropy
Synthetics:
create table t1 as
select a.n, current_date + a.n dt
from (select generate_series(1, 1000000) n) a distributed by(n);

insert into t1
select a.n + trunc(random() * 1000), a.dt
from t1 a
join (select generate_series(1, 100) n) b on 1 = 1;

Sorting:
create table t1_ordered as
select *
from t1
order by 1 distributed by(n);

select count(*) from t1 -- 101 000 000

Result:
select pg_relation_size('t1') -- 468,795,552
select pg_relation_size('t1_ordered') -- 258 197 512
Space saving: 45%

Test 2: 101 million records, high data entropy
Synthetics:
create table t2 as
select a.n, current_date + a.n dt, random() rnd
from (select generate_series(1, 1000000) n) a distributed by(n);

insert into t2
select a.n + trunc(random() * 100000000), a.dt, random() rnd
from t3 a
join (select generate_series(1, 100) n) b on 1 = 1;

Sorting:
create table t2_ordered as
select *
from t2
order by 1 distributed by(n);

Data identity check:
select count(*) from t2 -- 101,000,000
select h, sum(cnt) from (
select textin(record_out(t2)) as h, 1 as cnt from t3
union all
select textin(record_out(t2_ordered)) as h, -1 as cnt from t3_ordered) foo
group by h
having sum(cnt) <> 0;

=> 0 rows ( sets match )

Result:
select pg_relation_size('t2') -- 1 206 135 536
select pg_relation_size('t2_ordered') -- 1 075 361 608
Space savings: 11%

Recall that entropy R is a measure of chaos in a system, and in this context, the % of unique values ​​in a column, i.e.
t2.rnd has extreme cardinality due to its definition (101 million random numbers),
dt - R is 100 times lower,
R of the distribution key is almost the same as rnd (~100 million random numbers) in test #2 and 100 times less - in the 1st test (no more than 1.001 million random numbers)

🔸If you are interested in the recipe, share your results - let's save the planet from unnecessary garbage!
Extreme cases are considered.
Сколько primary сегментов на хосте вашего кластера
How many primary shards are there on your cluster host?
Anonymous Poll
28%
> 10
15%
2
7%
3
27%
4
1%
5
3%
6
1%
7
13%
8
0%
9
3%
10
Те из вас, чей ответ > 10 в опросе, просьба проголосовать вовторно в том же опросе ниже
Those of you who answered ">10" in the poll, please vote again in the same poll below
Сколько primary сегментов на хосте вашего кластера, ч.2
How many primary shards are there on your cluster host, part2?
Anonymous Poll
10%
11
25%
12
20%
13 ( или иное, or other )
5%
14
0%
15
15%
16
0%
17
5%
18
0%
19
20%
20
Вечер пятницы, и самое время продолжить сортирную тему , начатую в начале недели, т.к. есть повод - появился резонный комментарий от одного из активных подписчиков:
Результат ожидаем. Zstd на отсортированной колонке даёт большее сжатие. Совершенно бесполезная вещь для таблиц с большим количеством колонок.

Мой комментарий:
По идее - так оно и есть, коллега, НО есть нюанс, который продемонстрирую в этот раз на реальных данных крупного банка
🔸 Возьмем 6.5 TB таблицу account_balance, состоящую из 18 полей, ключ дистрибуции у которой - account_rk ( по сути л.с. клиента ) . Возьмем одну из ее топовых ( по размеру) партиций. Просто отсортируем по ключу - получим в результате 74 ГБ вместо 173 ГБ . А теперь отсортируем ее же по паре ключ + дата баланса - результат 41 ГБ. Цифры говорят сами за себя. Эффект кластеризации.
🔸 Возьмем таблицу проводок, состоящую из 92 полей, ключ дистрибуции у которой -doc_rk уникален в отличие от 1-го кейса После сортировки одной из ее секций по ключу получим 244 ГБ вместо 270. И вот тут самое интересное, экономия в 10% обусловлена главным образом не сжатием ключа при сортировке . Проверим, создав таблицу только из отсортированного doc_rk - размер 14.5 ГБ, в исходной табл-е вес этой колонки - 23.5 ГБ. Иными словами, при сортировке в структуре экономии в 10% ( 270 => 244 ГБ) на ужатый ключ приходится лишь 1/3 общего высвобожденного места ( 9 ГБ из 26 ) Феномен, о котором мне сложно судить, т.к. в отличие от 1-го кейса, тут строки в отсортированном наборе не имеют какой-либо связи кроме принадлежности одному сегменту. И даже, если предположить, что соседние doc_rk : N, N+1 определяют одну проводку, такие строки лягут на разные сегменты, ничего не зная друг о друге, тем самым исключая возможность дополнительной выгоды в виде наличия одинаковых атрибутов в паре.
🤔3
Друзья, начинаем эстафету, делимся в комментах своими Пасхальными изделиями!
Friends, let's start the relay race, share your Easter creations in the comments!
🔥7😁1
В точку!
Forwarded from Андрей
This media is not supported in your browser
VIEW IN TELEGRAM
Поэтому учиться программированию и работать сложно .приходишь после суток своей работы и тебе ещё надо сложное решать
👍11
Сегодня группа Arenadata проведет День инвестора. IPO компании на Московской бирже состоялось в октябре 2024 года.

Спасибо @lyubov_medvedeva за ссылку на конференцию:
https://arenaday.io/live/
👍1👎1
Greenplum secrets🎩
Секрет 8(Сколько весит таблица .. или о добром cross-join замолвите слово!) Как-то наш DBA подготовил отчет о размере таблиц по требуемому списку и крайне удивился. Оказалось, что функция pg_relation_size для вычисления размера таблицы дает разный результат…
Один из читателей сообщил, что у него рецепт выше не работает.
Проверил на разных контурах - у меня работает, но есть нюанс.

Если таблица public.foo2 распределена по полю tbl_nm ( содержащая список таблиц, размер котрых хотим получить), а не RANDОMLY,
то рез-ты действительно разные, причем разница в размерах, вычисленная двумя способами кратна числу сегментов.

Почему так, за гранью моего понимания.

One of the readers reported that the recipe above does not work for him.
I checked it on different circuits - it works for me, but there is a nuance.

If the public.foo2 table is distributed by the tbl_nm field (containing the list of tables whose size we want to get), and not RANDОMLY,
then the results are really different, and the difference in sizes calculated in two ways is a multiple of the number of segments.

Why this is so is beyond my understanding.
В иерархической таб-е в parent_id на высшем уровне иерархии пишете NULL или спец. код, который никуда не ссылается? In a hierarchical table, in the parent_id field at the top level of the hierarchy, use NULL or a number that refers to nothing
Anonymous Poll
72%
NULL
28%
спец.код (special code)
Секрет II (Владимирский транзит, ветер северный)
Долго думал, как и стоит ли шарить данный секрет ввиду его неочевидной ценности и витиеватости сюжетной линии.
Поэтому занесу его в раздел ништяков
К тому же, на первый взгляд, он не имеет отношения ни к MPP, ни к Postgre, а по сути относится к любой БД.
Думаю, я бы никогда не спросил себя, а значит не создал опрос выше, если бы не
случайное знакомство с 252 "пациентом ", спиллы которых лечу регулярно.
⌛️Сразу скажу, если non-ANSI join вида
a.id in (b.id, c.id) или a.id = coalesce(b.id,c.id)

для вас табу, не тратьте время на инфо ниже.

♻️︎Довелось на неделе тюнить запрос, спилл 21 ТБ
Те, кто используют рез-т A LEFT JOIN B, вероятно используют поля B, если запись B не найдена, через ф-ии
вида decode, coalesce, case when наконец.
И вы же, скорее всего, если в запросе есть JOIN C полученного на предыдщем шаге сета, вместо прямого использования
отсутствующих полей B обернете их в coalesce( b.id, c.<col> ) или что-то пооптимальнее.

Оставим на совести ленивых разрабов использование coalesce в join, который заставил меня переписать запрос ниже тройным UNION ALL, чтобы избавиться от спилла, суть не в этом.

А вот что действительно интересно, это то, что если скажем в иерархической таблице CL, хранящую связь client_rk => parent_clent_rk
у предков самого высокого уровня( интересно как это сказать одним словом ), вместо NULL в parent_client_rk записать терминальную ссылку в никуда,
скажем -1, полагая, что все client_rk > 0 определены на домене положительных чисел, есть шанс облажаться.

Вот и автор реального запроса в нашем ХД выше предполагает, что если LEFT JOIN не найдет строк в CL, то следующий INNER JOIN возьмет данные из TR, и это сработает корректно.

Скелет самого запроса ( убрал лишнее ):
 
select coalesce(cl.parent_client_rk, tr.payee_client_rk) as client_rk,
tr.document_date,
sbp.service_start_date,
sbp.agreement_num,
sum(tr.payer_rur_sum) as payer_rur_sum
from agent tr
left join client_hierarchy cl
on tr.payee_client_rk = cl.client_rk
inner join service sbp
on sbp.client_rk = coalesce(cl.parent_client_rk, tr.payee_client_rk) and
sbp.service_start_date <= tr.document_date::timestamp
group by coalesce(cl.parent_client_rk, tr.payee_client_rk),
tr.document_date,
sbp.service_start_date,
sbp.agreement_num

Но что, если LEFT JOIN взял строки CL, которые являются корнем иерархии(выше которых ничего нет), и у которых
БА решил, что в parent_clent_rk будет спец код (Don't ask me why.. хотя если по полю нужен индекс, то NULL не есть хорошо) ?

Запрос вообще говоря просто не вернет данные, т.к. на выходе coalesce(cl.parent_client_rk, ... ) будет константа = спец коду.

📌Закон Мерфи - если что-то может пойти не так, оно пойдет не так, тут вполне может напомнить о себе в 100500-й раз,
если SQL разработчик не остановит инерцию мышления, и не проверит, а что хранится в ссылке parent_id у праотцов.

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

Конечно, то, что мне попался пример с иерархическая табл-ей - случайность, которая помогла понять суть потенциальной бомбы, в принципе все это верно для любого запроса, где есть трио таблиц со связью вида
 
A LEFT JOIN B on a.<col1> = b.<col1> + coalesce (b.<col2>, C.<col>)

p.s.
Не ожидал, что вариант ответа: NULL наберет < 100%
👍3👏1
Секрет 35 ( Еще одна проблема пустой таблицы )
Т.к. Москву и окрестности накрыло сырью и хмурью, попробую чуток разнообразить ваш субботний авечер!
Я много говорил о неожиданных спецэффектах join-а с пустыми таблицами как и о преимуществах определенных видов join-ов перед другими, но много не значит достаточно.
BFG-10K. долго скучающее на стене, выстрелило накануне 1 Мая, напомнив мне, ЧТО незаслуженно забыт FULL OUTER JOIN!!!.
Спешу закрыть этот вопрос, т.к. во-первых, наверняка некоторые используют эту трансформацию, а во-вторых,
вчера на проме пробегал образцово-показательный зверь -) : полный join с пустой таблой, который наследил спиллом в 28 TB:
with new_set as (select * from account_balance where to_version_id = 9223372036854775807),
old_set as (select * from account_balance where 1=0)
select
sum(case when coalesce(t0.account_rk,t1.account_rk) is not null and
coalesce(t0.effective_date,t1.effective_date) is not null and
coalesce(t0.src_cd,t1.src_cd) is not null and
t0.hash_diff != t1.hash_diff then 1 else 0 end) change_count
from new_set t0 full outer join old_set t1
on t0.account_rk = t1.account_rk and
t0.effective_date = t1.effective_date and
t0.src_cd = t1.src_cd
where t0.account_rk is null or t1.account_rk is null or t0.hash_diff != t1.hash_diff

Данный запрос возбудил дважды мой интерес:
1) Зачем он, выполняющий мартышкин труд, нужен в принципе ?
2) Как он смог ТАК нагнуть сервер ?

Ответ на первый вопрос меня не удовлетворил, т.к. не объясняет его сакральную суть: де это регресс-тест.
⛏️Ответ на второй я нашел в плане идентичного запроса, но на порядок меньшей выборке в new_set(100 млрд строк), вот он .

Заметим, что в плане есть сортировка по полям join-а, которая в зависимости от объема данных, выполняется либо на диске
( Sort Method: external merge Disk: 6 254 949 152kB в данном случае )
либо в памяти ( Sort Method: quicksort Memory: 12826236kB для 100 млн строк )

Таким образом, для GPORCA не имеет значения число строк в табл-е справа от FULL JOIN (пустой в моем случае) - Собаки лают, караван идёт, все идет по плану! - Занавес!
План:
Aggregate  (cost=0.00..66485002.13 rows=1 width=24) (actual time=1073504.012..1073504.012 rows=1 loops=1)
-> Gather Motion 348:1 (slice2; segments: 348) (cost=0.00..66485002.13 rows=1 width=24) (actual time=1073500.931..1073503.909 rows=348 loops=1)
-> Aggregate (cost=0.00..66485002.13 rows=1 width=24) (actual time=1073502.299..1073502.300 rows=1 loops=1)
-> Redistribute Motion 348:348 (slice1; segments: 348) (cost=0.00..66424753.86 rows=287356316 width=78) (actual time=625468.045..953332.916 rows=287403064 loops=1)
-> Result (cost=0.00..48232823.20 rows=287356316 width=78) (actual time=722544.174..913870.520 rows=288956000 loops=1)
Filter: ((account_rk IS NULL) OR (account_rk IS NULL) OR (hash_diff <> hash_diff))
-> Merge Full Join (cost=0.00..35072823.46 rows=287356316 width=78) (actual time=722544.146..866509.306 rows=288956000 loops=1)
Merge Cond: ((account_rk = account_rk) AND (effective_date = effective_date) AND (src_cd = src_cd))
-> Sort (cost=0.00..2349522.41 rows=287356316 width=50) (actual time=722543.481..831079.605 rows=288956000 loops=1)
" Sort Key: account_rk, effective_date, src_cd"
Sort Method: external merge Disk: 6 254 949 152kB
-> Seq Scan on account_balance (cost=0.00..60482.72 rows=287356316 width=50) (actual time=0.549..51603.818 rows=288956000 loops=1)
Filter: (to_version_id = '9223372036854775807'::bigint)
-> Sort (cost=0.00..0.00 rows=0 width=28) (actual time=0.000..1.051 rows=0 loops=1)
" Sort Key: account_rk, effective_date, src_cd"
Sort Method: quicksort Memory: 11484kB
-> Result (cost=0.00..0.00 rows=0 width=28) (actual time=0.000..0.146 rows=0 loops=1)
-> Result (cost=0.00..0.00 rows=0 width=28) (actual time=0.000..0.086 rows=0 loops=1)
-> Result (cost=0.00..0.00 rows=0 width=28) (actual time=0.000..0.041 rows=0 loops=1)
One-Time Filter: false
Planning time: 82.315 ms
(slice0) Executor memory: 1092K bytes.
"* (slice1) Executor memory: 101556K bytes avg x 348 workers, 122489K bytes max (seg19). Work_mem: 121678K bytes max, 30902646K bytes wanted."
" (slice2) Executor memory: 147K bytes avg x 348 workers, 147K bytes max (seg0)."
Memory used: 524288kB
Memory wanted: 61806290kB
Optimizer: Pivotal Optimizer (GPORCA)
Execution time: 1073549.572 ms


Интересно отметить, что
🔸 спилл непропорционален числу строк в new_set, при увеличении которого с 10 до 100 млрд клонированием спилл вырос с 482 до 5965 ГБ
🔸 если в ON предикате join-а оставить только ключ дистрибцуии табл-ы account_rk, то спилл для new_set из 10 млрд строк сокращается до 153 ГБ, а Redistribute в плане исчез.

📌 Вывод: Если FULL JOIN в вашем коде неизбежен, проверьте табл-у справа на пустоту, и сделайте отдельную ветку кода на этот случай.
🔥1