Files
mini-lakehouse-lab/plans/module-08-maintenance-and-final-lab.md
T
ddadmin 4a736f6c4e docs(plan): добавлен план модуля 8 по обслуживанию таблиц
- Зачем:
  - нужно показать базовые операции обслуживания Iceberg-таблиц и финальную практику.
- Что:
  - создан файл plans/module-08-maintenance-and-final-lab.md с планом модуля.
  - обновлен plans/README.md с записью о новом модуле в таблицу содержания.
- Проверка:
  - проверка согласованности ссылок и структуры документации.
2026-03-07 23:13:37 +03:00

522 lines
47 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Модуль 8. Базовое обслуживание таблиц и финальная практика
**Статус:** `Draft`
**Последнее обновление:** `2026-03-07`
## Цель
Научить студента базовому обслуживанию Iceberg-таблиц (compaction и expire_snapshots) и закрепить все навыки курса в финальной сквозной практике. Студент создаёт демо-таблицу с искусственной проблемой мелких файлов, наблюдает деградацию, выполняет compaction и expire_snapshots, сопоставляет обслуживание с Greenplum AppendOnly, а затем самостоятельно проходит полный цикл `raw → bronze → silver → Trino → schema evolution → обслуживание` на отдельном мини-наборе данных. Результат: студент понимает, зачем Iceberg-таблицам нужно периодическое обслуживание, и может самостоятельно выполнить типовые DE-операции в Lakehouse-стенде.
## Результат для студента
После прохождения модуля студент:
- понимает проблему мелких файлов (small files problem) и её влияние на производительность чтения;
- умеет выполнить compaction (`rewrite_data_files`) и проверить результат через metadata-таблицы Iceberg;
- умеет выполнить `expire_snapshots` и понимает компромисс: очистка хранилища vs потеря time travel;
- знает порядок обслуживания: сначала compaction, потом expire_snapshots;
- может сопоставить обслуживание Iceberg с обслуживанием Greenplum AppendOnly-таблиц;
- может самостоятельно выполнить полный цикл `raw → bronze → silver → Trino → schema evolution → обслуживание`;
- прошёл финальный checkpoint курса и может объяснить ключевые архитектурные и прикладные принципы Lakehouse.
## Deliverables
- `notebooks/08_maintenance_and_final_lab.ipynb` — основной ноутбук Модуля 8;
- обновление `plans/README.md` — строка Модуля 8 в оглавлении.
Инфраструктурные изменения не требуются: Python-пакет `trino` уже добавлен в `jupyter/Dockerfile` в Модуле 6.
## Зависимости
### Предусловия
- Стенд поднят и работает (Модуль 1).
- Студент прошёл Модуль 5: таблица `lakehouse.silver.nyc_taxi_yellow` существует (не менее 24 колонок).
- Студент прошёл Модуль 6: Python `trino` клиент установлен, helper `trino_query()` используется для Trino-запросов.
- Raw-данные загружены в MinIO (Модуль 3): `s3a://lakehouse/raw/nyc_taxi/yellow_tripdata_*.parquet`.
- Модуль 7 рекомендуется, но не является жёстким предусловием: концепции snapshot-ов кратко повторяются в контексте обслуживания.
### Что используют последующие модули
Это финальный модуль курса. Downstream-зависимостей нет.
## Дизайн-решения
### Демо-таблица для обслуживания: искусственная проблема мелких файлов
Для демонстрации compaction и expire_snapshots создаётся таблица `lakehouse.default.taxi_maintenance_demo`. Silver-таблица не подходит: она создана одним CTAS и уже имеет оптимальную файловую структуру (мало крупных файлов). Демо-таблица намеренно фрагментируется:
- CTAS с 500 строками (snapshot 1, 1 data file);
- 8 мелких INSERT INTO по ~100 строк каждый (snapshots 29, 8 новых data files);
- итого: ~1300 строк, 9 data files, 9 snapshots.
Это создаёт наглядную проблему мелких файлов: 9 файлов вместо одного оптимального.
### Compaction: rewrite_data_files
`CALL lakehouse.system.rewrite_data_files(table => 'default.taxi_maintenance_demo')` — объединяет мелкие data files в меньшее количество крупных. Ключевые моменты для студента:
- создаёт новый snapshot (данные переписаны в новые файлы);
- старые data files остаются в MinIO до `expire_snapshots` — они ещё нужны для time travel к старым snapshot-ам;
- аналогия: дефрагментация диска или `ALTER TABLE ... REORGANIZE` в Greenplum.
### Порядок обслуживания: compaction → expire_snapshots
Осознанный порядок:
1. Сначала compaction — создаёт новые оптимальные файлы.
2. Потом expire_snapshots — удаляет старые snapshot-ы и файлы, на которые они ссылались (включая мелкие файлы до compaction).
Если сделать наоборот: expire_snapshots удалит snapshot-ы, но старые мелкие файлы останутся, потому что текущий snapshot ещё на них ссылается. Compaction после этого создаст новые файлы, но старые мелкие уже не будут привязаны ни к одному snapshot-у и станут orphan-файлами.
### expire_snapshots: компромисс между очисткой и time travel
`CALL lakehouse.system.expire_snapshots(table => '...', retain_last => 2)` — удаляет все snapshot-ы, кроме N последних. Ключевые моменты:
- удалённые snapshot-ы недоступны для time travel — это необратимо;
- по документации Iceberg, data files, на которые ссылались только удалённые snapshot-ы, должны удаляться из хранилища вместе со snapshot-ами. Однако поведение зависит от версии Iceberg, конфигурации каталога и файловой системы — **необходима проверка на стенде**. Если файлы не удаляются автоматически, в ноутбуке нужно явно отметить это и упомянуть `remove_orphan_files` как дополнительный шаг;
- `retain_last` — количество snapshot-ов, которые нужно сохранить (самые свежие);
- в production: expire_snapshots запускается периодически (Airflow, cron) с разумным retain_last (например, 10–30 дней истории).
Точный синтаксис (именованные параметры, `older_than` vs `retain_last`) зависит от версии Iceberg — нужна проверка на стенде при реализации.
### remove_orphan_files: упоминание без демонстрации
`remove_orphan_files` удаляет файлы в хранилище, которые не принадлежат ни одному snapshot-у (например, от упавших записей). В курсе упоминается для полноты картины, но не демонстрируется: при штатной работе orphan-файлы не возникают, и процедура нужна только для edge cases.
### Сравнение с silver: контрастная демонстрация
После демонстрации проблемы мелких файлов на демо-таблице показываем файловую статистику silver-таблицы. Ожидаемый результат: silver имеет мало крупных файлов (создан одним CTAS). Контрастный вывод: compaction нужен после множества мелких записей, а не после однократной загрузки.
### Greenplum AppendOnly: короткая параллель
Greenplum AppendOnly-таблицы имеют аналогичные проблемы:
- после UPDATE/DELETE в AppendOnly остаются «мёртвые» строки (dead tuples);
- `VACUUM` очищает мёртвые строки, `ALTER TABLE ... REORGANIZE` перестраивает сегменты;
- в Iceberg: `rewrite_data_files` аналог реорганизации, `expire_snapshots` аналог очистки устаревших версий.
Ключевое сходство: обе системы требуют периодического обслуживания. Ни Iceberg, ни AppendOnly не делают это автоматически в базовой конфигурации (в отличие от autovacuum для heap-таблиц в PostgreSQL).
### Финальная практика: отдельный namespace lakehouse.final
Финальная сквозная практика выполняется в отдельном namespace `lakehouse.final`, чтобы:
- не затрагивать основные таблицы bronze и silver;
- позволить студенту создать namespace самостоятельно (навык из Модуля 4);
- обеспечить чистый cleanup в конце.
Студент создаёт две таблицы (`trips_bronze`, `trips_silver`), проводит весь цикл, после чего удаляет namespace.
### Финальная практика: использование существующих raw-данных
Студент читает raw-данные из MinIO (`s3a://lakehouse/raw/nyc_taxi/yellow_tripdata_*.parquet`), уже загруженные в Модуле 3. Повторная загрузка в MinIO не требуется. Для контроля объёма — LIMIT 5000 строк на этапе чтения raw.
### Независимость от Модуля 7
Модуль 8 работает независимо от того, прошёл ли студент Модуль 7:
- если Модуль 7 пройден: silver может содержать колонку `loaded_at` — Модуль 8 не зависит от её наличия;
- если студент перезапустил Модуль 5 после Модуля 7: silver будет без `loaded_at` — Модуль 8 продолжает работать;
- концепции snapshot-ов кратко повторяются при объяснении expire_snapshots.
### Cleanup
- Демо-таблица `taxi_maintenance_demo` удаляется после демонстрации.
- Финальная практика: таблицы `lakehouse.final.trips_bronze` и `lakehouse.final.trips_silver` удаляются, namespace `lakehouse.final` удаляется.
- Silver (`lakehouse.silver.nyc_taxi_yellow`) и bronze (`lakehouse.bronze.nyc_taxi_yellow`) сохраняются — студент может продолжить эксперименты.
- Spark-сессия останавливается.
## План работ
1. Создать `notebooks/08_maintenance_and_final_lab.ipynb` по ячеечной структуре ниже.
2. Обновить `plans/README.md` — добавить строку Модуля 8 в таблицу оглавления.
3. Валидация: выполнить ноутбук сверху вниз на поднятом стенде после Модуля 6 (silver-таблица и Trino-доступ на месте, raw-данные в MinIO).
## Структура ноутбука
### Секция 0: Введение
- **[md]** Заголовок, цели модуля, prerequisites (Модуль 6 пройден, Модуль 7 рекомендуется). Две части модуля:
1. Базовое обслуживание: compaction и expire_snapshots.
2. Финальная практика: сквозной end-to-end сценарий.
- **[md]** Зачем нужно обслуживание. Iceberg-таблица — не «чёрный ящик». После множества записей накапливаются мелкие файлы и старые snapshot-ы. Без обслуживания: чтение замедляется (много мелких файлов), хранилище растёт (старые данные не удаляются). Это знакомо из мира Greenplum: AppendOnly-таблицы тоже требуют VACUUM и REORGANIZE.
- **[md]** Таблица-сравнение (предварительный обзор):
| | Greenplum AppendOnly | Iceberg |
|---|---|---|
| Проблема | Мёртвые строки после UPDATE/DELETE | Мелкие файлы после множества INSERT |
| Компактификация | `ALTER TABLE ... REORGANIZE` | `rewrite_data_files` |
| Очистка устаревших версий | Нет встроенной истории версий | `expire_snapshots` |
| Очистка мёртвых строк/файлов | `VACUUM` | `expire_snapshots` (удаляет старые файлы) |
| Автоматизация в базовой конфигурации | Ручной запуск | Ручной запуск |
| В production | Расписание через cron/Airflow | Расписание через cron/Airflow |
### Секция 1: Spark-сессия, Trino-клиент и проверка таблиц
- **[code]** SparkSession (паттерн Модулей 2–7). Константы: `SILVER_TABLE = "lakehouse.silver.nyc_taxi_yellow"`, `DEMO_TABLE = "lakehouse.default.taxi_maintenance_demo"`, `RAW_PATH = "s3a://lakehouse/raw/nyc_taxi/yellow_tripdata_*.parquet"`.
- **[code]** Assert: silver существует и непуста. Отсылка к Модулю 5.
- **[code]** Assert: raw-данные доступны в MinIO. Проверка через `spark.read.parquet(RAW_PATH).limit(1)` — при ошибке понятное сообщение с отсылкой к Модулю 3: «Raw-данные не найдены по пути {RAW_PATH}. Вернись в Модуль 3 и выполни загрузку данных в MinIO.» Raw нужен для финальной практики (Секция 9).
- **[code]** Helper `trino_query(sql)` (паттерн Модуля 6).
- **[md]** Silver на месте, raw-данные доступны, Trino готов. В первой части модуля создадим демо-таблицу и научимся её обслуживать. Во второй — финальная практика.
### Секция 2: Проблема мелких файлов
- **[md]** Каждый `INSERT INTO` в Iceberg создаёт новые data files. Если вставлять данные мелкими порциями (частые микробатчи, ручные INSERT-ы, инкрементальные загрузки), таблица накапливает множество мелких файлов. Это замедляет чтение: движку приходится открывать и читать каждый файл отдельно. Проблема знакома в мире Hadoop/Hive — и в Iceberg она решается через compaction.
- **[code]** Создание демо-таблицы с 500 строками (CTAS из silver):
```python
spark.sql(f"""
CREATE OR REPLACE TABLE {DEMO_TABLE}
USING iceberg AS
SELECT VendorID, tpep_pickup_datetime, tpep_dropoff_datetime,
passenger_count, trip_distance, fare_amount, total_amount,
pickup_borough, pickup_zone
FROM {SILVER_TABLE}
LIMIT 500
""")
print(f"Демо-таблица создана: {spark.table(DEMO_TABLE).count()} строк.")
```
- **[code]** 8 мелких INSERT INTO (цикл, по ~100 строк каждый):
```python
for i in range(8):
spark.sql(f"""
INSERT INTO {DEMO_TABLE}
SELECT VendorID, tpep_pickup_datetime, tpep_dropoff_datetime,
passenger_count, trip_distance, fare_amount, total_amount,
pickup_borough, pickup_zone
FROM {SILVER_TABLE}
LIMIT 100
""")
print(f"INSERT {i+1}/8 выполнен.")
print(f"Итого строк: {spark.table(DEMO_TABLE).count()}")
```
- **[code]** Файловая статистика через metadata-таблицу:
```python
files_df = spark.sql(f"""
SELECT file_path, file_format, record_count, file_size_in_bytes
FROM {DEMO_TABLE}.files
""")
print(f"Количество data files: {files_df.count()}")
files_df.show(truncate=40)
```
- **[code]** Snapshot-история:
```python
spark.sql(f"SELECT snapshot_id, committed_at, operation, summary FROM {DEMO_TABLE}.snapshots").show(truncate=False)
```
- **[md]** Результат: 9 data files (1 от CTAS + 8 от INSERT), 9 snapshot-ов. Каждый INSERT создал отдельный маленький файл. В production-сценарии после недель инкрементальных загрузок таблица может накопить сотни и тысячи мелких файлов.
- **[md]** Для сравнения — посмотрим на silver-таблицу, которая была создана одним CTAS:
- **[code]** Файловая статистика silver:
```python
silver_files = spark.sql(f"SELECT file_path, record_count, file_size_in_bytes FROM {SILVER_TABLE}.files")
print(f"Silver: {silver_files.count()} data file(s)")
silver_files.show(truncate=40)
```
- **[md]** Silver имеет мало крупных файлов — проблемы мелких файлов нет. Compaction нужен не всегда, а после множества мелких записей. Далее — исправляем проблему на демо-таблице.
### Секция 3: Compaction — rewrite_data_files
- **[md]** `rewrite_data_files` объединяет мелкие data files в меньшее количество крупных. Аналогия: дефрагментация диска. Или `ALTER TABLE ... REORGANIZE` в Greenplum, который перестраивает сегменты AppendOnly-таблицы.
- **[code]** Файловая статистика ДО compaction (сохраняем для сравнения):
```python
files_before = spark.sql(f"SELECT count(*) AS file_count, sum(file_size_in_bytes) AS total_bytes FROM {DEMO_TABLE}.files").first()
print(f"До compaction: {files_before['file_count']} файлов, {files_before['total_bytes']} байт")
```
- **[code]** Compaction:
```python
spark.sql(f"CALL lakehouse.system.rewrite_data_files(table => 'default.taxi_maintenance_demo')")
print("Compaction выполнен.")
```
- **[code]** Файловая статистика ПОСЛЕ compaction:
```python
files_after = spark.sql(f"SELECT count(*) AS file_count, sum(file_size_in_bytes) AS total_bytes FROM {DEMO_TABLE}.files").first()
print(f"После compaction: {files_after['file_count']} файлов, {files_after['total_bytes']} байт")
print(f"Было {files_before['file_count']} файлов → стало {files_after['file_count']}")
```
- **[code]** Проверка данных: count не изменился.
```python
print(f"Строк в таблице: {spark.table(DEMO_TABLE).count()}")
```
- **[md]** Данные не изменились — только реорганизованы. Compaction создал новый snapshot с новыми, более крупными файлами. Но старые мелкие файлы всё ещё лежат в MinIO: они нужны для time travel к старым snapshot-ам. Чтобы освободить место — нужен `expire_snapshots`.
- **[code]** Snapshot-история: появился новый snapshot от compaction.
```python
spark.sql(f"SELECT snapshot_id, committed_at, operation FROM {DEMO_TABLE}.snapshots").show(truncate=False)
```
### Секция 4: expire_snapshots — очистка истории
- **[md]** После compaction в таблице 10 snapshot-ов: 9 от INSERT-ов + 1 от compaction. Старые snapshot-ы ссылаются на мелкие файлы, которые уже не нужны для текущего чтения. `expire_snapshots` удаляет старые snapshot-ы из метаданных. Data files, на которые ссылались только удалённые snapshot-ы, по спецификации Iceberg тоже должны удаляться — но это зависит от конфигурации стенда (проверим на практике).
- **[md]** Важный компромисс: после expire_snapshots time travel к удалённым snapshot-ам невозможен. Это необратимо. В production нужно решить: сколько истории хранить? Обычно 10–30 дней. Для демо — оставим 2 последних snapshot-а.
- **[code]** Snapshot-история ДО expire (сохраняем count):
```python
snapshots_before = spark.sql(f"SELECT snapshot_id, committed_at, operation FROM {DEMO_TABLE}.snapshots")
print(f"Snapshot-ов до expire: {snapshots_before.count()}")
snapshots_before.show(truncate=False)
# Сохраняем ID первого snapshot для проверки time travel после expire
first_snapshot_id = snapshots_before.orderBy("committed_at").first()["snapshot_id"]
print(f"Самый старый snapshot: {first_snapshot_id}")
```
- **[code]** expire_snapshots:
```python
spark.sql(f"""
CALL lakehouse.system.expire_snapshots(
table => 'default.taxi_maintenance_demo',
retain_last => 2
)
""")
print("expire_snapshots выполнен.")
```
- **[code]** Snapshot-история ПОСЛЕ expire:
```python
snapshots_after = spark.sql(f"SELECT snapshot_id, committed_at, operation FROM {DEMO_TABLE}.snapshots")
print(f"Snapshot-ов после expire: {snapshots_after.count()}")
snapshots_after.show(truncate=False)
```
- **[code]** Попытка time travel к удалённому snapshot (ожидаем ошибку):
```python
try:
spark.sql(f"""
SELECT count(*) FROM {DEMO_TABLE}
VERSION AS OF {first_snapshot_id}
""").show()
print("Time travel сработал (snapshot не был удалён).")
except Exception as e:
print(f"Ожидаемая ошибка: {e}")
print("Snapshot удалён — time travel невозможен.")
```
- **[md]** Snapshot удалён, time travel невозможен. Это главное следствие expire_snapshots — необратимая потеря возможности отката к старым состояниям. В Модуле 7 мы использовали snapshot-ы для восстановления после ошибок — `expire_snapshots` лишает этой возможности для удалённых snapshot-ов. Поэтому expire нужно запускать обдуманно: не слишком агрессивно (потеря страховки), не слишком редко (рост метаданных и хранилища).
- **[md]** Примечание: физическое удаление data files из MinIO после expire зависит от конфигурации стенда. Если файлы остались — для их очистки существует отдельная процедура `remove_orphan_files`, которая удаляет файлы, не принадлежащие ни одному snapshot-у. В штатном режиме orphan-файлы не возникают (кроме как после expire или упавших записей). В этом курсе `remove_orphan_files` не демонстрируется.
### Секция 5: Порядок обслуживания и типичные ошибки
- **[md]** Правильный порядок обслуживания Iceberg-таблицы:
| Шаг | Операция | Что делает | Что будет, если пропустить |
|---|---|---|---|
| 1 | `rewrite_data_files` | Объединяет мелкие файлы в крупные | Чтение остаётся медленным |
| 2 | `expire_snapshots` | Удаляет старые snapshot-ы (и их файлы, если конфигурация позволяет) | Метаданные и хранилище растут без ограничений |
Если сделать наоборот (сначала expire, потом compaction): expire удалит snapshot-ы, но мелкие файлы останутся (текущий snapshot всё ещё на них ссылается). Compaction создаст новые файлы, но старые мелкие станут orphan-файлами — нужен будет дополнительный `remove_orphan_files`.
- **[md]** Типичные ошибки обслуживания:
**Ошибка 1: Никогда не запускать compaction.** Таблица после месяцев инкрементальных загрузок имеет тысячи мелких файлов. Чтение занимает минуты вместо секунд.
**Ошибка 2: expire_snapshots с `retain_last => 1` сразу после ошибки.** Студент вносит ошибку в данные (Модуль 7), затем запускает expire_snapshots, оставляя только последний (ошибочный) snapshot. Rollback к предыдущему состоянию невозможен. Правило: перед expire проверь, что текущее состояние таблицы корректно.
**Ошибка 3: Запускать обслуживание на каждый INSERT.** Compaction и expire_snapshots — тяжёлые операции. Запускать их после каждой записи — расточительно. В production: по расписанию (раз в день/неделю), а не на каждый батч.
### Секция 6: Параллель с Greenplum AppendOnly
- **[md]** Развернутое сравнение для студентов с опытом Greenplum:
| Аспект | Greenplum AppendOnly | Iceberg |
|---|---|---|
| Как накапливается «мусор» | UPDATE/DELETE оставляют мёртвые строки в сегментах | Множество INSERT создают мелкие data files |
| Влияние на чтение | Сканирование мёртвых строк замедляет запросы | Открытие множества мелких файлов замедляет запросы |
| Компактификация | `ALTER TABLE ... REORGANIZE` (перестройка сегментов) | `rewrite_data_files` (объединение файлов) |
| Очистка | `VACUUM` (удаление мёртвых строк) | `expire_snapshots` (удаление старых snapshot-ов; файлы — при поддержке конфигурацией) |
| Автоматизация | `autovacuum` для heap-таблиц, ручной VACUUM для AO | Ручной запуск, автоматизация через Airflow/cron |
| Просмотр состояния | `pg_stat_all_tables`, `gp_toolkit.gp_bloat_diag` | `table.files`, `table.snapshots` (metadata-таблицы Iceberg) |
| Time travel после очистки | Невозможен (нет встроенной истории) | Невозможен для удалённых snapshot-ов, возможен для оставшихся |
- **[md]** Ключевое сходство: ни одна система не обслуживает себя полностью автоматически в базовой конфигурации. И в Greenplum, и в Iceberg — это задача инженера или оркестратора. Ключевое отличие: Iceberg даёт встроенную историю (snapshot-ы) и контролируемую очистку (`expire_snapshots` с `retain_last`), чего нет в Greenplum.
### Секция 7: Самостоятельное задание — обслуживание
- **[md]** Две задачи для закрепления навыков обслуживания.
**Задача 1 (анализ silver).** Посмотри файловую статистику silver-таблицы (`lakehouse.silver.nyc_taxi_yellow`): количество data files, средний размер файла, количество snapshot-ов. Сравни с демо-таблицей до compaction. Нужна ли silver compaction? Ответь в markdown-ячейке и объясни — при каких условиях silver потребовала бы compaction?
**Задача 2 (практика).** Создай таблицу `lakehouse.default.taxi_compaction_practice` через CTAS (500 строк из silver). Выполни 5 INSERT INTO по 200 строк. Посмотри файловую статистику, выполни compaction и expire_snapshots (retain_last => 1). Проверь результат. Удали таблицу.
- **[code]** Пустая ячейка: `# Ваш код: файловая статистика silver (файлы, размеры, snapshot-ы)`
- **[md]** Пустая markdown-ячейка: `Ваш ответ на Задачу 1: нужна ли compaction для silver? При каких условиях потребовалась бы? ...`
- **[code]** Пустая ячейка: `# Ваш код: создание taxi_compaction_practice + 5 INSERT`
- **[code]** Пустая ячейка: `# Ваш код: файловая статистика до compaction`
- **[code]** Пустая ячейка: `# Ваш код: compaction + expire_snapshots`
- **[code]** Пустая ячейка: `# Ваш код: файловая статистика после + DROP TABLE`
### Секция 8: Удаление демо-таблицы
- **[md]** Демо-таблица для обслуживания больше не нужна.
- **[code]** `spark.sql(f"DROP TABLE IF EXISTS {DEMO_TABLE}")`. Вывод подтверждения.
### Секция 9: Финальная практика — сквозной end-to-end сценарий
- **[md]** Финальная практика курса. Студент самостоятельно проходит полный цикл Lakehouse-пайплайна: от raw-данных до обслуживания таблицы. Все навыки из Модулей 1–8 в одном сценарии.
Работа выполняется в отдельном namespace `lakehouse.final`, чтобы не затрагивать основные таблицы курса. После завершения — cleanup.
Подсказки: путь к raw-данным — `s3a://lakehouse/raw/nyc_taxi/yellow_tripdata_*.parquet` (Модуль 3). Для контроля объёма используй `LIMIT 5000`.
- **[md]** **Сценарий: 8 шагов.**
**Шаг 1 (namespace).** Создай namespace `lakehouse.final`.
**Шаг 2 (bronze).** Прочитай raw parquet из MinIO (`s3a://lakehouse/raw/nyc_taxi/yellow_tripdata_*.parquet`, LIMIT 5000 строк) и создай таблицу `lakehouse.final.trips_bronze` через CTAS. Это bronze: данные «как есть», без трансформаций.
**Шаг 3 (silver).** Создай `lakehouse.final.trips_silver` из bronze с тремя трансформациями:
- Фильтр: `fare_amount >= 0` (убрать аномалии)
- Нормализация: `passenger_count` → INT с обработкой NULL (`coalesce` + `cast`)
- Обогащение: вычисляемая колонка `trip_duration_minutes`
**Шаг 4 (Trino).** Прочитай `lakehouse.final.trips_silver` из Trino. Сравни count с результатом из Spark.
**Шаг 5 (schema evolution).** Добавь колонку `processed_at TIMESTAMP` к `trips_silver` через `ALTER TABLE ADD COLUMNS`. Проверь из Trino, что колонка видна.
**Шаг 6 (snapshot-ы).** Выполни 3 INSERT INTO `trips_silver` по ~300 строк из `trips_bronze`. Каждый INSERT должен включать те же трансформации, что и в шаге 3, плюс `current_timestamp() AS processed_at` для новой колонки из шага 5. Подсказка: если не указать `processed_at`, Spark выдаст ошибку несовпадения числа колонок; альтернативный вариант — использовать явный список target-колонок в `INSERT INTO trips_silver (col1, col2, ...)`. Создай snapshot-историю. Посмотри файловую статистику.
**Шаг 7 (обслуживание).** Выполни compaction (`rewrite_data_files`), затем `expire_snapshots` (retain_last => 1). Проверь: файлов стало меньше, snapshot-ов осталось 1.
**Шаг 8 (cleanup).** DROP TABLE `trips_silver`, DROP TABLE `trips_bronze`, DROP NAMESPACE `lakehouse.final`.
- **[code]** Пустая ячейка: `# Шаг 1: CREATE NAMESPACE`
- **[code]** Пустая ячейка: `# Шаг 2: прочитай raw и создай trips_bronze`
- **[code]** Пустая ячейка: `# Шаг 2: проверка trips_bronze (count, printSchema)`
- **[code]** Пустая ячейка: `# Шаг 3: создай trips_silver с трансформациями`
- **[code]** Пустая ячейка: `# Шаг 3: проверка trips_silver (count, проверка фильтра)`
- **[code]** Пустая ячейка: `# Шаг 4: чтение trips_silver из Trino`
- **[code]** Пустая ячейка: `# Шаг 5: ALTER TABLE ADD COLUMNS`
- **[code]** Пустая ячейка: `# Шаг 5: проверка из Trino (DESCRIBE)`
- **[code]** Пустая ячейка: `# Шаг 6: 3 INSERT INTO trips_silver`
- **[code]** Пустая ячейка: `# Шаг 6: файловая статистика и snapshot-ы`
- **[code]** Пустая ячейка: `# Шаг 7: compaction + expire_snapshots`
- **[code]** Пустая ячейка: `# Шаг 7: проверка результата`
- **[code]** Пустая ячейка: `# Шаг 8: cleanup (DROP TABLE, DROP NAMESPACE)`
### Секция 10: Финальный checkpoint
- **[md]** **Часть A — Обслуживание таблиц (Модуль 8):**
1. Что такое проблема мелких файлов и когда она возникает?
2. Что делает `rewrite_data_files`? Удаляет ли он старые файлы?
3. Что делает `expire_snapshots`? Какой компромисс он создаёт?
4. В каком порядке нужно выполнять compaction и expire_snapshots? Почему?
5. Как обслуживание Iceberg-таблиц соотносится с VACUUM/REORGANIZE в Greenplum?
- **[md]** **Часть B — Весь курс (итоговые вопросы):**
1. Назови три роли в архитектуре Lakehouse и какие компоненты стенда их выполняют.
2. Почему Spark может записать данные, а Trino прочитать ту же таблицу без копирования?
3. Чем raw отличается от bronze? Чем bronze отличается от silver?
4. Что такое snapshot в Iceberg? Чем он отличается от бэкапа в PostgreSQL?
5. Назови три безопасные и три опасные операции с Iceberg-таблицей.
6. Зачем нужны compaction и expire_snapshots в production-сценарии?
### Секция 11: Завершение
- **[md]** Итог модуля:
- Compaction (`rewrite_data_files`) — решает проблему мелких файлов, не меняя данные.
- `expire_snapshots` — очищает старые snapshot-ы и файлы, но лишает возможности time travel.
- Порядок: compaction → expire_snapshots.
- Обслуживание Iceberg похоже на обслуживание Greenplum AppendOnly: обе системы требуют периодической заботы.
- **[md]** Итог курса. Что студент теперь умеет:
1. Поднимать и диагностировать локальный Lakehouse-стенд (Модуль 1).
2. Объяснять роли storage, catalog, compute (Модуль 2).
3. Загружать raw-данные и проверять схему (Модуль 3).
4. Создавать и заполнять Iceberg-таблицы (Модуль 4).
5. Строить воспроизводимый поток raw → bronze → silver (Модуль 5).
6. Читать одну таблицу из Spark и Trino (Модуль 6).
7. Безопасно изменять таблицы: schema evolution, time travel, rollback (Модуль 7).
8. Обслуживать таблицы: compaction, expire_snapshots (Модуль 8).
Это базовый набор навыков для безопасной работы в Lakehouse-стенде. Дальше — production: партиционирование, MERGE, streaming, Airflow, governance. Но фундамент заложен.
- **[code]** `spark.stop()`.
### Дизайн-решения по ноутбуку
- **Исполняемый формат:** Spark SQL для compaction и expire_snapshots (stored procedures). Trino-запросы через Python `trino` клиент для downstream-верификации в финальной практике.
- **DBeaver-подсказки:** markdown-заметки рядом с Trino-ячейками (паттерн Модуля 6).
- **Helper-код:** `trino_query(sql)` — переиспользование из Модуля 6. Файловая статистика через metadata-таблицы Iceberg (`.files`, `.snapshots`).
- **Cleanup:** DROP демо-таблицы `taxi_maintenance_demo`. Финальная практика: DROP таблиц в namespace `final`, DROP namespace. Silver и bronze сохраняются.
- **Порядок:** проблема мелких файлов → compaction → expire_snapshots → Greenplum-параллель → ошибки → самостоятельное задание (обслуживание) → финальная практика (end-to-end) → checkpoint (модуль + курс) → итог курса.
## Checkpoint
Студент должен уметь:
- объяснить проблему мелких файлов и показать файловую статистику через metadata-таблицы;
- выполнить compaction и проверить, что файлов стало меньше, а данные не изменились;
- выполнить expire_snapshots и объяснить, почему time travel к удалённым snapshot-ам невозможен;
- назвать правильный порядок обслуживания и объяснить, почему он важен;
- сопоставить обслуживание Iceberg с обслуживанием Greenplum AppendOnly;
- самостоятельно выполнить полный цикл: raw → bronze → silver → Trino → schema evolution → обслуживание;
- ответить на итоговые вопросы курса по архитектуре, слоям, snapshot-ам и безопасным операциям.
## Acceptance Criteria
- `notebooks/08_maintenance_and_final_lab.ipynb` выполняется сверху вниз после прохождения Модуля 6 без внешних зависимостей (DBeaver не обязателен, Модуль 7 рекомендуется, но не обязателен).
- Ноутбук содержит: демонстрацию проблемы мелких файлов, compaction (`rewrite_data_files`), expire_snapshots, финальную сквозную практику.
- Методика курса: объяснение → демонстрация → самостоятельное повторение → checkpoint.
- Паттерны Модулей 1–7: SparkSession без `.master()`, `setLogLevel("ERROR")`, `trino_query()`.
- Демо-таблица `taxi_maintenance_demo` создаётся и удаляется внутри ноутбука.
- Финальная практика в отдельном namespace `lakehouse.final` с полным cleanup в конце.
- При отсутствии silver — assert с отсылкой к Модулю 5. При отсутствии raw-данных в MinIO — assert с отсылкой к Модулю 3.
- Текст на русском с параллелями к PostgreSQL/Greenplum.
- Финальный checkpoint покрывает как модуль, так и весь курс.
- Silver и bronze сохраняются после выполнения ноутбука.
- `plans/README.md` содержит строку Модуля 8.
## Риски
- **Синтаксис `rewrite_data_files`.** Точный синтаксис stored procedure зависит от версии Iceberg и Spark. Вариант: `CALL lakehouse.system.rewrite_data_files(table => 'default.taxi_maintenance_demo')`. Возможны различия в именованных vs позиционных параметрах. Нужна проверка на стенде.
- **Синтаксис `expire_snapshots`.** Возможные параметры: `older_than` (TIMESTAMP), `retain_last` (INT). По умолчанию `older_than` = 5 дней назад, что не подходит для демо (snapshot-ы созданы минуты назад). Нужно явно передать `retain_last => 2` и, возможно, `older_than => current_timestamp()`. Проверить на стенде, работает ли `retain_last` без `older_than`.
- **expire_snapshots не удаляет файлы.** В некоторых конфигурациях `expire_snapshots` может не удалять data files автоматически. В этом случае нужно добавить `remove_orphan_files` или явно упомянуть, что файлы удаляются отдельно. Проверить на стенде.
- **Время выполнения цикла INSERT.** 8 INSERT-ов по ~100 строк из silver: каждый INSERT — отдельный Spark job. На стенде это может занять 2–5 минут. Предупреждение в markdown.
- **Время выполнения compaction.** `rewrite_data_files` перечитывает и перезаписывает данные. На демо-таблице (~1300 строк) — секунды. На silver (миллионы строк) — потенциально минуты. Предупреждение в markdown.
- **Финальная практика: путь к raw-данным.** Путь `s3a://lakehouse/raw/nyc_taxi/yellow_tripdata_*.parquet` соответствует маршруту загрузки из Модуля 3. Если студент по какой-то причине загрузил файлы по другому пути, он увидит ошибку при чтении. Добавить подсказку в markdown: «Если путь отличается от указанного, посмотри Модуль 3.»
- **Финальная практика: объём INSERT-ов.** При LIMIT 5000 для bronze и 3 INSERT по 300 строк в silver — объём данных минимален. Compaction может быть no-op (файлы и так небольшие). Решение: в инструкции явно указать, что цель — увидеть изменение количества файлов и snapshot-ов, а не добиться значимого улучшения производительности.
- **Идемпотентность.** `CREATE OR REPLACE TABLE` для демо-таблицы — идемпотентна. Финальная практика: если студент запускает повторно, namespace и таблицы могут уже существовать. `CREATE NAMESPACE IF NOT EXISTS` и `CREATE OR REPLACE TABLE` обеспечивают идемпотентность. DROP в конце — `IF EXISTS`.
- **Независимость от Модуля 7.** Ноутбук не должен ссылаться на колонку `loaded_at` или результаты Модуля 7 как на обязательные предусловия. Snapshot-концепции кратко повторяются в контексте expire_snapshots.
## Out of Scope
- `remove_orphan_files` как отдельная демонстрация — упоминается, не демонстрируется.
- `rewrite_manifests` — оптимизация manifest-файлов, advanced-тема.
- Партиционирование и partition evolution — PRD: вне v1.
- Sort order и write distribution mode — advanced compaction options.
- Автоматизация обслуживания через Airflow/cron — упоминается как production-практика, не реализуется.
- Gold-слой в финальной практике — PRD: вне v1.
- Performance benchmarks (замеры времени чтения до/после compaction) — интересно, но выходит за рамки «базового обслуживания».
- `MERGE`, row-level `DELETE`, `UPDATE` — PRD: вне v1.
- Deep dive в Iceberg metadata JSON / manifest files — PRD: вне v1.