Bucket + Partition Design
В Уроке 01 мы видели, что данные в Paimon хранятся как LSM-деревья, а в Уроке 02 — что primary key определяет, как merge engine объединяет записи. Теперь вопрос: как Paimon распределяет записи по физическим файлам? Ответ — система из двух уровней: партиции (логическая сегментация по бизнес-атрибутам) и бакеты (физические единицы параллелизма внутри партиции).
Двухуровневая организация данных
Каждая таблица в Paimon делится на партиции (опционально) и бакеты (обязательно для primary key таблиц с фиксированным числом). Бакет — это отдельное LSM-дерево, которое обслуживается независимым writer thread’ом.
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):
- Вычислить partition — по partition key (если задан)
- Вычислить bucket —
hash(primary_key_columns) % N - Записать в LSM-дерево бакета
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) обработает дубликат.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 операции невозможно. Два типа ошибок:
Правило: целевой размер 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.
Один атомарный snapshot: DELETE + INSERT
Атомарность: Paimon выполняет delete в старой партиции и insert в новой как одну транзакцию (один snapshot). Нет промежуточного состояния, где запись существует в двух партициях или ни в одной. Cross-partition update требует параметр 'merge-engine' != 'none'.Cross-partition update — дорогая операция. Paimon должен найти старую партицию, сгенерировать DELETE, записать INSERT в новую партицию. Для стриминговых таблиц с высоким throughput это создаёт дополнительный overhead. Если partition key редко меняется (dt = дата создания заказа), проблемы нет. Если меняется часто (status = текущий статус) — используйте dynamic-bucket или пересмотрите схему партиционирования.
Dynamic Bucket
Фиксированное число бакетов (bucket = N) требует заранее знать распределение данных. Это ограничение: новая партиция начинается пустой, а hot partition может расти непредсказуемо. Dynamic bucket решает эту проблему.
Как работает маршрутизация в dynamic bucket? Paimon поддерживает in-memory index (primary key → bucket ID). При записи: lookup в индексе → если key найден, записать в существующий бакет. Если key новый и текущий бакет полон (достиг target-row-num) — создать новый бакет.
Dynamic bucket рекомендуется для стриминговых таблиц с неравномерным распределением данных. Типичный сценарий: event-таблица, партиционированная по дате, где объём данных меняется от дня к дню. Dynamic bucket адаптируется к каждой партиции индивидуально.
Unaware Bucket Mode
Для append-only таблиц (без primary key) Paimon предлагает bucket = -1 с особой семантикой: unaware-bucket mode. В этом режиме нет 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 таблицы:
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 не перехеширует существующие данные автоматически. Новые партиции используют новое число бакетов, но старые партиции сохраняют прежнее. Для полной миграции выполните INSERT OVERWRITE для каждой старой партиции — это перехеширует данные в новое число бакетов. На production-таблицах это может занять часы.
Партиционирование: стратегии и pruning
Партиции в Paimon аналогичны Hive-style партициям: каждая партиция — директория в object storage. Paimon поддерживает multi-level партиционирование (PARTITIONED BY (year, month, day)).
Manifest: фильтр по partition Отсечение на уровне planning IO к нерелевантным партициям
Paimon сканирует manifest (список snapshot'ов → data files). Manifest содержит partition info для каждого файла. Файлы с partition != '2025-03-27' отсекаются на уровне planning — ни один IO-запрос к ним не выполняется.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.Сравнение с другими форматами:
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
Стратегии партиционирования
Bucket Mode: сводная таблица решений
Есть 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).
Ключевые решения при проектировании таблицы:
-
Partition key — выбирайте атрибут с умеренной кардинальностью (дата, регион). Слишком высокая (user_id) → миллионы мелких партиций. Слишком низкая (country) → гигантские партиции без pruning.
-
Bucket mode — fixed (предсказуемый объём), dynamic (растущие данные), unaware (append-only).
-
Число бакетов — для fixed:
expected_partition_size / target_file_size. Для dynamic: настройтеdynamic-bucket-target-row-num. -
Cross-partition update — дорогая операция. Проектируйте partition key так, чтобы записи редко меняли партицию.
В следующем уроке мы рассмотрим, как Paimon управляет накопленными данными: компакция, deletion vectors, z-order сортировка, data skipping и snapshot lifecycle.