Apache Iceberg. Упражнение: Партицирование
Практика партицирования
Содержание
Вы создавали таблицы и эволюционировали их схемы. Это хорошо для небольших наборов данных, но что происходит, когда у вас пять миллионов строк? Или пять миллиардов? В какой‑то момент сканирование каждого файла только для того, чтобы найти заказы за прошлый вторник, становится, мягко говоря «неоптимальным».
Партицирование решает эту проблему, организуя данные в логические группы. Iceberg идёт на шаг дальше со скрытым партицированием (hidden partitioning) — вы определяете стратегию партицирования один раз, а Iceberg делает всё остальное. Больше никаких партиционных колонок, загромождающих ваши запросы. Больше никакой гимнастики с WHERE year=2025 AND month=01.
В этом упражнении вы создадите партицированную таблицу, сгенерируете достаточно данных, чтобы партицирование имело смысл, и увидите, как именно Iceberg отсекает партиции во время запроса.
Цели обучения
К концу этого упражнения вы сможете:
- Создавать таблицу со скрытым партицированием, используя трансформации партиций
- Генерировать реалистичные тестовые данные с помощью SQL
- Исследовать партиции с помощью метатаблиц
- Проверять отсечение партиций с помощью
EXPLAIN
Предварительные требования
- Выполненное упражнение «Эволюция схемы» (или запущенное окружение)
- Около 20 минут
Шаг 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:
- Видит фильтр
order_date = DATE '2025-03-15' - Применяет трансформацию партиции:
months(DATE '2025-03-15') = 662(март 2025) - Сканирует только файл мартовской партиции
Ключевой инсайт: вам не нужно было указывать 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 для ещё более эффективных запросов.