Learning Platform
Глоссарий Troubleshooting
Урок 13.03 · 40 мин
Продвинутый
Apache IcebergHidden PartitioningPartition EvolutionTransformBucketTruncatePartition PruningMulti-SpecPartition Statistics

Hidden partitioning и partition evolution

В предыдущих уроках мы разобрали иерархию метаданных и механику снимков. Теперь — одна из самых сильных сторон Iceberg: hidden partitioning (скрытое партиционирование) и partition evolution (эволюция партиционирования).

В Hive и Delta Lake пользователь явно указывает partition-колонки, и структура партиций видна в файловой системе. Iceberg полностью скрывает партиционирование от пользователя — запросы пишутся по оригинальным колонкам, а движок автоматически применяет partition pruning.

NOTE

Примеры кода — на Python с pyiceberg 0.11.1 (март 2026). Версия спецификации — V2.

Проблема явного партиционирования

В Hive и Delta Lake партиционирование — явное: пользователь создаёт partition-колонку, данные раскладываются в директории, и запросы должны явно фильтровать по partition-колонке.

-- Hive / Delta Lake: явное партиционирование
CREATE TABLE orders (
 order_id BIGINT,
 amount DECIMAL(10,2),
 order_ts TIMESTAMP
) PARTITIONED BY (order_date DATE); -- пользователь создал колонку

-- Запрос ДОЛЖЕН использовать partition-колонку
SELECT * FROM orders WHERE order_date = '2025-01-15'; -- OK, partition pruning
SELECT * FROM orders WHERE order_ts >= '2025-01-15'; -- FULL SCAN! Движок не знает связь order_ts → order_date

Проблемы:

Проблемы явного партиционирования
Leaking abstractionПользователь должен знать, что таблица партиционирована по order_date, и использовать именно эту колонку в WHERE. Если фильтр по order_ts — полный скан, хотя данные те же. Физическое хранение утекает в SQL-запросы.
Связанные колонкиПриходится поддерживать дополнительную колонку order_date, которая вычисляется из order_ts. Данные дублируются. INSERT должен заполнять обе колонки — ошибки при несоответствии.
Невозможна эволюцияЕсли нужно изменить партиционирование (с month на day), нужно перезаписать ВСЮ таблицу. Для петабайтной таблицы это дни/недели работы и downtime. Delta Lake не поддерживает изменение partition columns.
Silent correctness bugsПользователь пишет WHERE order_date = '2025-01-15' AND order_ts < '2025-01-15 06:00:00'. Движок читает весь partition '2025-01-15', хотя нужна только часть. Нет sub-partition pruning по оригинальной колонке.
Мелкие файлыПартиционирование по часу при небольшом объёме данных создаёт тысячи мелких файлов (small file problem). Решение: перепартиционировать на день — но это перезапись.
Несовместимые движкиHive создаёт директории year=2025/month=01/day=15/. Spark ожидает такую структуру. Presto/Trino — тоже, но с нюансами. Каждый движок парсит пути по-своему — несовместимости на уровне encoding.

Hidden partitioning — решение Iceberg

В Iceberg партиционирование скрыто от пользователя. Вместо partition-колонок используются transforms — функции, применяемые к существующим колонкам:

Hidden partitioning: пользователь не видит партиции
SELECT * FROM orders WHERE order_ts >= '2025-01-15'Пользователь пишет запрос по оригинальным колонкам. Не знает и не должен знать, как таблица партиционирована. SQL не содержит partition-колонок.
Iceberg движок

Автоматический partition pruning:

month(order_ts) >= '2025-01'Движок знает partition spec: month(order_ts). Автоматически вычисляет: если order_ts >= '2025-01-15', то month(order_ts) >= '2025-01'. Применяет manifest pruning + data file pruning по partition values.
Пропущены +Manifest files и data files с partition value < '2025-01' пропускаются. Manifest pruning на уровне manifest list + file pruning на уровне manifest file.
ПрочитаныЧитаются только файлы с partition value >= '2025-01'. Дополнительно, column-level min/max bounds (data skipping) отсекают файлы внутри января, где upper_bound(order_ts) < '2025-01-15'.

Ключевые преимущества:

  1. Запросы по оригинальным колонкам — нет производных partition-колонок
  2. Автоматический pruning — движок знает связь между фильтром и partition transform
  3. Нет дублирования данных — partition values хранятся в метаданных (manifest file), не в данных
  4. Partition evolution — можно изменить партиционирование без перезаписи данных

Partition transforms

Iceberg определяет набор transforms — функций, применяемых к source-колонке для вычисления partition value:

Partition transforms в Apache Iceberg
identityPartition value = значение колонки без изменений. Аналог обычного партиционирования в Hive. Используется для low-cardinality колонок (region, status, category).
yearИзвлекает год из timestamp/date. Partition value = число лет с Unix epoch (1970). Грубая гранулярность — для таблиц с данными за много лет.
monthИзвлекает год+месяц из timestamp/date. Partition value = число месяцев с Unix epoch. Самая частая гранулярность для аналитических таблиц.
dayИзвлекает дату из timestamp. Partition value = число дней с Unix epoch. Подходит для таблиц с высоким объёмом ежедневных данных (логи, события).
hourИзвлекает дату+час из timestamp. Partition value = число часов с Unix epoch. Самая мелкая временная гранулярность. Для streaming-таблиц с миллионами записей в час.
bucket[N]Hash-партиционирование: murmur3_32(value) mod N. Равномерно распределяет данные по N бакетам. Используется для high-cardinality колонок (user_id, order_id) для предотвращения data skew.
truncate[W]Обрезает значение: для string — первые W символов, для int — округление вниз до ближайшего кратного W. truncate[10](1234) = 1230. Группирует близкие значения.
voidВсегда возвращает null. Используется при partition evolution: заменяет предыдущий transform, когда партиционирование по этой колонке больше не нужно. Все файлы попадают в одну 'партицию'.

Примеры использования transforms

from pyiceberg.catalog import load_catalog
from pyiceberg.transforms import (
 IdentityTransform,
 YearTransform,
 MonthTransform,
 DayTransform,
 HourTransform,
 BucketTransform,
 TruncateTransform,
)
from pyiceberg.table.partitioning import PartitionSpec, PartitionField
import pyarrow as pa

catalog = load_catalog("demo", type="sql",
 uri="sqlite:////tmp/iceberg_demo/catalog.db",
 warehouse="file:///tmp/iceberg_demo")

catalog.create_namespace_if_not_exists("analytics")

schema = pa.schema([
 ("event_id", pa.int64()),
 ("user_id", pa.int64()),
 ("event_ts", pa.timestamp("us", tz="UTC")),
 ("event_type", pa.string()),
 ("payload", pa.string()),
])

# Партиционирование: day(event_ts) + bucket[16](user_id)
# Движок автоматически вычисляет partition values из данных
table = catalog.create_table(
 "analytics.events",
 schema=schema,
 partition_spec=PartitionSpec(
 PartitionField(
 source_id=3, # event_ts (field id)
 field_id=1000,
 transform=DayTransform(),
 name="event_ts_day"
 ),
 PartitionField(
 source_id=2, # user_id (field id)
 field_id=1001,
 transform=BucketTransform(num_buckets=16),
 name="user_id_bucket"
 ),
 ),
)

print(f"Partition spec: {table.spec()}")

Как transforms работают с запросами

Для каждого transform Iceberg знает projection — как преобразовать предикат на source-колонке в предикат на partition value:

Предикат на sourceTransformPartition предикат
event_ts >= '2025-03-15 10:00'dayevent_ts_day >= 20166 (days since epoch)
event_ts >= '2025-03-15 10:00'monthevent_ts_month >= 662 (months since epoch)
user_id = 42bucket[16]user_id_bucket = murmur3(42) % 16 = 6
event_type = 'click'identityevent_type = 'click'
payload LIKE 'error%'truncate[5]payload_trunc = 'error'
TIP

Для bucket transform Iceberg может вычислить точный бакет для = предиката, но не может pruning для range-предикатов (user_id > 100). Bucket-партиционирование эффективно только для point lookups и JOIN-ов.

Per-file partition statistics

В manifest file каждая запись data_file содержит partition — struct со значениями partition полей для этого файла. Это позволяет двухуровневый pruning:

Двухуровневый partition pruning
WHERE event_ts >= '2025-03-15'Запрос: SELECT * FROM events WHERE event_ts >= '2025-03-15'. Движок начинает pruning с верхнего уровня — manifest list.
Уровень 1: Manifest list pruning
Manifest AManifest list содержит partition summary для каждого manifest: lower_bound(event_ts_day) и upper_bound(event_ts_day). Manifest A: days 20100-20160 — все данные до марта 2025.
Manifest BManifest B: days 20160-20180 — содержит данные в нужном диапазоне. Нужно прочитать этот manifest и проверить отдельные файлы.
Уровень 2: File-level pruning
File 1File 1 в manifest B: partition value event_ts_day = 20163 (12 марта). Не проходит фильтр ≥ 15 марта (day 20166).
File 2File 2: partition value event_ts_day = 20165 (14 марта). Не проходит фильтр ≥ 15 марта.
File 3File 3: partition value event_ts_day = 20166 (15 марта). Проходит фильтр. Дополнительно применяется data skipping по column min/max для точного отбора строк.

Partition statistics metadata table

Iceberg 1.10.x поддерживает incremental partition stats — агрегированная статистика по partition values, доступная через metadata table partition_stats:

-- Spark: просмотр partition statistics
SELECT partition, record_count, file_count, total_data_file_size_in_bytes
FROM db.events.partitions;
# pyiceberg: просмотр через plan_files
scan = table.scan()
partition_stats = {}
for task in scan.plan_files():
 pv = str(task.file.partition)
 if pv not in partition_stats:
 partition_stats[pv] = {"files": 0, "records": 0, "bytes": 0}
 partition_stats[pv]["files"] += 1
 partition_stats[pv]["records"] += task.file.record_count
 partition_stats[pv]["bytes"] += task.file.file_size_in_bytes

for pv, stats in sorted(partition_stats.items()):
 print(f" {pv}: {stats['files']} files, "
 f"{stats['records']} records, "
 f"{stats['bytes'] / 1024 / 1024:.1f} MB")

Partition evolution

Partition evolution — возможность изменить партиционирование без перезаписи данных. Это уникальная особенность Iceberg, отсутствующая в Delta Lake и Hive.

Как это работает

Каждый partition spec имеет spec-id. Manifest files ссылаются на partition-spec-id. При partition evolution:

  1. Создаётся новый partition spec с новым spec-id
  2. Новый spec становится default-spec-id в metadata file
  3. Новые data files записываются с новым partition spec
  4. Старые data files остаются как есть — их manifest files ссылаются на старый spec
Partition evolution: month → day без перезаписи

Spec 0: month(event_ts)

Partition spec 0: month(event_ts). Все файлы, записанные до эволюции, используют этот spec. Каждый файл содержит данные за один месяц.
Старые файлыData files, записанные с partition spec 0. Partition value = месяц (int). Например: 662 (январь 2025), 663 (февраль 2025). Эти файлы НЕ перезаписываются.

ALTER TABLE ADD PARTITION FIELD day(event_ts) DROP PARTITION FIELD month(event_ts)

Команда partition evolution: добавляем day transform. Iceberg создаёт новый partition spec с spec-id=1. Это мгновенная операция — только изменение metadata.

Spec 1: day(event_ts)

Partition spec 1: day(event_ts). Все новые файлы используют этот spec. Каждый файл содержит данные за один день.
Новые файлыData files, записанные после эволюции. Partition value = день (int). Например: 20166 (15 марта 2025), 20167 (16 марта). Мельче гранулярность — лучше pruning для новых данных.
Обе группы сосуществуютПри сканировании Iceberg читает manifest files с обоими spec-id. Для каждого manifest применяется соответствующий partition spec: month для старых файлов, day для новых. Все файлы доступны через один table scan.

Partition evolution через pyiceberg

# Текущий partition spec
print(f"Current spec: {table.spec()}")
# PartitionSpec(day(3, 'event_ts_day', 1000), bucket[16](2, 'user_id_bucket', 1001))

# Эволюция: добавить hour вместо day
with table.update_spec() as update:
 update.remove_field("event_ts_day") # убрать day
 update.add_field(
 source_column_name="event_ts",
 transform=HourTransform(),
 name="event_ts_hour"
 )

print(f"New spec: {table.spec()}")
# PartitionSpec(void(3, 'event_ts_day', 1000), bucket[16](2, 'user_id_bucket', 1001), hour(3, 'event_ts_hour', 1002))

# Обратите внимание: day transform заменён на void (а не удалён),
# чтобы старые manifest files оставались валидными
WARNING

При удалении partition field старый transform заменяется на void, а не удаляется. Это гарантирует, что manifest files, ссылающиеся на старый spec, остаются валидными. Поле в partition struct остаётся (с null значением), но не влияет на pruning.

Типичные сценарии partition evolution

СценарийЭволюцияПричина
Рост данныхmonth → dayМесячные партиции стали слишком большими (10+ GB), нужна мельче гранулярность
Уменьшение данныхday → monthДневные партиции содержат мало данных (< 100 MB), small file problem
Streamingday → hourStreaming ingestion создаёт много файлов; часовая гранулярность балансирует pruning и file count
Добавление bucketдобавить bucket[N](user_id)Данные skewed по user_id, нужно равномерное распределение для JOIN
Отключениеday → voidПартиционирование по этой колонке больше не нужно

Multi-spec read path

При чтении Iceberg обрабатывает manifest files с разными partition specs прозрачно для пользователя:

Multi-spec read path: pruning для разных specs
WHERE event_ts >= '2025-03-15 10:00:00'Запрос: WHERE event_ts >= '2025-03-15 10:00:00'. Нужно отфильтровать данные по timestamp. Таблица имеет два partition spec-а: month и day (после эволюции).
Manifests (spec 0: month)Manifest files со старым spec (month). Iceberg проецирует предикат: event_ts >= '2025-03-15' → month >= 662 (март 2025). Файлы с month < 662 пропускаются. Файлы с month = 662 читаются полностью (pruning грубый).
РезультатПропущены: январь и февраль 2025. Прочитаны: март 2025 (весь месяц — 6 файлов). Дополнительно data skipping по column min/max отсекает записи до 15 марта внутри файлов.
Manifests (spec 1: day)Manifest files с новым spec (day). Проекция: event_ts >= '2025-03-15' → day >= 20166. Точный pruning — файлы до 15 марта пропускаются.
РезультатПропущены: 12-14 марта. Прочитаны: 15-17 марта (6 файлов). Мельче гранулярность нового spec даёт лучший pruning для свежих данных.

Объединение результатов: 12 файлов прочитано

Результаты обеих ветвей объединяются. Итого: 12 файлов из потенциальных ~20. Multi-spec read path прозрачен для пользователя — один запрос, один результат.

Оптимизация старых данных (опционально)

После partition evolution можно опционально перезаписать старые файлы с новым partition spec для улучшения pruning:

# Spark: перезапись старых файлов с новым partition spec
# CALL system.rewrite_data_files(
# table => 'db.events',
# where => 'event_ts < "2025-03-01"'
# )
#
# Это НЕ обязательно — таблица работает корректно с обоими specs.
# Перезапись — оптимизация для улучшения pruning старых данных.
TIP

Перезапись старых файлов после partition evolution — опциональная оптимизация, а не требование. Iceberg корректно работает с несколькими partition specs одновременно. Перезаписывайте только если pruning старых данных критичен для производительности запросов.

Projection и residual predicates

Не все предикаты полностью покрываются partition pruning. Оставшаяся часть — residual predicate — применяется при чтении data files:

Запрос: WHERE event_ts >= '2025-03-15 10:30:00'
Partition spec: day(event_ts)

Partition predicate: day >= 20166 (15 марта) → pruning
Residual predicate: event_ts >= '2025-03-15 10:30:00' → фильтр при чтении

Partition pruning пропускает файлы за 14 марта и раньше. Но файл с day=20166 (15 марта) содержит записи за весь день — с 00:00 до 23:59. Residual predicate >= 10:30 фильтрует строки при чтении этого файла.

# pyiceberg автоматически применяет row_filter
scan = table.scan(
 row_filter="event_ts >= '2025-03-15T10:30:00+00:00'"
)

# Можно посмотреть planned tasks
for task in scan.plan_files():
 print(f" File: {task.file.file_path}")
 print(f" Partition: {task.file.partition}")
 print(f" Records: {task.file.record_count}")
 # Residual filter применяется при to_arrow()

result = scan.to_arrow()
print(f"Rows after residual filter: {len(result)}")

Составные partition specs

Iceberg поддерживает несколько partition fields в одном spec:

# Пример: таблица событий с составным партиционированием
spec = PartitionSpec(
 PartitionField(
 source_id=3, # event_ts
 field_id=1000,
 transform=DayTransform(),
 name="event_ts_day"
 ),
 PartitionField(
 source_id=2, # user_id
 field_id=1001,
 transform=BucketTransform(num_buckets=16),
 name="user_id_bucket"
 ),
 PartitionField(
 source_id=4, # event_type
 field_id=1002,
 transform=IdentityTransform(),
 name="event_type"
 ),
)

При запросе с фильтром по нескольким колонкам Iceberg применяет pruning по всем partition fields одновременно:

SELECT * FROM events
WHERE event_ts >= '2025-03-15' -- pruning по day
 AND user_id = 42 -- pruning по bucket
 AND event_type = 'purchase' -- pruning по identity

Движок вычисляет: day >= 20166 AND bucket = 6 AND event_type = 'purchase', и читает только файлы, совпадающие по всем трём partition полям.

Составной partition pruning: day + bucket + identity
WHERE event_ts >= '2025-03-15' AND user_id = 42 AND event_type = 'purchase'Запрос фильтрует по трём колонкам. Iceberg применяет все три partition transforms одновременно, максимизируя pruning.
day pruningПропускает все файлы до 15 марта. Оставляет файлы за 15, 16, 17 марта.
bucket pruningИз оставшихся файлов пропускает 15 из 16 бакетов. Оставляет только bucket=6 (murmur3(42) mod 16).
identity pruningИз оставшихся файлов оставляет только event_type='purchase'. Пропускает click, view и другие типы событий.
Результат: ~1 файл из ~1000 (99.9% пропущено)Итог: из тысяч файлов остаются единицы. Три уровня pruning мультипликативно сокращают объём чтения. Для 1000 файлов: ~50 после day → ~3 после bucket → ~1 после identity.

Выбор partition strategy

Рекомендации по выбору transforms:

Тип данныхРекомендацияОбоснование
Временные ряды (логи, события)day(ts) или hour(ts)Естественная гранулярность для time-range запросов
Таблица фактов с датойmonth(date)Меньше файлов, достаточный pruning для аналитики
High-cardinality join keybucket[N](id)Равномерное распределение для JOIN-ов, N = предполагаемое число файлов
Low-cardinality категорияidentity(category)Точный pruning по значению
Строковый префиксtruncate[W](name)Группировка по первым W символам
# Пример: выбор bucket count
# Правило: N × средний размер партиции ≈ target file size (256-512 MB)
# Если таблица ~50 GB и target file size = 512 MB:
# 50 GB / 512 MB ≈ 100 файлов → bucket[16] даёт ~6 файлов на бакет
# Слишком мало файлов на бакет → увеличить до bucket[32] или bucket[64]
WARNING

Не переусердствуйте с bucket count. Слишком много бакетов создаёт small file problem (много мелких файлов). Слишком мало — недостаточный параллелизм при чтении. Начните с bucket[16] и корректируйте по результатам.

Сравнение с Delta Lake и Hive

АспектHiveDelta LakeApache Iceberg
Partition видимостьЯвные директорииЯвные колонкиСкрытые transforms
ЗапросыПо partition-колонкамПо partition-колонкамПо оригинальным колонкам
Transforms (generated columns)8 встроенных transforms
Автоматический pruning (только по partition path)ОграниченныйПолный (projection)
Partition evolutionНевозможнаНевозможнаМгновенная, без перезаписи
Multi-spec readДа, прозрачный
Bucket partitioningНативный, но жёсткий (Z-ORDER вместо)bucket[N] transform

Итоги

  1. Hidden partitioning — пользователь не видит партиции; запросы по оригинальным колонкам, pruning автоматический
  2. 8 transforms — identity, year, month, day, hour, bucket, truncate, void — покрывают все сценарии
  3. Двухуровневый pruning — manifest list (partition range summary) → manifest file (per-file partition values)
  4. Partition evolution — мгновенное изменение гранулярности (month→day, day→hour) без перезаписи данных
  5. Multi-spec read — старые файлы со старым spec + новые файлы с новым spec читаются прозрачно
  6. void transform — корректное отключение партиционирования по полю при эволюции
  7. Residual predicates — часть фильтра, не покрытая partition pruning, применяется при чтении data files

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Iceberg-таблица партиционирована `day(event_ts)`. Пользователь пишет запрос: `SELECT * FROM events WHERE event_ts > '2024-06-01'`. Пользователь НЕ указывает функцию `day()` в WHERE. Произойдёт ли partition pruning?

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

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

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

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