Apache Iceberg. Упражнение: Ветвление и тегирование
Практика Git‑подобного workflow
Содержание
До сих пор вы работали со снапшотами по их ID или временным меткам. Это работает, но попробуйте объяснить вашему финансовому директору, что «квартальный отчёт использует снапшот 7851662191363948742». Не очень удобно для пользователя.
Теги и ветки привносят Git‑подобную семантику в таблицы Iceberg:
- Теги — это именованные ссылки на конкретные снапшоты. Думайте о них как о закладках: «это данные, которые мы использовали для отчёта за Q4» или «это состояние перед большой миграцией».
- Ветки — это независимые линии разработки. Вы можете писать в ветку, не влияя на основную таблицу, валидировать изменения, а затем либо опубликовать их, либо выбросить. Это основа workflow Write‑Audit‑Publish (WAP).
В этом упражнении вы создадите теги для важных снапшотов (для соответствия требованиям), создадите staging‑ветку, загрузите в неё данные, проверите их и опубликуете в продакшен.
Цели обучения
К концу упражнения вы сможете:
- Создавать теги для снапшотов и управлять ими
- Запрашивать данные по конкретному тегу
- Создавать ветки для изолированных записей
- Реализовывать workflow Write‑Audit‑Publish
- Выполнять fast‑forward основной ветки для включения проверенных изменений
Предварительные требования
- Завершённое упражнение 4 (или запущенное окружение)
- Около 25 минут
Шаг 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.finance;USE demo.finance; DROP TABLE IF EXISTS transactions; CREATE TABLE transactions ( transaction_id BIGINT, account_id BIGINT, transaction_date DATE, amount DECIMAL(15, 2), description STRING, verified BOOLEAN)USING icebergTBLPROPERTIES ('format-version'='2', 'write.format.default'='parquet');
Загрузите некоторые начальные проверенные транзакции:
INSERT INTO transactions VALUES (1001, 100, CAST('2025-01-02' AS DATE), 5000.00, 'Начальный депозит', true), (1002, 100, CAST('2025-01-05' AS DATE), -150.00, 'Канцелярские товары', true), (1003, 101, CAST('2025-01-03' AS DATE), 12000.00, 'Начальный депозит', true), (1004, 101, CAST('2025-01-10' AS DATE), -3200.00, 'Покупка оборудования', true), (1005, 102, CAST('2025-01-04' AS DATE), 8500.00, 'Начальный депозит', true);
Проверьте данные:
SELECT * FROM transactions ORDER BY transaction_id;
| transaction_id | account_id | transaction_date | amount | description | verified |
|---|---|---|---|---|---|
| 1001 | 100 | 2025-01-02 | 5000.00 | Начальный депозит | true |
| 1002 | 100 | 2025-01-05 | -150.00 | Канцелярские товары | true |
| 1003 | 101 | 2025-01-03 | 12000.00 | Начальный депозит | true |
| 1004 | 101 | 2025-01-10 | -3200.00 | Покупка оборудования | true |
| 1005 | 102 | 2025-01-04 | 8500.00 | Начальный депозит | true |
Шаг 2: Создайте тег для закрытия месяца
31 января. Финансовой команде нужно закрыть книги. Давайте пометим текущее состояние тегом, чтобы мы всегда могли воспроизвести числа за январь:
ALTER TABLE transactions CREATE TAG `jan-2025-close`;
Проверьте, что тег создан:
SELECT * FROM demo.finance.transactions.refs;
Вы должны увидеть:
| name | type | snapshot_id | max_reference_age_in_ms | ... |
|---|---|---|---|---|
| jan-2025-close | TAG | ... | NULL | |
| main | BRANCH | ... | NULL |
Обратите внимание, что main также указан — это ветка по умолчанию, которая есть у каждой таблицы Iceberg.
Шаг 3: Продолжаем обычные операции
Наступает февраль. Поступают новые транзакции:
INSERT INTO transactions VALUES (1006, 100, CAST('2025-02-01' AS DATE), -89.99, 'Подписка на ПО', true), (1007, 102, CAST('2025-02-03' AS DATE), -1200.00, 'Оплата подрядчику', true), (1008, 101, CAST('2025-02-05' AS DATE), 4500.00, 'Получен платёж от клиента', true);
Текущая таблица теперь содержит 8 транзакций:
SELECT COUNT(*) FROM transactions;
Шаг 4: Запрос помеченного снапшота
Приходят аудиторы. Они хотят увидеть точно, как выглядели книги на момент закрытия января. Без проблем:
SELECT *FROM transactions VERSION AS OF 'jan-2025-close'ORDER BY transaction_id;
| transaction_id | account_id | transaction_date | amount | description | verified |
|---|---|---|---|---|---|
| 1001 | 100 | 2025-01-02 | 5000.00 | Начальный депозит | true |
| 1002 | 100 | 2025-01-05 | -150.00 | Канцелярские товары | true |
| 1003 | 101 | 2025-01-03 | 12000.00 | Начальный депозит | true |
| 1004 | 101 | 2025-01-10 | -3200.00 | Покупка оборудования | true |
| 1005 | 102 | 2025-01-04 | 8500.00 | Начальный депозит | true |
Только исходные 5 транзакций. Февральские данные в этом теге не существуют.
Давайте проверим, что балансы счетов соответствуют отчётности:
SELECT account_id, SUM(amount) AS balance_at_jan_closeFROM transactions VERSION AS OF 'jan-2025-close'GROUP BY account_idORDER BY account_id;
Вы должны увидеть:
| account_id | balance_at_jan_close |
|---|---|
| 100 | 4850.00 |
| 101 | 8800.00 |
| 102 | 8500.00 |
Эти числа неизменяемы. Независимо от того, что произойдёт с таблицей в будущем, этот тег всегда будет показывать одни и те же данные.
Шаг 5: Создайте staging‑ветку
Теперь давайте рассмотрим более сложный сценарий. Ваш ETL‑пайплайн имеет новую партию транзакций для загрузки, но вы хотите проверить их, прежде чем они попадут в продакшен.
Создайте staging‑ветку:
ALTER TABLE transactions CREATE BRANCH staging;
Проверьте ссылки:
SELECT name, type, snapshot_idFROM demo.finance.transactions.refs;
| name | type | snapshot_id |
|---|---|---|
| main | BRANCH | 4187204227757276764 |
| staging | BRANCH | 4187204227757276764 |
| jan-2025-close | TAG | 5170763347723356942 |
Теперь у вас три ссылки: main, staging и jan-2025-close. Обратите внимание, что staging начинается с того же снапшота, что и main.
Шаг 6: Запись в staging‑ветку
Чтобы писать в ветку с использованием Write‑Audit‑Publish, сначала нужно включить эту возможность:
ALTER TABLE demo.finance.transactions SET TBLPROPERTIES ('write.wap.enabled' = 'true');
Затем нужно установить, какую ветку вы хотите использовать как цель записи:
SET spark.wap.branch = staging;
Теперь любые записи идут в staging‑ветку, а не в main. Загрузите новую партию:
INSERT INTO transactions VALUES (1009, 100, CAST('2025-02-10' AS DATE), -500.00, 'Консалтинговый сбор', false), (1010, 103, CAST('2025-02-10' AS DATE), 15000.00, 'Депозит нового клиента', false), (1011, 101, CAST('2025-02-11' AS DATE), -750.00, 'Командировочные расходы', false), (1012, 100, CAST('2025-02-12' AS DATE), 2000.00, 'Получен возврат', false);
Теперь проверьте обе ветки:
-- Продакшен (ветка main) — нужно явно запроситьSELECT COUNT(*) AS main_countFROM transactions VERSION AS OF 'main'; -- Staging‑ветка (текущая цель записи, также доступна по имени)SELECT COUNT(*) AS staging_countFROM transactions VERSION AS OF 'staging';
В продакшене 8 транзакций. В staging — 12. Ветки разошлись.
Шаг 7: Валидация данных в staging
Перед публикацией давайте запустим некоторые проверки качества данных в staging:
-- Проверка на отрицательные балансы (нарушение бизнес‑правила)SELECT account_id, SUM(amount) AS balanceFROM transactions VERSION AS OF 'staging'GROUP BY account_idHAVING SUM(amount) < 0;
Отрицательных балансов нет — хорошо.
-- Проверка на непроверенные транзакции (требуют ревью)SELECT *FROM transactions VERSION AS OF 'staging'WHERE verified = false;
| transaction_id | account_id | transaction_date | amount | description | verified |
|---|---|---|---|---|---|
| 1009 | 100 | 2025-02-10 | -500.00 | Консалтинговый сбор | false |
| 1010 | 103 | 2025-02-10 | 15000.00 | Депозит нового клиента | false |
| 1011 | 101 | 2025-02-11 | -750.00 | Командировочные расходы | false |
| 1012 | 100 | 2025-02-12 | 2000.00 | Получен возврат | false |
Новые транзакции непроверенные. В реальном workflow вы могли бы:
- Запустить автоматические правила валидации
- Отправить алерты, если обнаружены аномалии
- Провести человеческий ревью и утверждение
Вы можете проверить, что продакшен всё ещё нетронут:
SELECT COUNT(*)FROM transactions VERSION AS OF 'main'WHERE verified = false;
Ноль непроверенных в продакшене.
Давайте пометим транзакции в staging как проверенные (помните, мы всё ещё пишем в staging):
UPDATE transactionsSET verified = trueWHERE verified = false;
Проверьте обновление (всё ещё в staging):
SELECT transaction_id, verifiedFROM transactions VERSION AS OF 'staging'WHERE transaction_id >= 1009;
| transaction_id | verified |
|---|---|
| 1011 | true |
| 1012 | true |
| 1010 | true |
| 1009 | true |
Все проверены.
Шаг 8: Публикация проверенных изменений
Данные валидированы. Пора публиковать в продакшен.
Сначала посмотрим, куда указывает каждая ветка:
SELECT name, type, snapshot_idFROM demo.finance.transactions.refs;
| name | type | snapshot_id |
|---|---|---|
| main | BRANCH | 4187204227757276764 |
| staging | BRANCH | 3530026071892536539 |
| jan-2025-close | TAG | 5170763347723356942 |
Staging опережает main. Чтобы опубликовать, мы делаем fast‑forward main к снапшоту staging:
CALL system.fast_forward('demo.finance.transactions', 'main', 'staging');
| branch_updated | previous_ref | updated_ref |
|---|---|---|
| main | 4187204227757276764 | 3530026071892536539 |
Это перемещает указатель ветки main так, чтобы он совпадал с staging. Никакие данные не копируются — это просто обновление метаданных.
Теперь сбросьте цель записи обратно на main:
ALTER TABLE demo.finance.transactions UNSET TBLPROPERTIES ('write.wap.enabled');
Проверьте, что продакшен теперь содержит все данные:
SELECT * FROM transactions ORDER BY transaction_id;
| transaction_id | account_id | transaction_date | amount | description | verified |
|---|---|---|---|---|---|
| 1001 | 100 | 2025-01-02 | 5000.00 | Начальный депозит | true |
| 1002 | 100 | 2025-01-05 | -150.00 | Канцелярские товары | true |
| 1003 | 101 | 2025-01-03 | 12000.00 | Начальный депозит | true |
| 1004 | 101 | 2025-01-10 | -3200.00 | Покупка оборудования | true |
| 1005 | 102 | 2025-01-04 | 8500.00 | Начальный депозит | true |
| 1006 | 100 | 2025-02-01 | -89.99 | Подписка на ПО | true |
| 1007 | 102 | 2025-02-03 | -1200.00 | Оплата подрядчику | true |
| 1008 | 101 | 2025-02-05 | 4500.00 | Получен платёж от клиента | true |
| 1009 | 100 | 2025-02-10 | -500.00 | Консалтинговый сбор | true |
| 1010 | 103 | 2025-02-10 | 15000.00 | Депозит нового клиента | true |
| 1011 | 101 | 2025-02-11 | -750.00 | Командировочные расходы | true |
| 1012 | 100 | 2025-02-12 | 2000.00 | Получен возврат | true |
Все 12 транзакций, все проверены.
Шаг 9: Очистка ветки
Staging‑ветка выполнила свою задачу. Вы можете удалить её:
ALTER TABLE transactions DROP BRANCH staging;
Проверьте ссылки:
SELECT name, type FROM demo.finance.transactions.refs;
| name | type |
|---|---|
| main | BRANCH |
| jan-2025-close | TAG |
Остались только main и jan-2025-close.
Шаг 10: Создайте ещё один тег
Давайте пометим закрытие февраля:
ALTER TABLE transactions CREATE TAG `feb-2025-close`;
Теперь вы можете сравнить любые две точки во времени:
SELECT period, account_id, balance FROM ( -- Балансы января SELECT 'Январь' AS period, account_id, SUM(amount) AS balance FROM transactions VERSION AS OF 'jan-2025-close' GROUP BY account_id UNION ALL -- Балансы февраля SELECT 'Февраль' AS period, account_id, SUM(amount) AS balance FROM transactions VERSION AS OF 'feb-2025-close' GROUP BY account_id) resultsORDER BY period, account_id;
Вы должны увидеть:
| period | account_id | balance |
|---|---|---|
| Февраль | 100 | 6260.01 |
| Февраль | 101 | 12550.00 |
| Февраль | 102 | 7300.00 |
| Февраль | 103 | 15000.00 |
| Январь | 100 | 4850.00 |
| Январь | 101 | 8800.00 |
| Январь | 102 | 8500.00 |
Это невероятно мощно для финансовой отчётности, аудиторских следов и анализа трендов.
Шаг 11: Управление тегами
Теги могут иметь политики удержания. Создайте тег с автоматическим истечением:
ALTER TABLE transactions CREATE TAG `temp-debug-tag` RETAIN 7 DAYS;
Через 7 дней этот тег станет кандидатом на очистку во время экспирации снапшотов.
Перечислите все теги с их свойствами:
SELECT name, type, snapshot_id, max_reference_age_in_ms / 86400000 AS retention_daysFROM demo.finance.transactions.refsWHERE type = 'TAG';
| name | type | snapshot_id | retention_days |
|---|---|---|---|
| feb-2025-close | TAG | 3530026071892536539 | NULL |
| jan-2025-close | TAG | 5170763347723356942 | NULL |
| temp-debug-tag | TAG | 3530026071892536539 | 7.0 |
Не забывайте регулярно удалять теги, которые больше не нужны, чтобы держать ссылки таблицы в порядке:
ALTER TABLE transactions DROP TAG `temp-debug-tag`;
Шаг 12: Очистка
Выйдите из Spark SQL:
exit;
Если вы закончили:
docker compose down -v
Что вы узнали
- Теги — это именованные неизменяемые ссылки на снапшоты — идеально для соответствия требованиям и воспроизводимости.
- Ветки позволяют изолированные записи без влияния на продакшен.
- Workflow Write‑Audit‑Publish (WAP): stage → validate → fast‑forward.
fast_forwardпубликует ветку, перемещая указательmain.- Теги и ветки — это операции только с метаданными — мгновенные и дешёвые.
Практические use‑cases
Теги:
- Регуляторное соответствие («это Q4 2024, как отчитано в SEC»)
- Воспроизводимость машинного обучения («модель v2.3 обучалась на этом снапшоте»)
- Маркеры релизов («before‑migration», «after‑migration»)
Ветки:
- Валидация ETL перед публикацией
- A/B‑тестирование различных трансформаций данных
- Экспериментирование без риска для продакшена
- Blue/green‑деплойменты данных
«Ветвление и тегирование в Iceberg — это как Git для данных: можно ставить закладки на важные моменты и экспериментировать в изоляции, не боясь сломать продакшен. Только не забывайте удалять старые ветки, иначе ваш data lake превратится в архив забытых экспериментов.»