Learning Platform
Глоссарий Troubleshooting
Урок 15.04 · 40 мин
Продвинутый
Apache PaimonBucketHash BucketDynamic BucketUnaware BucketPartitionPartition PruningRescale BucketCross-Partition UpdatePrimary Key

Bucket + Partition Design

В Уроке 01 мы видели, что данные в Paimon хранятся как LSM-деревья, а в Уроке 02 — что primary key определяет, как merge engine объединяет записи. Теперь вопрос: как Paimon распределяет записи по физическим файлам? Ответ — система из двух уровней: партиции (логическая сегментация по бизнес-атрибутам) и бакеты (физические единицы параллелизма внутри партиции).

Двухуровневая организация данных

Каждая таблица в Paimon делится на партиции (опционально) и бакеты (обязательно для primary key таблиц с фиксированным числом). Бакет — это отдельное LSM-дерево, которое обслуживается независимым writer thread’ом.

Партиция → Бакет → LSM-дерево: иерархия хранения
Таблица: ordersPaimon-таблица с партиционированием по дате и фиксированными бакетами. Каждая комбинация partition + bucket = отдельное LSM-дерево. Primary key определяет маршрутизацию записей внутри партиции.
разделение по partition key
Partition: 2025-03-25Все заказы за 25 марта. Внутри — 4 бакета (bucket-0 .. bucket-3). Запись маршрутизируется в бакет по hash(order_id) % 4.
Partition: 2025-03-26Все заказы за 26 марта. Те же 4 бакета. Каждый бакет — независимое LSM-дерево: MemTable → sorted runs → SST-файлы (Parquet). Компакция в каждом бакете автономна.
Partition: 2025-03-27Последняя партиция. Активно принимает записи. Бакеты независимо flush'ат MemTable и запускают компакцию. Четыре LSM-дерева работают параллельно.
hash(order_id) % 4

Bucket-0 LSM Tree

Bucket-0: LSM-дерево для записей, где hash(order_id) % 4 = 0. Свои MemTable, sorted runs, SST-файлы. Компакция и snapshot — независимые. Writer thread обслуживает только этот бакет.

Bucket-1 LSM Tree

Bucket-1: записи с hash(order_id) % 4 = 1. Полностью изолирован от bucket-0. Нет shared state между бакетами — максимальный параллелизм записи.

Bucket-2 LSM Tree

Bucket-2: записи с hash(order_id) % 4 = 2. Каждый бакет — это директория в object storage: /partition=2025-03-27/bucket-2/. Внутри — SST-файлы (Parquet), manifest, changelog.

Bucket-3 LSM Tree

Bucket-3: записи с hash(order_id) % 4 = 3. Если запись с тем же order_id приходит повторно — merge engine обработает дедупликацию внутри этого бакета.

Почему бакеты? Primary key таблица должна уметь найти существующую запись при upsert. В Delta Lake и Iceberg это делается через полный scan или вспомогательные индексы. В Hudi — через Bloom/Record Index. Paimon использует хеширование: запись с primary key K всегда попадает в бакет hash(K) % num_buckets. Merge engine внутри бакета (LSM-дерево) обеспечивает корректное объединение.

Hash Bucket: механика назначения

Для primary key таблицы с параметром bucket = N (N > 0):

  1. Вычислить partition — по partition key (если задан)
  2. Вычислить buckethash(primary_key_columns) % N
  3. Записать в LSM-дерево бакета
Hash-назначение: запись → bucket
Запись: order_id=12345dt=2025-03-27Входная запись: order_id=12345, dt='2025-03-27', amount=99.50. Primary key = order_id. Partition key = dt. Запись сначала маршрутизируется в партицию dt=2025-03-27, затем — в бакет.
hash(order_id) % 4= hash(12345) % 4= bucket-3Hash-функция: MurmurHash3 от primary key колонок (исключая partition key). hash(12345) = 0x7A3B... → 0x7A3B % 4 = 3. Запись маршрутизируется в bucket-3. Primary key полностью определяет бакет — повторная запись с тем же order_id попадёт в тот же бакет.

Partition: 2025-03-27 Bucket-3 → LSM Tree

Bucket-3 в партиции 2025-03-27. LSM-дерево принимает запись: MemTable → sorted run → SST. Если order_id=12345 уже существует — merge engine (deduplicate / partial-update / aggregate) обработает дубликат.
NOTE

Primary key в Paimon включает partition key. Если вы определяете PRIMARY KEY (order_id, dt) NOT ENFORCED с PARTITIONED BY (dt), то бакет вычисляется по hash(order_id) — partition-колонки исключаются из hash-вычисления, потому что маршрутизация по партициям уже выполнена.

Детерминизм: одна и та же запись всегда попадает в один и тот же бакет. Это критично для merge engine: при повторном upsert запись order_id=12345 снова окажется в bucket-3, где LSM-дерево найдёт предыдущую версию и выполнит merge. Если бы запись попала в другой бакет — merge engine не смог бы найти предыдущую версию, и возникли бы дубликаты.

Как выбрать число бакетов?

Число бакетов bucket = N — это фиксированная настройка при создании таблицы. Изменить N без rescale операции невозможно. Два типа ошибок:

Бакетов слишком мало vs слишком много
Слишком мало бакетов (bucket=2)2 бакета на партицию с 10M записей → 5M записей/бакет. Проблемы: 1) LSM-дерево раздувается — больше sorted runs, тяжелее компакция. 2) Параллелизм ограничен 2 writer threads. 3) Большие SST-файлы замедляют IO.
Слишком много бакетов (bucket=1024)1024 бакета на партицию с 10M записей → ~10K записей/бакет. Проблемы: 1) 1024 LSM-дерева с маленькими SST-файлами — object storage LIST overhead. 2) Мелкие файлы: Parquet менее эффективен на <1MB. 3) Checkpoint: Flink должен snapshot'ить 1024 state backend'а.

Правило: целевой размер SST-файла — 128–256 MB (по умолчанию target-file-size = 128MB). Если партиция содержит ~10 GB данных, то 10 GB / 128 MB ≈ 80 бакетов — разумная отправная точка. На практике 4–128 бакетов покрывают большинство сценариев.

Cross-Partition Update

Что происходит, когда запись меняет партицию? Пример: заказ order_id=12345 был в партиции dt=2025-03-25, но бизнес-логика переносит его в dt=2025-03-27.

Cross-Partition Update: delete + insert
Старая партиция: 2025-03-25Запись order_id=12345 существует в bucket-3 партиции 2025-03-25. При cross-partition update Paimon генерирует DELETE (-D) для этой записи: удаляет из старой партиции. LSM-дерево bucket-3 добавляет tombstone marker.
cross-partition
Новая партиция: 2025-03-27Paimon генерирует INSERT (+I) для order_id=12345 в новой партиции. Запись маршрутизируется в bucket-3 (hash тот же). LSM-дерево принимает новую запись. Два бакета в разных партициях модифицируются атомарно в одном snapshot.

Один атомарный snapshot: DELETE + INSERT

Атомарность: Paimon выполняет delete в старой партиции и insert в новой как одну транзакцию (один snapshot). Нет промежуточного состояния, где запись существует в двух партициях или ни в одной. Cross-partition update требует параметр 'merge-engine' != 'none'.
WARNING

Cross-partition update — дорогая операция. Paimon должен найти старую партицию, сгенерировать DELETE, записать INSERT в новую партицию. Для стриминговых таблиц с высоким throughput это создаёт дополнительный overhead. Если partition key редко меняется (dt = дата создания заказа), проблемы нет. Если меняется часто (status = текущий статус) — используйте dynamic-bucket или пересмотрите схему партиционирования.

Dynamic Bucket

Фиксированное число бакетов (bucket = N) требует заранее знать распределение данных. Это ограничение: новая партиция начинается пустой, а hot partition может расти непредсказуемо. Dynamic bucket решает эту проблему.

Dynamic Bucket: автоматическое масштабирование
bucket = -1 (dynamic)Параметр bucket = -1 включает dynamic bucket mode. Paimon автоматически определяет количество бакетов для каждой партиции на основе объёма данных. Новая партиция начинается с 1 бакета и растёт по мере записи.
автоматическое масштабирование
Партиция A: 500K строкМаленькая партиция: 500K строк < target (2M). Paimon назначает 1 бакет. LSM-дерево обслуживает все 500K записей. Минимальный overhead.
Партиция B: 8M строкСредняя партиция: 8M строк / 2M target = 4 бакета. Paimon автоматически разделяет данные на 4 LSM-дерева. Каждый бакет обслуживает ~2M строк.
Партиция C: 50M строкГорячая партиция: 50M строк / 2M target = 25 бакетов. Автоматическое масштабирование: Paimon создаёт 25 LSM-деревьев. Параллелизм записи и чтения масштабируется с объёмом данных.

Как работает маршрутизация в dynamic bucket? Paimon поддерживает in-memory index (primary key → bucket ID). При записи: lookup в индексе → если key найден, записать в существующий бакет. Если key новый и текущий бакет полон (достиг target-row-num) — создать новый бакет.

TIP

Dynamic bucket рекомендуется для стриминговых таблиц с неравномерным распределением данных. Типичный сценарий: event-таблица, партиционированная по дате, где объём данных меняется от дня к дню. Dynamic bucket адаптируется к каждой партиции индивидуально.

Unaware Bucket Mode

Для append-only таблиц (без primary key) Paimon предлагает bucket = -1 с особой семантикой: unaware-bucket mode. В этом режиме нет hash-распределения — записи просто назначаются в файлы по мере поступления.

Unaware Bucket: append-only без hash-распределения

Append-Only Table Primary Key

Append-only таблица: нет primary key, нет upsert, нет merge. Записи только добавляются. Пример: raw event log, аудит-трейл, IoT-телеметрия. Для таких таблиц hash-бакеты избыточны.

Writer назначает файл Без hash-функции Без LSM-merge

Unaware bucket mode: записи назначаются в файлы writer'ами без hash-функции. Каждый writer создаёт свои файлы. Нет LSM-merge — файлы иммутабельны после flush. Компакция — только объединение мелких файлов в крупные.

Parquet файлы Без bucket-структуры

Результат: файлы Parquet, организованные по партициям. Внутри партиции — нет бакетов. Файлы создаются по мере поступления. Подходит для append-heavy workloads где data skipping через min/max статистику важнее, чем key-based routing.

Отличия unaware-bucket от primary key таблицы:

Primary Key + Hash Bucket vs Append-Only + Unaware Bucket
Primary Key + Fixed/Dynamic BucketPrimary key таблица: hash-routing, LSM-merge, upsert, delete, merge engines (deduplicate/partial-update/aggregate). Бакет = единица параллелизма, гарантирует co-location записей с одинаковым PK. Стоимость: hash overhead, state management для routing.
Append-Only + Unaware BucketAppend-only таблица: нет hash, нет LSM-merge, нет upsert. Записи распределяются round-robin по writer'ам. Читатели используют min/max статистику для data skipping. Максимальный throughput записи — нет overhead routing.
NOTE

Unaware-bucket mode поддерживает deletion vectors (Paimon 1.0+) — маркеры удалённых строк внутри Parquet-файлов. Это позволяет DELETE-операции на append-only таблицах без перезаписи файлов, но без upsert-семантики (нет merge engine).

Rescale Bucket

Что если вы выбрали bucket = 4, а данные выросли в 10x? Rescale bucket — операция изменения числа бакетов. Paimon поддерживает её начиная с версии 0.8.

Rescale Bucket: увеличение с 4 до 16 бакетов
До: bucket = 4Исходная конфигурация: 4 бакета. Каждый обслуживает ~2.5M записей из 10M. SST-файлы стали слишком большими (>500MB), компакция медленная, write throughput деградирует.
ALTER TABLE SET bucket = 16
После: bucket = 16Новая конфигурация: 16 бакетов. НОВЫЕ партиции создаются с 16 бакетами. Старые партиции НЕ перехешируются автоматически — данные в них остаются в 4 бакетах. Для перехеширования старых партиций нужна compaction операция (INSERT OVERWRITE SELECT *).
WARNING

Rescale bucket не перехеширует существующие данные автоматически. Новые партиции используют новое число бакетов, но старые партиции сохраняют прежнее. Для полной миграции выполните INSERT OVERWRITE для каждой старой партиции — это перехеширует данные в новое число бакетов. На production-таблицах это может занять часы.

Партиционирование: стратегии и pruning

Партиции в Paimon аналогичны Hive-style партициям: каждая партиция — директория в object storage. Paimon поддерживает multi-level партиционирование (PARTITIONED BY (year, month, day)).

Partition Pruning: запрос читает только релевантные партиции
SELECT * FROM ordersWHERE dt = '2025-03-27'SQL-запрос с фильтром по partition key. Paimon анализирует WHERE-условие и определяет набор релевантных партиций ДО обращения к данным. Partition pruning — первый уровень фильтрации, исключает целые директории.
partition pruning

Manifest: фильтр по partition Отсечение на уровне planning IO к нерелевантным партициям

Paimon сканирует manifest (список snapshot'ов → data files). Manifest содержит partition info для каждого файла. Файлы с partition != '2025-03-27' отсекаются на уровне planning — ни один IO-запрос к ним не выполняется.
только dt=2025-03-27

Bucket-0

Bucket-0 в партиции 2025-03-27. Читается полностью (или с data skipping по min/max stats). Partition pruning сократил scan с 365 партиций до 1.

Bucket-1

Bucket-1 в партиции 2025-03-27. Все бакеты релевантной партиции читаются параллельно — каждый бакет = один split для execution engine (Flink/Spark).

Bucket-2

Bucket-2 в партиции 2025-03-27.

Bucket-3

Bucket-3 в партиции 2025-03-27. Итого: 4 split'а для параллельного чтения вместо 4 × 365 = 1460 split'ов без pruning.

Сравнение с другими форматами:

Partition pruning: Paimon vs Delta Lake vs Iceberg vs Hudi
Delta LakeDelta Lake: Hive-style партиции (директории). Partition pruning через delta_log statistics. Также data skipping через column-level min/max в transaction log.
Apache IcebergIceberg: partition metadata в manifest files. Hidden partitioning — partition transform (day, month, bucket, truncate) не виден в данных. Partition evolution без rewrite.
Apache HudiHudi: Hive-style партиции + Metadata Table с file listing. Partition pruning + record-level indexing (Bloom, HFile). Более гранулярный: index указывает конкретный FileGroup.
Apache PaimonPaimon: Hive-style партиции + bucket hash routing. Двухуровневый pruning: 1) partition pruning (исключить партиции), 2) bucket pruning (если фильтр по PK — вычислить hash → один конкретный бакет). Уникальная возможность: point lookup за O(1) через hash.

Bucket pruning — уникальная особенность Paimon. Если запрос содержит фильтр по primary key, Paimon может вычислить hash(PK) % N и прочитать единственный бакет вместо всех N:

-- Point lookup: partition pruning + bucket pruning
SELECT * FROM orders
WHERE dt = '2025-03-27' AND order_id = 12345;
-- Paimon читает: 1 партиция × 1 бакет × LSM-lookup
-- Вместо: 365 партиций × 4 бакета × full scan

Стратегии партиционирования

Рекомендации по выбору partition + bucket
Стриминговый ingestion (CDC)CDC из OLTP: записи с upsert/delete. Partition по дате события (created_at). Bucket: dynamic (-1) если объём непредсказуем, fixed (4-32) если стабилен. Merge engine: deduplicate или partial-update.
Event streaming (IoT, logs)Append-only события без PK. Partition по дате/час. Bucket: unaware (-1 без PK) — максимальный throughput. Нет merge overhead. Data skipping через min/max.
Batch ETL (ежедневная загрузка)Крупные batch-записи раз в день. Partition: дата загрузки. Bucket: fixed (расчёт по объёму: data_size / target_file_size). INSERT OVERWRITE = полная перезапись партиции.
Dimension table (медленно меняющаяся)Таблица-справочник: маленькая (тысячи - миллионы записей), редкие update. Partition: нет (unpartitioned). Bucket: fixed (1-4). Merge engine: deduplicate. Streaming lookup join.

Bucket Mode: сводная таблица решений

Выбор bucket mode: decision tree

Есть Primary Key?

Первый вопрос: есть ли у таблицы primary key? Если да — нужен hash routing для корректного merge. Если нет — append-only, hash routing не нужен.

→ Unaware Bucket bucket = -1

Нет primary key → append-only таблица. Единственный вариант: unaware bucket (bucket = -1). Записи распределяются по writer'ам без hash. Нет merge engine.

→ Объём предсказуем?

Есть primary key. Следующий вопрос: объём данных предсказуем? Если да — fixed bucket. Если растёт непредсказуемо — dynamic bucket.

→ Fixed Bucket bucket = N

Предсказуемый объём → fixed bucket (bucket = N). Вычислите N: data_per_partition / target_file_size. Преимущество: стабильный параллелизм, предсказуемая компакция. Bucket pruning по PK.

→ Dynamic Bucket bucket = -1

Непредсказуемый объём → dynamic bucket (bucket = -1 + PK). Paimon масштабирует бакеты автоматически. Параметр: dynamic-bucket-target-row-num. Overhead: in-memory index для PK → bucket mapping.

Подводим итоги

Двухуровневая система партиционирования Paimon — partition + bucket — решает две задачи одновременно: логическая организация данных (partition по бизнес-атрибутам) и физический параллелизм (bucket как единица LSM-дерева и writer thread).

Ключевые решения при проектировании таблицы:

  1. Partition key — выбирайте атрибут с умеренной кардинальностью (дата, регион). Слишком высокая (user_id) → миллионы мелких партиций. Слишком низкая (country) → гигантские партиции без pruning.

  2. Bucket mode — fixed (предсказуемый объём), dynamic (растущие данные), unaware (append-only).

  3. Число бакетов — для fixed: expected_partition_size / target_file_size. Для dynamic: настройте dynamic-bucket-target-row-num.

  4. Cross-partition update — дорогая операция. Проектируйте partition key так, чтобы записи редко меняли партицию.

В следующем уроке мы рассмотрим, как Paimon управляет накопленными данными: компакция, deletion vectors, z-order сортировка, data skipping и snapshot lifecycle.

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Paimon-таблица: PRIMARY KEY (order_id, dt) NOT ENFORCED, PARTITIONED BY (dt), bucket = 8. Запрос: SELECT * FROM orders WHERE dt = '2025-03-27' AND order_id = 12345. Сколько бакетов Paimon прочитает?

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

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

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

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