Партиционирование, hidden partitioning, transforms и schema evolution
В уроке 3 мы видели, что Trino отбрасывает ненужные манифесты по диапазонам партиционирующих колонок и ненужные файлы по статистике — это и есть pruning. Партиционирование — главный рычаг, которым инженер управляет этим отсевом. Но партиционирование в Iceberg устроено принципиально иначе, чем в старом Hive: оно скрытое. А ещё в Iceberg и схему, и саму схему партиционирования можно менять на живой таблице, не переписывая данные. Этот урок объясняет механику hidden partitioning, разбирает transforms и показывает, почему schema evolution в Iceberg безопасна.
Iceberg: hidden partitioning и partition evolutionБоль партиционирования в Hive и как Iceberg её убирает
Сначала — как было в Hive, потому что Iceberg проектировали именно против этих проблем. В Hive партиция — это физическая директория, названная по значению колонки: /orders/order_date=2026-05-19/. Чтобы партиционировать таблицу по дате, в неё добавляли отдельную партиционирующую колонку, и у этого было три болезненных следствия.
Первое: партиционирующая колонка видна в схеме, и запросы должны фильтровать именно по ней. Если таблица партиционирована по колонке order_day, а запрос фильтрует WHERE order_date = ... по другой колонке, partition pruning не сработает — Hive прочитает всю таблицу. Пользователь обязан знать схему партиционирования и подстраивать запросы.
Второе: партиционирующее значение нужно вычислять и записывать руками. Хотите партиции по дню от колонки-timestamp — при вставке надо самому посчитать date(order_ts) и положить в order_day. Ошибётесь — данные лягут не в ту партицию.
Третье, и самое тяжёлое: схема партиционирования в Hive фиксирована при создании таблицы. Поняли через год, что партиции по дню слишком мелкие и нужны по месяцу — нет способа изменить это, кроме как создать новую таблицу и переписать в неё всё.
Hidden partitioning: партиция в метаданных, а не в директории
В Iceberg партиция — это не директория, а запись в метаданных. В уроке 3 мы видели: manifest list хранит для каждого манифеста диапазоны партиционирующих значений, манифест — партицию каждого файла. Партиционирование живёт в этом слое метаданных, а не в путях файлов. Это и называется hidden partitioning — скрытое партиционирование.
Из этого вытекает ключевое: партиционирующая колонка не нужна. Вы партиционируете таблицу не по вспомогательной колонке, а напрямую по выражению от обычной колонки. Например, по дню от order_ts. Iceberg сам при каждой записи вычисляет значение партиции из order_ts и записывает его в метаданные. Инженеру не надо ни добавлять колонку, ни считать значение руками — движок делает это сам.
И главный выигрыш на чтении. Запрос фильтрует по обычной колонке — WHERE order_ts >= TIMESTAMP '2026-05-19 00:00:00'. Trino знает из метаданных, что таблица партиционирована по дню от order_ts, применяет ту же функцию к границам фильтра и отбрасывает манифесты и файлы ненужных дней. Pruning срабатывает без того, чтобы пользователь знал схему партиционирования. Запрос написан естественно, а отсев работает. В Hive это было невозможно.
-- Iceberg: партиционируем по выражению от обычной колонки order_ts.
-- Отдельная партиционирующая колонка НЕ нужна.
CREATE TABLE iceberg.sales.events (
event_id BIGINT,
user_id BIGINT,
order_ts TIMESTAMP(6),
amount DECIMAL(12,2)
)
WITH (
format = 'PARQUET',
partitioning = ARRAY['day(order_ts)']
);
INSERT INTO iceberg.sales.events VALUES
(1, 100, TIMESTAMP '2026-05-19 09:14:00', DECIMAL '120.00'),
(2, 101, TIMESTAMP '2026-05-20 10:02:00', DECIMAL '80.00');
-- INSERT: 2 rows
-- Фильтр по обычной колонке order_ts. Pruning сработает:
-- Trino применит day() к границам и отбросит файлы ненужных дней.
SELECT count(*) FROM iceberg.sales.events
WHERE order_ts >= TIMESTAMP '2026-05-20 00:00:00';
-- _col0
-- -------
-- 1
Проверить, что pruning сработал, можно через EXPLAIN ANALYZE: в строке table scan для Iceberg видно число прочитанных файлов. Если запрос с фильтром по партиционирующему выражению читает все файлы — partitioning подобран неудачно или фильтр не сводится к границам партиции.
Transforms: какие бывают функции партиционирования
Выражение в partitioning — это partition transform, функция, превращающая значение колонки в значение партиции. Iceberg даёт фиксированный набор transforms, и выбор между ними — это инженерное решение о гранулярности партиций.
| Transform | Что делает | Когда уместен |
|---|---|---|
identity (просто имя колонки) | Партиция = само значение колонки | Колонка с небольшим числом значений: страна, регион, тип события |
year(col) | Партиция по году | Очень крупные исторические таблицы |
month(col) | Партиция по месяцу | Таблицы с многолетней историей и помесячным доступом |
day(col) | Партиция по дню | Самый частый выбор для событийных и фактовых таблиц |
hour(col) | Партиция по часу | Стриминг с большим объёмом в час |
bucket(col, N) | Хеш-партиция в одну из N корзин | Колонка с высокой кардинальностью: user_id, order_id |
truncate(col, W) | Партиция по усечённому значению (числа или строки) | Группировка близких значений: префиксы строк, диапазоны чисел |
Главная ошибка партиционирования — неверная гранулярность. Слишком мелкие партиции (hour там, где хватило бы day) дают много мелких файлов и раздутые метаданные — ту самую деградацию из урока 5. Слишком крупные (year для таблицы с ежедневными запросами) — partition pruning почти не отсекает данные, потому что нужный день всё равно внутри огромной годовой партиции. Цель — чтобы типичный запрос затрагивал немного партиций, а каждая партиция была достаточно крупной для эффективных файлов.
Отдельно про bucket. identity на колонке высокой кардинальности вроде user_id создал бы миллионы крошечных партиций — катастрофа. bucket(user_id, 64) хеширует user_id в одну из 64 корзин: число партиций ограничено и предсказуемо, а запрос WHERE user_id = 12345 всё равно прунится — Trino вычислит, в какой корзине лежит этот хеш, и прочитает только её.
Можно комбинировать transforms: партиционировать сразу по нескольким выражениям.
-- Партиционирование сразу по дню и по корзине user_id
CREATE TABLE iceberg.sales.events_v2 (
event_id BIGINT,
user_id BIGINT,
order_ts TIMESTAMP(6),
amount DECIMAL(12,2)
)
WITH (
format = 'PARQUET',
partitioning = ARRAY['day(order_ts)', 'bucket(user_id, 64)']
);
Schema evolution: менять схему без переписывания
Iceberg позволяет менять схему таблицы безопасно и без переписывания файлов. Причина — в уроке 3: каждая колонка в Iceberg имеет внутренний стабильный числовой идентификатор, а имя — лишь подпись поверх id. Файлы данных хранят значения по id колонки. Поэтому переименование колонки меняет только запись в metadata-файле; ни один Parquet-файл не трогается.
Поддерживаемые операции ALTER TABLE:
-- Добавить колонку. Старые файлы её не содержат — для их строк будет NULL.
ALTER TABLE iceberg.sales.events ADD COLUMN channel VARCHAR;
-- Переименовать колонку. Меняется только метаданные: id колонки прежний.
ALTER TABLE iceberg.sales.events RENAME COLUMN amount TO order_amount;
-- Удалить колонку. Данные в файлах остаются, но колонка скрыта из схемы.
ALTER TABLE iceberg.sales.events DROP COLUMN channel;
Также поддерживается type widening — безопасное расширение типа: INTEGER -> BIGINT, REAL -> DOUBLE, увеличение precision у DECIMAL. Эти преобразования безопасны, потому что любое старое значение валидно в широком типе — BIGINT вмещает любой INTEGER. Обратное (BIGINT -> INTEGER) Iceberg не разрешает: значение могло бы не влезть, это потеря данных.
Почему «добавить колонку» безопасно для старых файлов? Старый Parquet-файл просто не содержит данных новой колонки. Iceberg видит по id, что в файле такой колонки нет, и подставляет для его строк NULL. Никакого переписывания. В Hive добавление колонки было хрупким и часто требовало осторожности именно потому, что там колонки сопоставлялись по позиции, а не по стабильному id.
Partition evolution: менять само партиционирование
Iceberg умеет уникальное — менять схему партиционирования на живой таблице. Это та проблема Hive, которая там вообще не имела решения.
-- Год назад партиционировали по дню. Партиции стали мелкими.
-- Меняем на месяц — БЕЗ переписывания старых данных.
ALTER TABLE iceberg.sales.events
SET PROPERTIES partitioning = ARRAY['month(order_ts)'];
Механика тонкая. Старые файлы данных остаются партиционированными по-старому (по дню) — их никто не переписывает. Новые записи партиционируются по-новому (по месяцу). В таблице сосуществуют две схемы партиционирования, и Iceberg хранит в метаданных обе. Для запроса это прозрачно: Trino применяет к старым файлам старую схему партиционирования, к новым — новую, и pruning корректно работает над обеими. Старые данные при желании можно постепенно перевести на новую раскладку через OPTIMIZE, но это не обязательно для корректности.
Partition evolution не переписывает прошлое автоматически. Если старые данные критичны для производительности и старая раскладка плоха, запустите OPTIMIZE с WHERE по старым партициям — он перепишет их уже по новой схеме. Без этого старые файлы останутся в прежней раскладке: корректность не пострадает, но выигрыш от новой схемы партиционирования получат только новые данные.
Попробуй сам
В песочнице создайте Iceberg-таблицу events с колонкой order_ts TIMESTAMP(6), партиционированную по day(order_ts). Загрузите данные за несколько разных дней. Упражнение первое: выполните SELECT с фильтром по order_ts за один день, оберните в EXPLAIN ANALYZE и найдите в table scan число прочитанных файлов — убедитесь, что прочитаны не все. Упражнение второе на schema evolution: выполните ADD COLUMN region VARCHAR, затем SELECT region FROM events по строкам, вставленным до добавления колонки — какое значение вернулось и почему. Затем RENAME COLUMN и проверьте, что данные на месте. Упражнение третье на partition evolution: смените партиционирование на month(order_ts) через SET PROPERTIES, вставьте новые строки, посмотрите events$partitions. Письменно объясните, почему в таблице теперь сосуществуют партиции двух гранулярностей и почему это не сломало запросы.