Apache Iceberg. Тегирование и ветвление
Git для ваших данных
Содержание
Помните, когда вы впервые по‑настоящему разобрались с ветвлением в Git? Вы думали: «Это меняет всё»?
Что ж, Iceberg делает то же самое для ваших данных. Каждая таблица имеет ветку main. Каждый раз, когда вы изменяете таблицу, создаётся новый снапшот, и ссылка на ветку main следует за новыми снапшотами.
flowchart LR
A(("Snapshot<br/>(empty)")) --> B(("Snapshot 1")) --> C(("Snapshot 2")) -.- main@{shape: rounded, label:main}
classDef branchClass stroke:#bce613,color:#bce613
class main branchClass
Этот ярлык, эта ссылка всегда указывает на новый снапшот, который вы создаёте. Вам не нужно ничего делать, чтобы продвинуть её. Вам даже не нужно постоянно об этом думать. Это просто происходит под капотом.
Наша старая добрая таблица продаж имеет ветку main, и она работает в продакшене, обслуживая дашборды и отчёты по всей компании.
Но потом кто‑то звонит. Говорит: плохие новости, товар B был неправильно оценён. Должен быть на 10% дешевле.
Ветка для исправления цен
Им нужно, чтобы вы исправили это и оценили влияние на возвраты. Но вы не можете просто начать обновлять продакшен‑данные. Эти расчёты требуют тестирования, и финансистам нужно утвердить их, прежде чем мы что‑то сделаем. Это просто своего рода «давайте спроецируем, каким будет влияние». Ошибётесь — получите сломанные отчёты по выручке, разгневанных клиентов и, вероятно, разгневанных руководителей. Ни у кого не будет хорошего дня.
С Iceberg всё просто. Мы создадим ветку pricing_fix, чтобы выполнить всю нашу работу.
flowchart LR
A(("Snapshot<br/>(empty)")) --> B(("Snapshot 1")) --> C(("Snapshot 2")) -.- main@{shape: rounded, label:main}
C -.- pricing_fix@{shape: rounded, label:pricing_fix}
classDef branchClass stroke:#bce613,color:#bce613
class main,pricing_fix branchClass
alter table sales create branch pricing_fix
Вы делаете это, тестируете свои расчёты и обновления, получаете подтверждение от финансистов, что всё выглядит правильно. И не бойтесь что‑то сломать, потому что main останется нетронутым, пока вы не будете уверены, что всё выглядит хорошо.
В main могут появляться новые продажи, новые коммиты. Это нормально. Вы находитесь, так сказать, в своём собственном «частном Бирюлево в вашей ветке pricing_fix.
Как только финансисты одобрят ваши расчёты по возвратам, вы обновите все продажи товара B в вашей ветке с пересмотренной суммой. Это создаст новый снапшот, верно? Вы записали что‑то, выполнили некоторые обновления, поэтому мы получаем новый снапшот. Назовём его снапшот 3 (для простоты). В реальности ID снапшотов — это большие, длинные, случайно выглядящие числа. Я использую 1, 2 и 3, потому что они хорошо помещаются на слайдах. Просто знайте, что в реальной жизни они не обязательно последовательны.
flowchart LR
A(("Snapshot<br/>(empty)")) --> B(("Snapshot 1")) --> C(("Snapshot 2")) -.- main@{shape: rounded, label:main}
C --> D(("Snapshot 3")) -.- pricing_fix@{shape: rounded, label:pricing_fix}
classDef branchClass stroke:#bce613,color:#bce613
class main,pricing_fix branchClass
update sqles.branch_pricing_fixset amount = 0.9 * amountwhere product = 'Widget B';
Время проверить исправление. Синтаксис запроса ветки — ещё одна из тех вещей, которые зависят от движка. В моём случае я просто добавляю branch_pricing_fix к имени таблицы. Проверьте документацию для вашего движка — у вас может быть что‑то другое.
Товар B стоил $200 в main со скидкой 10%. Мы должны увидеть $180. И мы выполняем этот запрос:
SELECT * FROM sales.branch_pricing_fix WHERE product = 'Widget B';
Вот он. Выглядит хорошо. Похоже, наше обновление было в целом разумным и сделало правильную вещь. Но, опять же, это обновление не влияет на main. Снапшот записан не туда. Продакшен продолжает работать, делая своё дело. Новые продажи публикуются. Создаются новые снапшоты. Бизнес как обычно.
Fast‑forward мержи
Хорошо, вы поработали в своей ветке. Финансисты всё одобрили. Пора смерджить её обратно в main.
Процедура fast‑forward в Iceberg, которую вы видите в этом вызове, обрабатывает эту операцию:
flowchart LR
A(("Snapshot<br/>(empty)")) --> B(("Snapshot 1")) --> C(("Snapshot 2")) --> E(("Snapshot 4")) --> F(("Snapshot 5")) -.- main@{shape: rounded, label:main}
C --> D(("Snapshot 3")) -.- pricing_fix@{shape: rounded, label:pricing_fix}
classDef branchClass stroke:#bce613,color:#bce613
class main,pricing_fix branchClass
CALL system.fast_forward('sales', 'main', 'pricing_fix'); -- Cannot fast-forward: main is not an ancestor of pricing_fix
Давайте запустим. Ой, мы получили сообщение об ошибке. Что случилось?
Fast‑forward работает как fast‑forward в Git. Он требует, чтобы main был прямым предком вашей ветки без дополнительных коммитов или снапшотов в main. Но здесь main продвинулся вперёд. Новые изменения пришли, пока вы работали, что, в общем‑то, ожидаемо, верно?
Fast‑forward — отличный случай. Как и мерж в Git, он всегда работает, это супер тривиально — просто перемещение указателей. Но нетрудно представить, что продакшен движется вперёд, пока мы занимаемся тестированием.
Так что теперь? Поскольку вы использовали операции обновления, вы не можете сделать fast‑forward. Единственный вариант здесь — начать заново. Итак, что мы сделаем: удалим нашу ветку pricing_fix, пересоздадим её из текущего main, перезапустим обновление.
alter table sales drop branch pricing_fix;alter table sales create branch pricing_fix; update sqles.branch_pricing_fixset amount = 0.9 * amountwhere product = 'Widget B';
Теперь main — прямой предок, и fast‑forward работает. main может перейти к снапшоту 6, совпадающему с новой веткой pricing_fix.
Fast‑forward — самый простой способ смерджить изменения.
CALL system.fast_forward('sales', 'main', 'pricing_fix');
flowchart LR
A(("Snapshot<br/>(empty)")) --> B(("Snapshot 1")) --> C(("Snapshot 2")) --> E(("Snapshot 4")) --> F(("Snapshot 5")) --> G(("Snapshot 6")) -.- main@{shape: rounded, label:main}
C --> D(("Snapshot 3"))
G -.- pricing_fix@{shape: rounded, label:pricing_fix}
classDef branchClass stroke:#bce613,color:#bce613
class main,pricing_fix branchClass
Он перемещает указатель вашей ветки вперёд, и всё. Никакого нового снапшота, нулевые накладные расходы, просто изменение того, что Iceberg называет ref (ссылкой). Но мы видели подвох: он работает только если целевая ветка является прямым предком. Если истории разошлись, fast‑forward терпит неудачу. Досадно.
Cherry‑pick и теги
Итак, что делать, когда fast‑forward не работает? Используйте Cherry‑pick.
Опять же, есть прямая аналогия с Git, и Cherry‑pick, как правило, продвинутая функция Git. Если вы не знаете, как её использовать, просто перенесите всё то понимание, которое у вас есть, сюда. Это очень поможет. Если нет, давайте вместе разберём эту диаграмму.
flowchart LR
A(("Snapshot<br/>(empty)")) --> B(("Snapshot 1")) --> C(("Snapshot 2")) --> E(("Snapshot 4")) --> F(("Snapshot 5")) --> G(("Snapshot 6")) -.- main@{shape: rounded, label:main}
C --> D(("Snapshot 3"))
F --> H(("Snapshot 7"))-.- new_sales@{shape: rounded, label:new_sales}
classDef branchClass stroke:#bce613,color:#bce613
class main,new_sales branchClass
Вы построили новый ETL‑задание для продаж партнёров в ветке new_sales, добавив миллион записей. Эта ветка отделилась от main в снапшоте 5, создав снапшот 7. Но main продолжает двигаться. Жизнь идёт без нас, верно? Теперь он в снапшоте 6. Истории разошлись. Мы больше не можем сделать fast‑forward.
Cherry‑pick спешит на помощь. Берём снапшот 7 (это то, что означает cherry‑pick), воспроизводим его поверх текущей ветки main, и это создаст снапшот 8, который содержит снапшот 6 плюс снапшот 7 — весь этот контент в одном месте (по крайней мере, хочется надеяться). И мы делаем это, вызывая функцию cherry_pick, как вы видите здесь:
flowchart LR
... ---> E(("Snapshot 4")) --> F(("Snapshot 5")) --> G(("Snapshot 6")) --> I(("Snapshot 8")) -.- main@{shape: rounded, label:main}
F --> H(("Snapshot 7"))-.- new_sales@{shape: rounded, label:new_sales}
H -.->|cherry-pick| I
classDef branchClass stroke:#bce613,color:#bce613
class main,new_sales branchClass
CALL system.cherry_pick('sales', 'main', 7);
Когда истории расходятся и fast‑forward терпит неудачу, Cherry‑pick — ваше решение. Он берёт снапшот из вашей ветки и воспроизводит эти изменения поверх текущего состояния main, создавая совершенно новый снапшот со всем объединённым, с двумя важными ограничениями, которые вы должны знать:
- Работает только с append‑операциями и dynamic overwrite. Никаких row‑level updates не допускается при cherry‑pick. Так что если у вас есть снапшоты с обновлениями на уровне строк, cherry‑pick не сработает. Только append и dynamic overwrite.
- Cherry‑pick интегрирует изменения только в
main, не в другие ветки. Странное ограничение, но вам не разрешено cherry‑pick, скажем, изmainв вашу ветку или из чьей‑то ветки в вашу ветку. Он действительно предназначен для того, чтобы приводитьmainв актуальное состояние — переносить работу из ветки в продакшен.
Теги
И снова, с Git они точно такие же. Тег — это неизменяемая удобная метка, которая указывает на снапшот. Скажем, снапшот 2 особенный. И помните, в реальном мире ID снапшота — это ужасно неудобное число. Вы только будете копировать и вставлять его, а не держать в голове. Может быть, это данные на конец квартала, или то, что вы представили совету директоров, или точное состояние, о котором аудиторы спросят через полгода, что‑то в этом роде. Какова бы ни была причина, вы хотите сохранить эту конкретную точку во времени.
Итак, вы создаёте для него тег, в данном случае Q3_report. Одна команда ALTER TABLE CREATE TAG, и готово. Эта метка присвоена этому снапшоту и будет там столько, сколько вы пожелаете.
Вы можете запросить тег с помощью синтаксиса tag_ (ещё одна вещь, которая может зависеть от движка, поэтому проверьте документацию):
SELECT * FROM sales.tag_Q3_report;
Это показывает таблицу продаж такой, какой она выглядела, какой существовала в момент тега Q3_report — сразу после второй вставки, как вы видите здесь.
Что, если вам интересно, какие ветки и теги существуют для конкретной таблицы? У вас есть эта метатаблица refs, которая как бы прикреплена к вашей основной таблице. Итак, в этом случае мы запросим sales.refs и увидим: вот наши ветки и наши теги.
select name, type, snapshot_id from sales.ref
| name | type | snapshot_id |
|---|---|---|
| main | branch | 5 |
| Q3_report | tag | 2 |
| pricing_fix | branch | 3 |
Это таблица, которую вы просто читаете. Iceberg поддерживает её в актуальном состоянии для вас. Каждая ветка, каждый тег — все они в одном месте.
Надеюсь, они не выходят из‑под контроля с кучей‑кучей веток. Вы можете представить проекты в Git, где много веток, которые живут долго. Не знаю, всегда ли мы хотим видеть это в нашем data lake, но это ценная, важная функция, которой вы должны пользоваться.
Аналогично, тегирование — это тоже вещь. У вас может оказаться много тегов. И приятно иметь возможность перечислить их и увидеть все в одном месте.
И помните, это разные вещи — ветки и теги. Не то чтобы вы выбирали одно или другое. Это оба инструмента, которые должны быть в вашем арсенале.
- Ветка — это изменяемая линия эволюции таблицы. Это имя для альтернативной истории, которую вы можете развивать. Она отслеживает свою собственную цепочку снапшотов по мере того, как вы записываете данные вперёд, независимо. Это в точности как ветки Git. Используйте их для экспериментов. Вы можете создать ветки dev, staging, QA, своего рода environment‑ветки для тестирования изменений без влияния на продакшен, если у вас нет буквально отдельного dev‑ или staging‑data lake.
main— всегда ветка по умолчанию. Каждая таблица имеет её. Чтение или запись вmain— вам не нужно ничего говорить об имени ветки. Это то, где вы будете по умолчанию. Это ветка, к которой любой запрос будет обращаться по умолчанию.- Для других веток вам нужно явно ссылаться на них, как вы видите здесь.
Теги другие — это неизменяемые указатели на конкретные снапшоты. Тег снапшота остаётся там навсегда. Это как закладка, которая не двигается. Неважно, какие новые снапшоты вы создаёте, тег просто остаётся там. Используйте это, чтобы отмечать важные снапшоты, которые вы не хотите терять, к которым хотите обращаться по удобному для человека имени.
Управление временем жизни
Я упомянул, что если бы ветки размножались как сумасшедшие, это могло бы стать немного беспорядочным? Да, каждый снапшот добавляет метаданные. Если вы храните каждый снапшот вечно, эти метаданные просто растут без границ. Обычно вам нужна только довольно недавняя история — месяц, квартал, что‑то, что говорят юристы, может быть, не 10 лет снапшотов.
Iceberg даёт вам встроенные политики удержания для веток и тегов. Вы можете установить правила удержания, и старые снапшоты, связанные с этими ветками и тегами, могут автоматически очищаться.
Когда вы создаёте ветку, вы можете установить две вещи: как долго ветка живёт и сколько истории снапшотов вы хотите сохранить. Например:
-- создать ветку pricing_fix на 7 дней и сохранять только последние 2 снапшотаALTER TABLE sales CREATE BRANCH pricing_fixRETAIN 7 DAYSWITH SNAPSHOT RETENTION 2 SNAPSHOTS;
Здесь мы говорим Iceberg две вещи: ветка pricing_fix должна жить 7 дней (даже если она простаивает), но сохранять только последние 2 снапшота в этой ветке. Пройдёт 7 дней без записей — ветка удалится. Накопится больше двух снапшотов — старые автоматически очистятся.
Для тегов, которые отлично подходят для сохранения важных вещей, таких как данные на конец квартала, контрольные точки и всё такое, даже критичные снапшоты не должны жить вечно. Еженедельные теги прошлого года никто не смотрит. Даже квартальные снапшоты могут истечь после разумного периода.
Эта команда создаёт тег Q3_report на снапшоте 2 и говорит Iceberg сохранять этот снапшот живым 365 дней, независимо от других операций очистки:
ALTER TABLE sales CREATE TAG Q3_reportON VERSION 2RETAIN 365 DAYS;
Таким образом, тег получает год жизни. Этот снапшот получает год жизни вместе с ним. В то время как более новые снапшоты в main могут быть очищены раньше в зависимости от других политик удержания, снапшот 2 остаётся заблокированным на год из‑за этого тега.
Write‑Audit‑Publish
Если вы инженер данных, вы, вероятно, знакомы с паттерном Write‑Audit‑Publish (WAP). Если нет, это трёхэтапный подход, чтобы предотвратить попадание плохих данных к пользователям и другим системам, чтобы они не стали видимыми в продакшене. Идея проста:
- Write — вы записываете новые данные в изолированное окружение, которое никто другой пока не видит, только вы. Может быть, это отличные данные. Это самые лучшие данные. Это очень стильные данные. Но, может быть, это мусор. Неважно, потому что это не в продакшене.
- Audit — затем вы запускаете свой набор валидаций. Проверяете на null там, где их не должно быть. Ищете дубликаты, проверяете целостность схемы, делаете всю свою очистку. Это 90% вашей жизни, знаете ли. И это ваш шанс поймать проблемы, прежде чем их увидит кто‑то ещё.
- Publish — только после прохождения валидации вы коммитите в продакшен. Переключение атомарно. Старые данные в один момент, новые данные в следующий. Нет частичных обновлений.
Этот процесс действительно помогает, когда что‑то идёт не так. Например, если валидация не прошла, просто отбросьте эту ветку и попробуйте снова. Никакой сложности отката, никакой порчи продакшена, никаких объяснений вашему вице‑президенту, почему выручка внезапно стала отрицательной на дашборде.
В Iceberg этот режим встроен, поэтому такой паттерн очень простой:
-- включить WAP режимALTER TABLE sales SET TBLPROPERTIES ( 'write.wap.enabled' = 'true');SET spark.wap.branch = etl_run_20251030; -- 1. WRITE: добавление продаж от партнеровINSERT INTO salesSELECT * FROM affiliates_sales_pipeline(); -- 2. AUDIT: Проверка данныхSELECT COUNT(*), SUM(price)FROM sales.branch_etl_run_20251030; -- За. Если всё OK: PUBLISHCALL system.fast_forward('sales', 'main' , 'etl_run_20251030'); -- 3b. Если НЕ OK: DROP BRANCHALTER TABLE sales DROP BRANCH etl_run_20251030;RESET spark.wap.branch;
Сначала включите его и дайте вашей сессии имя ветки. Любой идентификатор работает. Здесь у нас etl_run_2025_1030. С этого момента каждая запись — insert, update, delete — полностью пропускает main и идёт в WAP‑ветку, которую Iceberg создаёт автоматически. Последний шаг: если всё проверено, опубликуйте в домене с помощью Fast‑forward. Если нет — удалите ветку. Просто и безопасно.
Ветвление и тегирование превращают ваши таблицы в активы с контролем версий. Та же дисциплина, которую мы применяли к коду десятилетиями, теперь для данных. Тестируйте изменения безопасно, валидируйте перед публикацией, деплойте без скрещённых пальцев. Это здорово.
Так что идите и создайте ту ветку для рискованного преобразования, которого вы избегали. Удалите её, если она провалится, продвиньте её, если она работает. Продакшен останется чистым в любом случае.
«Ветвление в Iceberg — это как Git для данных: можно экспериментировать в изоляции, а потом либо смерджить, либо выбросить, не испортив основную историю. Только не забудьте удалить старые ветки, иначе ваш data lake превратится в лес забытых экспериментов.»