Apache Iceberg. Партицирование и эволюция партиций

Партицирование без боли

|

Давайте начнём с очевидного вопроса: что такое партицирование в контексте Iceberg?

Представьте таблицу с миллиардами строк — обычное дело в data lake. Допустим, вам нужны все продажи за прошлый вторник. Без партицирования движку запросов придётся сканировать каждый файл, чтобы найти их. Это не здорово. Это будет много сканирований, очень затратно и долго.

С партицированием данные организованы так, что движок может пропускать файлы, которые не имеют значения. На пути чтения это называется pruning (отсечение) — очень важная часть оптимизации работы движка.

Представьте разницу между аккуратным картотечным шкафом и гигантской кучей бумаг.

Ранние дни: жёсткие физические директории

Раньше партицирование означало создание физических директорий для каждого среза данных, например month=01, month=02 и так далее. Это работало, но было очень жёстким. Вы встраивали это в физическую структуру данных, и если вы хотели изменить схему партицирования… удачи! Пришлось бы перемещать данные в новые директории (или копировать, чтобы старое оставалось онлайн, пока новое поднимается).

Это много работы.

Iceberg: скрытое партицирование (hidden partitioning)

Iceberg использует другой подход — скрытое партицирование. Информация о партициях живёт в слое метаданных, конкретно в manifest‑файлах, а файлы данных могут физически находиться где угодно.

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

Это даёт довольно мощные преимущества.

Пример: таблица продаж

Давайте создадим таблицу продаж, партицированную по дате продажи и региону:

CREATE TABLE sales (    sale_id BIGINT,    sale_date DATE,    region STRING,    amount DECIMAL(10, 2))USING icebergPARTITIONED BY (months(sale_date), region);

Обратите внимание: мы используем не sale_date напрямую, а функцию months(sale_date). Это трансформация партиции (partition transform), встроенная в Iceberg. Она даёт эффективное отсечение на уровне месяцев без необходимости создавать партицию для каждой отдельной даты и без вывода отдельной колонки «месяц».

Вам не нужна дополнительная колонка — Iceberg обрабатывает трансформацию автоматически.

Как это выглядит в manifest‑файле

Вот упрощённый manifest‑файл для этой таблицы. В нём три записи, каждая указывает на отдельный файл данных. Первая запись указывает на файл file-0001.parquet в пути warehouse/sales/data/ в нашем S3‑бакете.

[  {    "data_file": "s3://warehouse/sales/data/file-0001.parquet",    "partition": {      "months (sale_date)": 627, // Март 2025      "region": "RU"      },    "record_count": 250000,    "file_size_in_bytes": 134217728,    "lower_bounds": { "sale_date": "2025-03-01", "amount": 1.00 },    "upper_bounds": { "sale_date": "2025-03-31", "amount": 9999.99 }  },  {    "data_file": "s3://warehouse/sales/data/file-0002.parquet",    "partition": {      "months (sale_date)": 627, // Март 2025      "region": "CH"      },    "record_count": 180000,    "file_size_in_bytes": 125829120,    "lower_bounds": { "sale_date": "2025-03-01", "amount": 5.00 },    "upper_bounds": { "sale_date": "2025-03-31", "amount": 8999.99 }  },  {    "data_file": "s3://warehouse/sales/data/file-0003.parquet",    "partition": {      "months (sale_date)": 628, // Апрель 2025      "region": "RU"    },      "record_count": 300000,      "file_size_in_bytes": 150994944,      "lower_bounds": { "sale_date": "2025-04-01", "amount": 2.00 },      "upper_bounds": { "sale_date": "2025-04-30", "amount": 7500.00 }  }]

Теперь посмотрим на значения партиций. Вместо хранения сырой даты продажи Iceberg хранит преобразованное значение: months(sale_date) = 627 (это март 2025) плюс регион RU, потому что оба поля были в объявлении партиции.

Когда вы запрашиваете таблицу продаж по конкретной дате, Iceberg точно знает, какую партицию проверять. Дата, которую вы хотите, находится в предикатах запроса, а метаданные говорят, какие даты будут (или, что важнее, не будут) в каждом файле данных.

Для запроса с предикатами «март 2025, регион RU» движок будет сканировать ровно один файл — тот самый верхний файл file-0001.parquet.

Встроенные трансформации партиций

В Iceberg есть множество встроенных трансформаций для времени:

  • years, months, days, hours — эффективны для временного партицирования.
  • bucket — хэширует данные для равномерного распределения, подходит для колонок с высокой кардинальностью (например, ID пользователей).
  • truncate — группирует похожие значения на основе строки, полезно для кодов продуктов.
-- Трансформации на основе времени years (timestamp_col)months (timestamp_col)days (timestamp_col)hours (timestamp_col) -- Трансформации высокой кардинальности-- bucket (N, col) - Разбить колонку на N бакетовbucket (8, '550e8400-e29b-41d4-a716-446655440000') = 3bucket (8, '123e4567-e89b-12d3-a456-426614174000') = 7 -- truncate (width, col) - обрезать строкиtruncate (3, 'TV-55-SAM-2023') = 'TV-5'truncate (3, 'TV-55-SAM-2025') = 'TV-5'truncate (3, 'TV-77-SAM-2025') = 'TV-7'

Вы не ссылаетесь на эти трансформации в своих запросах. Вы просто пишете WHERE‑условие при чтении, а Iceberg автоматически обрабатывает трансформации и отсечение на пути чтения.

Эволюция партиций (partition evolution)

А что, если нужно изменить партицирование? Это эволюция партиций, и очень вероятно, что вы будете делать это на таблице, уже заполненной данными и партициями.

В старых форматах таблиц такое изменение было головной болью. Вы выбирали структуру директорий (например, year/month/day), которая казалась хорошей в тот момент, и были привязаны к ней, пока не перестроите всю таблицу — снова физическое копирование данных.

С Iceberg всё просто. Вы можете эволюционировать партицирование в любой момент, когда эволюционируют ваши потребности, потенциально без перезаписей и, надеемся, без боли.

Пример: переход с месячного на дневное партицирование

Допустим, ваша таблица продаж растёт как сумасшедшая (отличная новость, хорошая работа!). Месячные партиции начинают становиться громоздкими, и вы хотите переключиться на дневные.

В Iceberg это простое обновление спецификации партиций:

ALTER TABLE sales DROP PARTITION FIELD sale_date;ALTER TABLE sales DROP PARTITION FIELD region;ALTER TABLE sales ADD PARTITION FIELD days(sale_date);ALTER TABLE sales ADD PARTITION FIELD region;

Ключевой момент: это изменение применяется только к новым данным. Старые месячные партиции (те файлы данных, которые уже были разделены по месяцам) останутся как есть, если вы не перепишете их (чего вам, вероятно, не хочется).

Таким образом, сканирование по старым месячным партициям, если в запросе есть предикат по дню, может быть не таким эффективным, как могло бы быть — всё равно придётся сканировать весь месячный файл. Но для новых данных, поступающих с новой схемой партицирования (по дням), будет более эффективное отсечение и сканирование.

На время запроса вы ничего об этом не знаете. Вы просто пишете WHERE‑условие, притворяясь, что ничего не знаете о партицировании. Всё это касается только определения самой таблицы.

Визуализация: запрос, охватывающий старые и новые партиции

Допустим, у нас есть запрос на продажи с 14 апреля по 9 мая, а мы переразбили партиции в начале мая. Апрель партицирован по месяцам, а с 1 мая — по дням.

Партиции включены в план запроса

PARTITIONED BY
months(sale_date)

PARTITIONED BY
days(sale_date)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  • Левая сторона (старые месячные партиции): нам придётся сканировать весь апрельский раздел, потому что мы хотим получить последние две недели — это всё, что у нас есть.
  • Правая сторона (дневные партиции): мы можем хирургически взять только первые девять партиций и проигнорировать десятую и последующие.

Сиреневые блоки показывают, какие партиции отсекаются и не сканируются. Салатовые — какие сканируются. Движок сканирует все эти разные партиции, сшивает результаты и выдаёт вам один связный набор результатов на выходе.

Как выбрать стратегию партицирования?

Хороший вопрос, и честный ответ, как всегда, — это зависит от ваших шаблонов запросов.

Партицирование — это в первую очередь оптимизация производительности, поэтому вы должны знать, как выглядят ваши запросы и какие SLA по производительности вам нужны для различных предикатов при чтении.

Временное партицирование

Если в ваших данных есть временные метки, временное партицирование — обычно самый безопасный вариант. Большинство аналитических запросов фильтруют по времени. Если вы когда‑либо использовали дашборд или анализировали бизнес‑данные, вы знаете, что там обычно есть дата. Это даёт хорошее отсечение и отправную точку.

Высокая кардинальность: bucket и truncate

Если у вас есть колонки с высокой кардинальностью, на помощь приходят две трансформации:

  • Bucket хэширует данные для равномерного распределения. Если вы всегда соединяете по customer_id и у вас очень много клиентов, а customer_id всегда присутствует в предикатах — разбейте на bucket’ы. Тогда строки с одинаковым customer_id окажутся в одном файле, что приведёт к лучшему отсечению и меньшему сканированию.
  • Truncate группирует похожие значения на основе строки. Если у вас есть код продукта, который начинается с общей строки, а затем идут цифры, вы можете обрезать его, скажем, до первых семи символов. Тогда похожие коды будут партицироваться вместе и окажутся в одном файле данных.

Обе трансформации сохраняют количество партиций управляемым, обеспечивая максимально эффективную фильтрацию и отсечение.

Осторожно: слишком много колонок

Вы можете партицировать по нескольким колонкам одновременно — иногда это полезно, но чревато последствиями. Если переборщить, вы получите много мелких партиций, а это ещё один способ вызвать проблему мелких файлов. Никто этого не хочет.

Размер файлов: золотая середина

Хорошее правило: стремитесь к партициям размером от 100 МБ до 1 ГБ. Слишком маленькие (значительно меньше 100 МБ) — много накладных расходов. Слишком большие — отсечение не работает, много I/O при чтении. Нужен компромисс.

Конечно, ваш опыт может отличаться. Продвинутые пользователи могут тонко настраивать, и есть случаи, когда люди перегибают в одну или другую стороны и всё равно получают отличные результаты. Но для начала правило «100 МБ–1 ГБ» вас не подведёт.

Итог

Партицирование в Iceberg не приковывает вас к одной схеме навсегда, как старые форматы. Вы можете эволюционировать стратегию партицирования по мере роста данных, и эта эволюция применяется к новым данным, поступающим со временем, сохраняя старые макеты на месте и позволяя бесшовно запрашивать всё.

Это гибко, эффективно, даёт отличную производительность и делает жизнь намного проще по мере масштабирования таблиц.


«Партицирование — это как сортировка носков по ящикам: если делать это с умом, вы всегда найдёте нужную пару за секунды. Если нет — придётся перерывать всю корзину.»


→ Вперед к упражнению по партицированию

← Назад к упражнению «Эволюция схемы»