Learning Platform
Глоссарий Troubleshooting
Урок 09.04 · 23 мин
Средний
cbojoin-distributionbroadcastpartitioned

Join distribution: BROADCAST против PARTITIONED

Прошлый урок решал вопрос «в каком порядке джойнить таблицы». Этот — другой, не менее важный: «как именно распределить один джойн по кластеру». Когда Trino соединяет две таблицы, данные физически разбросаны по воркерам, а для join по равенству строки с совпадающим ключом обязаны встретиться на одной ноде. Свести их можно двумя принципиально разными способами: разослать одну таблицу целиком всем (BROADCAST) или перераспределить обе по хэшу ключа (PARTITIONED).

Выбор между ними CBO делает по статистике, и он влияет на скорость и память сильнее, чем почти любая другая деталь плана. Этот урок — про механику обоих способов, про их компромисс и про свойство, которым выбор управляется.


Build-сторона и probe-сторона

Сначала термин, нужный для всего дальнейшего. Hash join в Trino работает в две фазы. По одной таблице строится хэш-таблица в памяти — это build-сторона (build side). По другой таблице идёт поток строк, и для каждой в хэш-таблице ищется совпадение — это probe-сторона (probe side).

Асимметрия принципиальна. Build-сторона целиком оказывается в памяти как хэш-таблица — её размер критичен. Probe-сторона стримится строка за строкой, в памяти целиком не лежит — она может быть огромной. Поэтому build-стороной всегда стараются сделать меньшую таблицу. И именно от build-стороны зависит, какой способ распределения выбрать.

Build-сторона и probe-сторона hash join
Build-сторонаПо ней строится хэш-таблица в памяти. Размер критичен — она целиком в памяти. Делают меньшую таблицу
probe ищет совпадения
Probe-сторонаПо ней идёт поток строк, для каждой ищется совпадение в хэш-таблице. Стримится, в памяти целиком не лежит — может быть огромной

BROADCAST join: разослать build-сторону всем

При BROADCAST join build-сторона (меньшая таблица) целиком копируется на каждый воркер. Каждая нода получает полную копию build-таблицы, строит из неё локальную хэш-таблицу, и затем probe-сторона остаётся на месте — каждый воркер пробивает свою часть probe-данных по локальной хэш-таблице.

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

Что здесь цена. Build-сторона рассылается на каждую ноду и на каждой целиком держится в памяти как хэш-таблица. Если build-таблица — 50 МБ, а воркеров 20, по сети уходит 50 МБ × 20 = 1 ГБ суммарной рассылки, и каждый из 20 воркеров тратит 50 МБ памяти на свою копию. Пока build-сторона мала, это копейки. Но если build-сторона велика — её копия не влезет в память воркера, и join упадёт по памяти. BROADCAST работает, только когда build-сторона достаточно мала.

BROADCAST: build-сторона на каждый воркер
Build-сторона (маленькая)Меньшая таблица целиком копируется на каждый воркер кластера
копия на каждую ноду
Воркер 1: копия + probeПолная копия build-стороны плюс своя часть probe-данных, которые не двигались по сети
Воркер 2: копия + probeТо же: полная копия build-стороны и своя часть probe. Probe-сторона осталась на месте

PARTITIONED join: перераспределить обе таблицы по хэшу

При PARTITIONED join (его ещё зовут hash join по способу распределения) обе таблицы перераспределяются по сети по хэшу join-ключа. Хэш-функция от ключа определяет, на какой воркер уедет строка, — и применяется к строкам обеих таблиц одинаково. Поэтому строки с одинаковым ключом из обеих таблиц гарантированно попадают на один воркер; там они и соединяются.

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

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

PARTITIONED: обе таблицы по хэшу ключа
Таблица AПерераспределяется по хэшу join-ключа: строка едет на воркер, определяемый хэшем
Таблица BПерераспределяется по тому же хэшу того же ключа — строки с равным ключом попадут на тот же воркер, что и из A
равные ключи -> один воркер
Воркер держит свою долю обеих таблицКаждый воркер получает только свою часть build-стороны — суммарная память кластера складывается, можно джойнить большое с большим

Компромисс и свойство join-distribution-type

Сведём выбор в таблицу — это и есть суть компромисса.

АспектBROADCASTPARTITIONED
Build-сторонакопируется на каждый воркер целикомразрезана по воркерам
Probe-сторонаостаётся на месте, не двигаетсяперераспределяется по сети
Сетевой обменмалый (только build × число нод)большой (шаффл обеих таблиц)
Память на воркерполная build-таблицатолько своя доля build
Когда подходитbuild-сторона малаобе таблицы большие
РискOOM, если build-сторона великалишний шаффл, если build мала

Управляет выбором свойство — конфигурационное join-distribution-type, сессионное join_distribution_type. Три значения:

  • AUTOMATIC — по умолчанию. CBO сам решает по статистике: оценивает размер build-стороны и, если она достаточно мала, выбирает BROADCAST, иначе PARTITIONED.
  • BROADCAST — принудительно broadcast.
  • PARTITIONED — принудительно partitioned.

Решение AUTOMATIC опирается на порог: build-сторона уходит в BROADCAST, только если её оценочный размер не превышает лимит. Лимит задаётся свойством join-max-broadcast-table-size (сессионно join_max_broadcast_table_size) и по умолчанию составляет 100 МБ. Build-сторона мельче порога — BROADCAST; крупнее — PARTITIONED.

И снова всё упирается в статистику. Чтобы оценить размер build-стороны, CBO нужны row count и data size этой таблицы. Нет статистики — нет надёжной оценки размера — AUTOMATIC выбирает вслепую и может промахнуться: назначить BROADCAST таблице, которая на деле велика.

Spark: broadcast hints и автоматический broadcast join
WARNING

Опасный сценарий — BROADCAST большой таблицы из-за устаревшей статистики. CBO оценил build-сторону как маленькую (статистика говорит про вчерашний объём), выбрал BROADCAST, разослал на каждый воркер таблицу, которая в память ноды не влезает, — запрос падает с out of memory. Симптом узнаваем: join валится по памяти, хотя кластер по объёму данных должен справляться. Лечение по порядку: сначала ANALYZE затронутых таблиц, чтобы CBO видел реальные размеры; если это срочно и нужно обойти прямо сейчас — SET SESSION join_distribution_type = 'PARTITIONED' для конкретного запроса. Принудительный PARTITIONED — безопасный аварийный режим: он медленнее из-за шаффла, но не падает по памяти на больших таблицах.


Как увидеть тип распределения

Тип распределения каждого джойна виден в EXPLAIN — у join-узла указан distribution type. Полезно сравнить план до и после ANALYZE или при разных значениях join_distribution_type:

EXPLAIN
SELECT o.orderkey, c.name
FROM tpch.sf100.orders o
JOIN tpch.sf1.customer c ON o.custkey = c.custkey;

В плане у join-узла будет помечен тип. Здесь orders из sf100 — крупная (probe-сторона), customer из sf1 — небольшая. CBO, видя по статистике, что customer мала, скорее всего выберет для неё REPLICATED (так в плане обозначается build-сторона broadcast join’а): маленький customer рассылается на все ноды, большой orders остаётся на месте. Если бы обе таблицы были крупными, в плане стоял бы PARTITIONED с шаффлом обеих.


Попробуй сам

На песочнице курса (Trino 481):

  1. Возьмите EXPLAIN для join tpch.sf100.orders с tpch.sf1.customer по custkey. Найдите join-узел и определите тип распределения. Объясните, какая таблица стала build-стороной и почему CBO выбрал именно этот тип.

  2. Выполните тот же EXPLAIN дважды: сначала с SET SESSION join_distribution_type = 'BROADCAST';, затем с 'PARTITIONED'. Сравните планы. Опишите, что в каждом случае происходит с probe-стороной — двигается она по сети или нет. Верните AUTOMATIC.

  3. Рассуждение в двух абзацах. Первый: почему BROADCAST дешевле по сети, но падает по памяти на большой build-стороне — распишите, что именно копируется и куда. Второй: команда жалуется, что join валится с out of memory, хотя кластер по объёму данных должен справляться; назовите вероятную причину и два шага лечения.


Проверка знанийKnowledge check
Чем BROADCAST join отличается от PARTITIONED join, в чём компромисс между ними, и почему BROADCAST большой таблицы из-за устаревшей статистики приводит к падению по памяти?
ОтветAnswer
Hash join работает с build-стороной (по ней строится хэш-таблица в памяти, ею делают меньшую таблицу) и probe-стороной (по ней идёт поток строк с поиском совпадений, она стримится и может быть огромной). При BROADCAST join build-сторона целиком копируется на каждый воркер: каждая нода получает полную копию build-таблицы и строит из неё локальную хэш-таблицу, а probe-сторона остаётся на месте и не двигается по сети. При PARTITIONED join обе таблицы перераспределяются по сети по хэшу join-ключа: одинаковый хэш применяется к строкам обеих таблиц, поэтому строки с равным ключом попадают на один воркер, и каждый воркер держит только свою долю build-стороны. Компромисс такой. BROADCAST дёшев по сети — большая probe-сторона не перемещается, по сети уходит только build, умноженная на число нод, — но build-сторона целиком держится в памяти каждого воркера, поэтому BROADCAST работает только когда build-сторона мала. PARTITIONED дороже — это полноценный шаффл обеих таблиц, гигабайты по сети, — но каждый воркер хранит лишь свою долю build-стороны, суммарная память кластера складывается, и можно джойнить большие таблицы с большими, чего BROADCAST не может. Управляет выбором свойство join-distribution-type: в режиме AUTOMATIC CBO оценивает размер build-стороны по статистике и выбирает BROADCAST, если он не превышает порог join-max-broadcast-table-size (по умолчанию 100 МБ), иначе PARTITIONED. BROADCAST большой таблицы из-за устаревшей статистики падает по памяти, потому что CBO оценивает build-сторону как маленькую (статистика отражает старый объём), выбирает BROADCAST и рассылает на каждый воркер таблицу, копия которой не влезает в память ноды, — отсюда out of memory. Лечение: собрать актуальную статистику через ANALYZE, а как срочный обход — принудительно задать join_distribution_type = PARTITIONED, который медленнее из-за шаффла, но не падает по памяти на больших таблицах.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что происходит при BROADCAST join?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6