Apache Iceberg. Упражнение: Партицирование

Практика партицирования

|

Вы создавали таблицы и эволюционировали их схемы. Это хорошо для небольших наборов данных, но что происходит, когда у вас пять миллионов строк? Или пять миллиардов? В какой‑то момент сканирование каждого файла только для того, чтобы найти заказы за прошлый вторник, становится, мягко говоря «неоптимальным».

Партицирование решает эту проблему, организуя данные в логические группы. Iceberg идёт на шаг дальше со скрытым партицированием (hidden partitioning) — вы определяете стратегию партицирования один раз, а Iceberg делает всё остальное. Больше никаких партиционных колонок, загромождающих ваши запросы. Больше никакой гимнастики с WHERE year=2025 AND month=01.

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

Цели обучения

К концу этого упражнения вы сможете:

  • Создавать таблицу со скрытым партицированием, используя трансформации партиций
  • Генерировать реалистичные тестовые данные с помощью SQL
  • Исследовать партиции с помощью метатаблиц
  • Проверять отсечение партиций с помощью EXPLAIN

Предварительные требования

Шаг 1: Запуск окружения

Если окружение ещё не запущено:

docker compose up -d

Запустите Spark SQL:

docker compose exec -it spark-iceberg spark-sql --conf "spark.hadoop.hive.cli.print.header=true"

Создайте базу данных, если её ещё нет:

CREATE NAMESPACE IF NOT EXISTS demo.ecommerce;USE demo.ecommerce;

Шаг 2: Создание партицированной таблицы

На этот раз мы добавим предложение партицирования. Мы будем партицировать по месяцу, полученному из даты заказа:

CREATE TABLE orders_partitioned (    order_id BIGINT,    customer_id BIGINT,    order_date DATE,    total_amount DECIMAL(10, 2),    status STRING)USING icebergPARTITIONED BY (months(order_date))TBLPROPERTIES ('format-version'='2', 'write.format.default'='parquet');

months(order_date) — это трансформация партиции (partition transform). Iceberg поддерживает несколько таких трансформаций: year(), month(), day(), hour() для временных меток, bucket() для хэш‑распределения и truncate() для строк. Красота в том, что ваши запросы не должны о них знать — вы просто запрашиваете order_date как обычно.

Вы можете проверить детальную схему таблицы:

SHOW CREATE TABLE orders_partitioned;

Шаг 3: Генерация тестовых данных

Пяти строк недостаточно для демонстрации партицирования. Давайте сгенерируем 1000 заказов, распределённых по шести месяцам.

Функции Spark explode() и sequence() помогут нам сгенерировать тестовые данные:

INSERT INTO orders_partitionedSELECT    row_num AS order_id,    1 + (row_num % 100) AS customer_id,    date_add(DATE '2025-01-01', row_num % 180) AS order_date,    ROUND(CAST((10 + (row_num % 500)) AS DECIMAL(10,2)) + (row_num % 100) / 100.0, 2) AS total_amount,    CASE (row_num % 4)        WHEN 0 THEN 'completed'        WHEN 1 THEN 'completed'        WHEN 2 THEN 'pending'        ELSE 'cancelled'    END AS statusFROM (SELECT explode(sequence(1, 1000)) AS row_num);

Давайте разберём, что происходит:

  • sequence(1, 1000) генерирует массив [1, 2, 3, ..., 1000]
  • explode() разворачивает его в 1000 строк
  • row_num % 180 распределяет заказы по 180 дням (с января по июнь)
  • row_num % 100 даёт нам 100 разных клиентов
  • Распределение статусов: примерно 50% completed, 25% pending, 25% cancelled

Проверьте, что данные загрузились:

SELECT COUNT(*) FROM orders_partitioned;
count(1)
1000

Проверьте распределение по месяцам:

SELECT    DATE_TRUNC('month', order_date) AS month,    COUNT(*) AS order_countFROM orders_partitionedGROUP BY 1ORDER BY 1;

Вы должны увидеть заказы, распределённые с января по июнь 2025 года.

Вот что получилось у меня:

month order_count
2025‑01‑01 00:00:00 185
2025‑02‑01 00:00:00 168
2025‑03‑01 00:00:00 186
2025‑04‑01 00:00:00 161
2025‑05‑01 00:00:00 155
2025‑06‑01 00:00:00 145

Шаг 4: Исследование партиций

Теперь посмотрим, как Iceberg организовал наши данные. Метатаблица partitions показывает, какие именно партиции существуют:

SELECT * FROM demo.ecommerce.orders_partitioned.partitions;

Вы должны увидеть шесть партиций — по одной на каждый месяц. Номера партиций находятся в диапазоне от 660 до 665, что представляет количество месяцев с января 1970 (эпоха Iceberg для трансформации month()). Обратите внимание на колонки record_count и file_count — каждая партиция отслеживает свою собственную статистику.

Вот что получилось у меня:

partition record_count file_count total_size
{order_date_month=660} 185 1 2162
{order_date_month=663} 161 1 1958
{order_date_month=664} 155 1 1957
{order_date_month=661} 168 1 1934
{order_date_month=662} 186 1 2100
{order_date_month=665} 145 1 1894

Давайте также посмотрим на файлы:

SELECT    partition,    file_path,    record_count,    file_size_in_bytesFROM demo.ecommerce.orders_partitioned.files;

Вы увидите, что каждая партиция имеет свой собственный файл, организованный по месяцам в пути файла:

partition file_path record_count file_size_in_bytes
{"order_date_month":664} s3://warehouse/ecommerce/orders_partitioned/data/order_date_month=2025‑05/… 155 2779
{"order_date_month":665} s3://warehouse/ecommerce/orders_partitioned/data/order_date_month=2025‑06/… 145 2715
{"order_date_month":662} s3://warehouse/ecommerce/orders_partitioned/data/order_date_month=2025‑03/… 186 2923
{"order_date_month":663} s3://warehouse/ecommerce/orders_partitioned/data/order_date_month=2025‑04/… 161 2778
{"order_date_month":660} s3://warehouse/ecommerce/orders_partitioned/data/order_date_month=2025‑01/… 185 2937
{"order_date_month":661} s3://warehouse/ecommerce/orders_partitioned/data/order_date_month=2025‑02/… 168 2757

Шаг 5: Отсечение партиций в действии

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

Давайте проверим отсечение партиций с фактическим подсчётом строк. Начнём с полного сканирования таблицы:

SELECT COUNT(*) AS total_ordersFROM orders_partitioned;

Результат: 1000.

Теперь посмотрим, что происходит, когда вы фильтруете только март:

SELECT COUNT(*) AS march_ordersFROM orders_partitionedWHERE order_date >= DATE '2025-03-01'  AND order_date < DATE '2025-04-01';

Результат: 186.

Итак, 186 строк за март из 1000 всего.

Давайте используем EXPLAIN COST, чтобы увидеть разницу в том, что Iceberg планирует сканировать. Сначала полная таблица:

EXPLAIN COSTSELECT * FROM orders_partitioned;

Посмотрите на строку Statistics в выводе:

Statistics(sizeInBytes=46.9 KiB, rowCount=1.00E+3)

Это показывает, что Iceberg отсканирует ~47 КБ по всем 1000 строкам (все шесть партиций).

Теперь сравните с фильтрованным запросом:

EXPLAIN COSTSELECT * FROM orders_partitionedWHERE order_date >= DATE '2025-03-01'  AND order_date < DATE '2025-04-01';

Вы должны увидеть что‑то вроде:

Statistics(sizeInBytes=8.7 KiB, rowCount=186)

Это и есть отсечение партиций в действии! Фильтрованный запрос сканирует только ~8,5 КБ и 186 строк — только мартовскую партицию. Iceberg автоматически определил, что только партиция 662 (март 2025) может содержать соответствующие строки, поэтому полностью пропустил остальные пять партиций.

В реальном data lake с терабайтами данных это разница между 30‑секундным запросом и 3‑часовым.

Шаг 6: Преимущество скрытого партицирования

Обратите внимание, что во всех наших запросах выше мы фильтровали по order_date, а не по какой‑либо партиционной колонке. Давайте продемонстрируем это ещё одним примером:

-- Запрос на один день в мартеSELECT COUNT(*) AS single_day_ordersFROM orders_partitionedWHERE order_date = DATE '2025-03-15';

Вы должны получить 6 заказов за этот конкретный день. Теперь попробуйте EXPLAIN COST для этого запроса:

EXPLAIN COSTSELECT COUNT(*) AS single_day_ordersFROM orders_partitionedWHERE order_date = DATE '2025-03-15';

Обратите внимание, что Statistics всё ещё показывают rowCount=186 — столько же, сколько когда мы запрашивали весь месяц! Это потому, что отсечение партиций работает на уровне партиций. Поскольку мы партицировали по months(order_date), Iceberg отсекает до мартовской партиции (все 186 строк), а затем фильтрует до конкретного дня во время сканирования.

За кулисами Iceberg:

  1. Видит фильтр order_date = DATE '2025-03-15'
  2. Применяет трансформацию партиции: months(DATE '2025-03-15') = 662 (март 2025)
  3. Сканирует только файл мартовской партиции

Ключевой инсайт: вам не нужно было указывать WHERE partition = 662 или WHERE month = 'March'. Вы просто фильтровали по фактической колонке даты, и Iceberg автоматически определил, какую партицию читать.

Это и есть скрытое партицирование. Структура партиций — это деталь реализации, о которой ваши запросы не должны беспокоиться. Сравните это с Hive‑стилем партицирования, где вам пришлось бы:

  • Добавлять колонку partition_month в вашу таблицу
  • Корректно заполнять её при каждой записи
  • Включать её в WHERE‑условие каждого запроса

С Iceberg вы определяете трансформацию один раз — month(order_date) — и забываете о ней.

Шаг 7: Очистка

Выйдите из Spark SQL:

exit;

Если вы закончили на сегодня:

docker compose down -v

Что вы узнали

  • Трансформации партиций, такие как months(), позволяют Iceberg автоматически организовывать данные без изменения вашей схемы
  • Метатаблица partitions показывает, как именно распределены данные
  • Отсечение партиций сильно сокращает объём сканируемых данных
  • Скрытое партицирование сохраняет ваши запросы чистыми — никаких синтетических партиционных колонок не требуется

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


Что дальше? В следующем уроке мы изучим Кластеризацию — как организовать данные по нескольким атрибутам через Z‑ordering для ещё более эффективных запросов.

→ Вперёд к кластеризации

← Назад к уроку «Партицирование и эволюция партиций»