Files
mini-lakehouse-lab/plans/module-04-bronze-with-iceberg.md
T
ddadmin 7ae771df68 docs(plan): добавлен план модуля 4 по bronze-слою
- Зачем:
  - нужен согласованный draft модуля про первую Iceberg-таблицу и переход от raw к bronze.
- Что:
  - добавлен план `module-04-bronze-with-iceberg.md` с deliverables, структурой ноутбука и checkpoints.
  - зафиксированы решения по CTAS, инспекции metadata Iceberg и зависимости модуля 5 от bronze-таблицы.
- Проверка:
  - план вручную сверен с `docs/course_program.md` и `docs/course_prd.md`.
  - runtime-проверка ноутбука и SQL-операций на стенде не выполнялась.
2026-03-07 12:02:02 +03:00

224 lines
21 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.
# Модуль 4. Первая рабочая Iceberg-таблица и слой bronze
**Статус:** `Draft`
**Последнее обновление:** `2026-03-07`
## Цель
Научить студента переходу от набора raw-файлов к управляемой Iceberg-таблице. Студент создаёт namespace `bronze`, записывает raw-данные NYC Taxi в Iceberg-таблицу и осматривает внутреннюю структуру таблицы (snapshots, data files, manifests). Студент понимает разницу между «каталогом с Parquet-файлами» и «управляемой таблицей» и связывает bronze-слой с привычным stg/ods-мышлением.
## Результат для студента
После прохождения модуля студент:
- умеет создать Iceberg namespace и таблицу через Spark SQL;
- умеет загрузить raw-данные из MinIO в bronze-таблицу через CTAS (`CREATE OR REPLACE TABLE ... AS SELECT`);
- понимает разницу между `spark.read.parquet("s3a://...")` (набор файлов) и `spark.table("lakehouse.bronze.nyc_taxi_yellow")` (управляемая таблица);
- умеет осмотреть структуру Iceberg-таблицы: snapshots, history, data files;
- умеет сопоставить физическую структуру в MinIO (metadata/, data/) с логической таблицей;
- понимает, зачем нужен bronze-слой и как он соотносится с stg/ods в классическом DWH;
- понимает, что таблица в Lakehouse — это не «один каталог с файлами», а согласованный набор data files + metadata + catalog entry.
## Deliverables
- `notebooks/04_bronze_with_iceberg.ipynb` — основной ноутбук Модуля 4;
- обновление индекса планов в `plans/README.md`.
Инфраструктурные изменения не требуются: `docker-compose.yml`, `START_HERE.md`, `docs/stack_reference.md` уже содержат всё необходимое после Модуля 3.
## Зависимости
### Предусловия
- Стенд поднят и работает (Модуль 1).
- Студент прошёл Модуль 3: raw-данные загружены в `s3a://lakehouse/raw/nyc_taxi/yellow_tripdata_*.parquet` и `taxi_zone_lookup.csv`.
- Каталог `lakehouse` настроен в `spark-defaults.conf` как JdbcCatalog с warehouse `s3a://lakehouse/warehouse`.
### Что используют последующие модули
- Модуль 5 читает `lakehouse.bronze.nyc_taxi_yellow` для построения silver.
- Модуль 6 читает bronze/silver через Trino.
## Дизайн-решения
### Namespace: `lakehouse.bronze` (не `module_04`)
Семантическое имя, потому что:
- Модуль 5 будет читать `lakehouse.bronze.*` для построения silver — семантическое имя делает связь явной.
- Подкрепляет мышление слоями `raw → bronze → silver` (PRD Risk #4: не смешивать роли слоёв).
- Модуль 2 использовал `module_02` для throwaway-демо с cleanup. Bronze — постоянный слой без cleanup, поэтому другая конвенция.
### Создание и наполнение таблицы: CTAS с явным списком колонок
Используем `CREATE OR REPLACE TABLE ... USING iceberg AS SELECT col1, col2, ... FROM raw_view` (CTAS). Это:
- объединяет создание и наполнение в один шаг — понятнее и ближе к `CREATE TABLE AS SELECT` в PostgreSQL;
- явный список колонок в SELECT исключает ошибки из-за несовпадения порядка или состава колонок (PRD Risk #3, #5);
- `CREATE OR REPLACE` обеспечивает идемпотентность: повторный запуск пересоздаёт таблицу, snapshot history остаётся чистой;
- trade-off: `CREATE OR REPLACE` уничтожает историю — в production это опасно, но для первичной загрузки учебного bronze допустимо. В Модуле 7 студент увидит, почему в рабочих сценариях нужен другой подход.
В Секции 2 DDL с явной схемой показывается как справочный пример (не выполняется) — чтобы студент увидел знакомую форму `CREATE TABLE (col type, ...)`.
### Инспекция Iceberg: три уровня глубины
Модуль 2 показал level 1 (catalog entry в PostgreSQL + файлы в MinIO). Модуль 4 идёт глубже:
- **Spark metadata tables:** `.snapshots`, `.history`, `.files` — встроенные metadata-таблицы Iceberg;
- **Физическая структура в MinIO:** boto3 листинг с разделением `[DATA]` / `[META]`, ASCII-дерево каталогов;
- **Таблица-сравнение** PostgreSQL/Greenplum vs Iceberg (pg_catalog ↔ JDBC catalog, heap files ↔ Parquet data files, WAL/MVCC ↔ snapshots).
### Cleanup: нет
Bronze нужен Модулю 5. Spark-сессия останавливается. Явное пояснение в финале ноутбука: «Мы намеренно не удаляем bronze-таблицы. В Модуле 5 мы будем строить silver на основе bronze.»
### Taxi zone lookup: самостоятельное задание
Студент создаёт `lakehouse.bronze.taxi_zone_lookup` как упражнение. Это подготовка к join в Модуле 5 (silver).
## План работ
1. Создать `notebooks/04_bronze_with_iceberg.ipynb` по ячеечной структуре, описанной ниже.
2. Обновить `plans/README.md` — добавить строку Модуля 4 в таблицу оглавления.
3. Валидация: убедиться, что ноутбук выполняется сверху вниз на поднятом стенде после прохождения Модуля 3 (raw-данные в MinIO).
## Структура ноутбука
### Секция 0: Введение
- **[md]** Заголовок: «Модуль 4. Первая рабочая Iceberg-таблица и слой bronze». Цели модуля. Prerequisite: Модуль 3 пройден, raw-данные в MinIO. Таблица-сравнение:
| | Классический DWH (Greenplum) | Lakehouse |
|---|---|---|
| Raw-данные | Загружены в stg-таблицу через COPY/gpfdist | Файлы в объектном хранилище (raw-зона) |
| Bronze/ODS | `CREATE TABLE AS SELECT` из stg | `CREATE TABLE ... USING iceberg AS SELECT` из raw (CTAS) |
| Метаданные | pg_catalog (внутри СУБД) | Внешний каталог (PostgreSQL JDBC) + файлы metadata в MinIO |
| История изменений | Нет (или WAL на уровне СУБД) | Snapshots и manifests (встроены в формат) |
### Секция 1: Spark-сессия и проверка raw
- **[code]** SparkSession (паттерн Модулей 2-3: без `.master()`, `setLogLevel("ERROR")`).
- **[code]** Быстрая проверка: `spark.read.parquet("s3a://lakehouse/raw/nyc_taxi/yellow_tripdata_*.parquet").count()` + `.printSchema()`. Assert на ненулевой count с отсылкой к Модулю 3.
- **[md]** Сейчас читаем как набор файлов — нет истории, каталогизации, контроля схемы. Дальше создадим управляемую таблицу.
### Секция 2: Создание namespace и знакомство с DDL
- **[md]** Что такое namespace в Iceberg (аналог schema/database в PostgreSQL). Почему `bronze`, а не `module_04`.
- **[code]** `CREATE NAMESPACE IF NOT EXISTS lakehouse.bronze`.
- **[md]** Как выглядит DDL Iceberg-таблицы: показываем полную схему Yellow Taxi как справочный пример (не выполняем). Это знакомая студенту форма — `CREATE TABLE ... (col type, ...)`, как в PostgreSQL. Объясняем ключевое отличие: `USING iceberg` — указание на формат таблицы.
- **[md]** Для создания и одновременного наполнения таблицы данными используем CTAS (CREATE TABLE AS SELECT) — это удобнее, чем отдельные DDL + INSERT. Перечисляем колонки: VendorID, tpep_pickup_datetime, tpep_dropoff_datetime, passenger_count, trip_distance, RatecodeID, store_and_fwd_flag, PULocationID, DOLocationID, payment_type, fare_amount, extra, mta_tax, tip_amount, tolls_amount, improvement_surcharge, total_amount, congestion_surcharge, Airport_fee.
### Секция 3: Загрузка raw → bronze (CTAS)
- **[md]** Аналогия с `CREATE TABLE ods AS SELECT ... FROM stg` в Greenplum. Почему явный список колонок: `SELECT *` привязывается к порядку, а не к именам — это хрупко и может дать некорректное сопоставление полей. Используем `CREATE OR REPLACE TABLE ... AS SELECT col1, col2, ... FROM raw_yellow` — CTAS одновременно создаёт таблицу и наполняет данными.
- **[md]** Идемпотентность: `CREATE OR REPLACE` при повторном запуске пересоздаёт таблицу с чистого листа — snapshot history остаётся чистой (один snapshot = одна загрузка). Trade-off: в production `CREATE OR REPLACE` — разрушительная операция (удаляет историю), но для первичной загрузки учебного bronze это осознанный выбор ради простоты. В Модуле 7 увидим, почему в рабочих сценариях нужен другой подход.
- **[code]** Чтение raw → `createOrReplaceTempView("raw_yellow")``CREATE OR REPLACE TABLE lakehouse.bronze.nyc_taxi_yellow USING iceberg AS SELECT VendorID, tpep_pickup_datetime, ... FROM raw_yellow` (CTAS с явным списком колонок).
- **[code]** Верификация: count bronze vs count raw — должны совпасть.
### Секция 4: Файлы vs управляемая таблица
- **[md]** Ключевой момент модуля: два способа доступа к одним и тем же данным.
- **[code]** Демо обоих способов: `spark.read.parquet("s3a://...")` vs `spark.table("lakehouse.bronze.nyc_taxi_yellow")`. Оба дают одинаковый count и columns, но таблица даёт каталог, историю, схему, доступ через Trino.
- **[md]** Таблица-сравнение свойств:
| Свойство | Raw файлы | Iceberg-таблица |
|---|---|---|
| Каталогизация | Нет | Да (PostgreSQL JDBC) |
| История изменений | Нет | Snapshots |
| Контроль схемы | Выводится из файлов | Зафиксирована в metadata |
| Чтение из Trino | Невозможно | Да |
| Атомарность записи | Нет | Да |
### Секция 5: Осмотр структуры Iceberg
#### 5a: Snapshots
- **[md]** Snapshot = «снимок» состояния таблицы. Аналогия с PostgreSQL: как если бы каждая транзакция оставляла именованный checkpoint.
- **[code]** `SELECT * FROM lakehouse.bronze.nyc_taxi_yellow.snapshots`.
- **[md]** Пояснение полей: `committed_at`, `snapshot_id`, `operation`, `summary`.
#### 5b: History
- **[code]** `SELECT * FROM lakehouse.bronze.nyc_taxi_yellow.history`.
- **[md]** Последовательность snapshots. Каждый — результат конкретной операции.
#### 5c: Data files
- **[code]** `SELECT file_path, file_format, record_count, file_size_in_bytes / 1024 / 1024 AS size_mb FROM lakehouse.bronze.nyc_taxi_yellow.files`.
- **[md]** Каждый data file — отдельный Parquet в MinIO. Iceberg знает, какие файлы принадлежат текущему snapshot.
#### 5d: Физическая структура в MinIO
- **[code]** Листинг через boto3 с разделением `[DATA]` / `[META]` (паттерн из Module 2). Итого по размерам data vs metadata.
- **[md]** ASCII-дерево (после CTAS — один snapshot, одна операция):
```
warehouse/bronze/nyc_taxi_yellow/
├── metadata/
│ ├── v1.metadata.json ← CTAS создал таблицу и записал данные (текущий)
│ ├── snap-*.avro ← manifest list (один snapshot)
│ └── *.avro ← manifests (списки data files)
└── data/
├── *.parquet ← данные
└── ...
```
CTAS создаёт таблицу и наполняет данными за одну операцию, поэтому в metadata один metadata JSON и один snapshot.
В Модуле 7 увидим, как snapshots помогают вернуться к предыдущему состоянию.
### Секция 6: raw → bronze и привычное мышление
- **[md]** Мини-лекция (3-4 абзаца):
- В классическом DWH: stg → ods → dds. В Lakehouse: raw → bronze → silver (→ gold, за рамками курса).
- Bronze — первый управляемый слой. Содержит данные «как есть» из raw, но уже как Iceberg-таблицу: с каталогом, историей, возможностью чтения через Trino.
- Bronze ≠ raw. Raw — неизменяемый архив файлов. Bronze — управляемая таблица, которую можно пересоздать из raw.
- В Модуле 5 — silver: очистка, трансформации, бизнес-логика.
### Секция 7: Самостоятельное задание
- **[md]** Инструкция: (1) прочитать `taxi_zone_lookup.csv` из raw-зоны, (2) создать `lakehouse.bronze.taxi_zone_lookup` с DDL (LocationID INT, Borough STRING, Zone STRING, service_zone STRING), (3) загрузить данные, (4) проверить через `spark.table(...)`, (5) осмотреть snapshots.
- **[code]** 5 пустых ячеек `# Ваш код: ...`.
### Секция 8: Checkpoint
- **[md]** Вопросы для самопроверки:
1. Чем отличается `spark.read.parquet("s3a://...")` от `spark.table("lakehouse.bronze.nyc_taxi_yellow")`?
2. Что хранит snapshot и зачем он нужен?
3. Где физически лежат данные bronze-таблицы и где метаданные?
4. Почему bronze — это не то же самое, что raw?
5. Что произойдёт, если удалить один data file из MinIO, но не изменить metadata?
6. Можете ли вы показать таблицу в MinIO Console (через браузер)?
### Секция 9: Завершение
- **[md]** «Мы намеренно не удаляем bronze-таблицы. В Модуле 5 мы будем строить silver-слой на основе `lakehouse.bronze.nyc_taxi_yellow`.»
- **[code]** `spark.stop()`.
### Дизайн-решения по ноутбуку
- **Helper-код:** весь код inline в ноутбуке. Студент должен видеть каждый шаг: CTAS, metadata-запросы, boto3.
- **Cleanup:** нет. Bronze нужен Модулю 5. Spark-сессия останавливается.
## Checkpoint
Студент должен уметь:
- создать Iceberg namespace и таблицу через Spark SQL;
- загрузить raw-данные в bronze-таблицу;
- объяснить разницу между чтением raw-файлов и чтением управляемой таблицы;
- показать snapshots, history и data files через metadata-таблицы Iceberg;
- показать физическую структуру таблицы в MinIO (через UI или код);
- объяснить, зачем нужен bronze-слой и чем он отличается от raw.
## Acceptance Criteria
- `notebooks/04_bronze_with_iceberg.ipynb` можно выполнить сверху вниз в поднятом Jupyter-окружении после прохождения Модуля 3 (raw-данные в MinIO).
- Ноутбук следует методике курса: `объяснение → демонстрация → самостоятельное повторение → checkpoint`.
- Ноутбук использует паттерны из Модулей 1-3: SparkSession без `.master()`, `setLogLevel("ERROR")`, `boto3` для MinIO.
- Ноутбук идемпотентен: повторное выполнение (`CREATE OR REPLACE`) не дублирует данные и не роняет ячейки.
- CTAS использует явный список колонок, а не `SELECT *`.
- Bronze-таблица `lakehouse.bronze.nyc_taxi_yellow` остаётся после выполнения ноутбука для использования в Модуле 5.
- При отсутствии raw-данных — понятный assert с отсылкой к Модулю 3.
- Текст ноутбука на русском языке с параллелями к PostgreSQL/Greenplum.
- `plans/README.md` содержит строку Модуля 4 в оглавлении.
## Риски
- **Время CTAS.** ~7-10 млн строк за 1-3 мин на стенде (2 workers × 2 GB). Extended (12 мес) — до 5-10 мин. Добавить markdown-предупреждение перед ячейкой загрузки.
- **Схема raw PARQUET.** Фиксируем колонки NYC Taxi 2024 в DDL. Если студент скачал другой год — DDL может не совпасть. Пояснение: придерживаться инструкции из `START_HERE.md`.
- **Column case sensitivity.** NYC Taxi PARQUET использует mixed case (`VendorID`, `PULocationID`). Iceberg хранит имена case-sensitive. DDL должен совпадать с именами в PARQUET. Проверить на стенде при реализации.
- **Мелкие файлы.** Spark может создать много мелких data files (по числу partitions). Нормально для Module 4 — тема compaction отложена до Модуля 8. Упомянуть, не решать.
- **Идемпотентность.** Используем `CREATE OR REPLACE TABLE ... AS SELECT` (CTAS) для повторных запусков — пересоздаёт таблицу целиком, snapshot history остаётся чистой. Это проще и надёжнее, чем `DELETE FROM` + `INSERT INTO` (который усложняет snapshot history и требует проверки поддержки `DELETE` на текущем стеке). Компромисс: `CREATE OR REPLACE` уничтожает предыдущие snapshots, но для учебного bronze это допустимо. В markdown явно объяснить этот trade-off.
## Out of Scope
- Трансформации и очистка данных — отложено до Модуля 5.
- Чтение bronze через Trino — отложено до Модуля 6.
- Time travel и schema evolution — отложено до Модуля 7.
- Compaction и vacuum — отложено до Модуля 8.
- Партиционирование bronze-таблицы (PRD: вне v1).
- Gold-слой и бизнес-витрины.