From a085b2fc5503ccaf1feffddf9ed677e1fef22324 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Tue, 15 Sep 2026 07:35:39 -0400 Subject: [PATCH] =?UTF-8?q?fix(course):=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=81=D0=B5=D0=BC=D0=B0=D0=BD?= =?UTF-8?q?=D1=82=D0=B8=D0=BA=D0=B0=20Iceberg=20snapshots?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: объяснения CREATE OR REPLACE и rollback расходились с поведением Iceberg 1.10.0 и ломали повторный запуск упражнений. - Что: исправлены материалы и планы модулей 2, 4, 5, 7 и 8; одноразовые таблицы сбрасываются через DROP TABLE PURGE. - Проверка: JSON и Python AST валидны; на Spark 3.5.1, Iceberg 1.10.0, JDBC/MinIO и Trino 478 подтверждены rollback, replace, time travel и повторный сброс до одного snapshot-а. --- docs/mentor_notes.md | 2 +- notebooks/02_lakehouse_mental_model.ipynb | 8 +-- notebooks/04_bronze_with_iceberg.ipynb | 4 +- notebooks/05_silver_layer.ipynb | 4 +- notebooks/07_safe_table_changes.ipynb | 62 +++++++++++++------- notebooks/08_maintenance_and_final_lab.ipynb | 16 +++-- plans/module-02-lakehouse-mental-model.md | 4 +- plans/module-04-bronze-with-iceberg.md | 13 ++-- plans/module-05-silver-layer.md | 6 +- plans/module-07-safe-table-changes.md | 50 +++++++++------- plans/module-08-maintenance-and-final-lab.md | 13 ++-- 11 files changed, 106 insertions(+), 76 deletions(-) diff --git a/docs/mentor_notes.md b/docs/mentor_notes.md index 72e227a..45eb719 100644 --- a/docs/mentor_notes.md +++ b/docs/mentor_notes.md @@ -72,7 +72,7 @@ - **Студент путает schema evolution и data snapshot.** `ALTER TABLE ADD COLUMNS` не создаёт новый data snapshot — это metadata-only операция. Snapshot создаётся только при изменении данных (INSERT, DELETE и т.д.). - **Time travel: забывает сохранить snapshot_id.** Без сохранённого ID в переменную приходится заново запрашивать `table.snapshots`. Привычка: перед экспериментом запиши ID текущего состояния. -- **`rollback_to_snapshot` vs `CREATE OR REPLACE`.** Ключевое отличие: rollback сохраняет историю, `CREATE OR REPLACE` уничтожает её. Это описано в Секции 10 ноутбука. +- **`rollback_to_snapshot` vs `CREATE OR REPLACE`.** Rollback переключает текущее состояние на существующий snapshot-предок и не создаёт новый snapshot. `CREATE OR REPLACE` создаёт новый snapshot без родителя и заменяет текущие данные, схему и партиционирование; прежние snapshot-ы сохраняются для time travel, но перестают быть предками текущего состояния. Это описано в Секциях 8 и 10 ноутбука. ### Модуль 8. Обслуживание и финальная практика diff --git a/notebooks/02_lakehouse_mental_model.ipynb b/notebooks/02_lakehouse_mental_model.ipynb index cd71428..7e9d998 100644 --- a/notebooks/02_lakehouse_mental_model.ipynb +++ b/notebooks/02_lakehouse_mental_model.ipynb @@ -264,9 +264,9 @@ "metadata": {}, "outputs": [], "source": [ - "# Удаляем таблицы (это удалит их и из каталога, и из хранилища)\n", - "spark.sql(\"DROP TABLE IF EXISTS lakehouse.module_02.demo_table\")\n", - "spark.sql(\"DROP TABLE IF EXISTS lakehouse.module_02.my_first_table\")\n", + "# PURGE удаляет одноразовые таблицы и из каталога, и из хранилища\n", + "spark.sql(\"DROP TABLE IF EXISTS lakehouse.module_02.demo_table PURGE\")\n", + "spark.sql(\"DROP TABLE IF EXISTS lakehouse.module_02.my_first_table PURGE\")\n", "\n", "# Удаляем namespace\n", "spark.sql(\"DROP NAMESPACE IF EXISTS lakehouse.module_02\")\n", @@ -297,4 +297,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/notebooks/04_bronze_with_iceberg.ipynb b/notebooks/04_bronze_with_iceberg.ipynb index 8264559..8b81037 100644 --- a/notebooks/04_bronze_with_iceberg.ipynb +++ b/notebooks/04_bronze_with_iceberg.ipynb @@ -216,7 +216,7 @@ "cell_type": "markdown", "id": "350d2087", "metadata": {}, - "source": "## 3. Создание `bronze` namespace и `Iceberg`-таблицы\n\nСначала создаём namespace `lakehouse.bronze`, а затем выполняем `CTAS` (`CREATE OR REPLACE TABLE ... USING iceberg AS SELECT ...`).\n\nЧто такое namespace в `Iceberg`:\n\n- это логический контейнер для таблиц;\n- если ты работал с `PostgreSQL` — это ближайший аналог `schema` внутри базы;\n- запись `lakehouse.bronze.nyc_taxi_yellow` читается как `catalog.namespace.table`.\n\nПочему здесь используем именно `bronze`, а не `module_04`:\n\n- имя сразу отражает роль слоя в пайплайне `raw -> bronze -> silver`;\n- Модуль 5 будет читать именно `lakehouse.bronze.*`, и семантическое имя делает маршрут прозрачным;\n- в отличие от демонстрационного `module_02`, этот слой не временный и будет использоваться в следующих модулях.\n\nЭто удобный учебный компромисс:\n\n- один шаг создаёт таблицу и сразу наполняет её;\n- повторный запуск секции остаётся идемпотентным;\n- минус: `CREATE OR REPLACE` пересоздаёт таблицу и сбрасывает старую историю snapshot-ов.\n\nДля первого `bronze`-слоя это допустимо. В Модуле 7 мы отдельно разберём, почему в рабочих сценариях нужно аккуратнее относиться к истории таблицы." + "source": "## 3. Создание `bronze` namespace и `Iceberg`-таблицы\n\nСначала создаём namespace `lakehouse.bronze`, а затем выполняем `CTAS` (`CREATE OR REPLACE TABLE ... USING iceberg AS SELECT ...`).\n\nЧто такое namespace в `Iceberg`:\n\n- это логический контейнер для таблиц;\n- если ты работал с `PostgreSQL` — это ближайший аналог `schema` внутри базы;\n- запись `lakehouse.bronze.nyc_taxi_yellow` читается как `catalog.namespace.table`.\n\nПочему здесь используем именно `bronze`, а не `module_04`:\n\n- имя сразу отражает роль слоя в пайплайне `raw -> bronze -> silver`;\n- Модуль 5 будет читать именно `lakehouse.bronze.*`, и семантическое имя делает маршрут прозрачным;\n- в отличие от демонстрационного `module_02`, этот слой не временный и будет использоваться в следующих модулях.\n\nЭто удобный учебный компромисс:\n\n- один шаг создаёт таблицу и сразу наполняет её;\n- повторный запуск приводит текущее содержимое к тому же результату и не дублирует строки;\n- при каждом запуске `CREATE OR REPLACE` создаёт новый snapshot, а старые snapshot-ы сохраняются для time travel.\n\nВажно: замена может изменить текущие данные, схему и партиционирование таблицы. Новый snapshot не продолжает прежнюю цепочку: у него нет `parent_id`, а старые snapshot-ы больше не являются предками текущего состояния (`is_current_ancestor = false`). Это не отдельная именованная ветка — текущей по-прежнему остаётся `main`. Подробнее эту разницу разберём в Модуле 7." }, { "cell_type": "code", @@ -650,4 +650,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/notebooks/05_silver_layer.ipynb b/notebooks/05_silver_layer.ipynb index 6f26752..40ceebe 100644 --- a/notebooks/05_silver_layer.ipynb +++ b/notebooks/05_silver_layer.ipynb @@ -280,7 +280,7 @@ "\n", "Используем `CREATE OR REPLACE TABLE ... AS SELECT` (CTAS) в namespace `lakehouse.silver`.\n", "\n", - "**Важное предупреждение:** `CREATE OR REPLACE` — это полная перезапись таблицы. Все предыдущие данные и история snapshot-ов будут потеряны. В учебном silver это осознанный компромисс ради идемпотентности: повторный запуск ноутбука пересоздаёт таблицу с чистого листа. В Модуле 7 мы увидим, как делать изменения безопаснее.\n", + "**Важное предупреждение:** `CREATE OR REPLACE` атомарно заменяет текущее содержимое таблицы и может заменить её схему и партиционирование. При этом Iceberg сохраняет предыдущие snapshot-ы для time travel, но новый snapshot не продолжает их цепочку: старые состояния перестают быть предками текущего. В учебном silver это осознанный компромисс ради воспроизводимого результата без дублирования строк. В Модуле 7 мы разберём эту семантику подробнее.\n", "\n", "На 3 месяцах данных (~7 млн строк) эта операция может занять 2-4 минуты." ] @@ -610,4 +610,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/notebooks/07_safe_table_changes.ipynb b/notebooks/07_safe_table_changes.ipynb index 196af74..b36afa7 100644 --- a/notebooks/07_safe_table_changes.ipynb +++ b/notebooks/07_safe_table_changes.ipynb @@ -153,7 +153,7 @@ "- `operation` — тип операции (append, overwrite).\n", "- `summary` — краткая статистика (added-data-files, total-records и т.д.).\n", "\n", - "Если вы не перезапускали Модуль 5 через CREATE OR REPLACE, у silver будет 1 snapshot (от CTAS).\n", + "После первого запуска Модуля 5 у silver будет 1 snapshot от CTAS. Каждый повторный `CREATE OR REPLACE` добавит ещё один snapshot: прежние состояния сохранятся, хотя перестанут быть предками текущего.\n", "В Greenplum такой истории нет: после `INSERT` предыдущее состояние таблицы недоступно без бэкапа.\n" ] }, @@ -211,7 +211,7 @@ "Колонка добавлена. Данные не изменились — Iceberg не перезаписывал data files. \n", "В рабочем сценарии `loaded_at` заполнялся бы при следующих INSERT-ах: `INSERT INTO ... SELECT ..., current_timestamp() AS loaded_at FROM ...`. Существующие строки остаются с NULL — это нормальная практика эволюции схемы. Заполнение существующих строк (UPDATE) — отдельная тема, требующая перезаписи файлов.\n", "\n", - "Примечание: если вы повторно запустите Модуль 5 после Модуля 7, `CREATE OR REPLACE TABLE` пересоздаст silver без колонки `loaded_at`. Это ещё одна иллюстрация того, почему `CREATE OR REPLACE` опасен в рабочих сценариях — он стирает все изменения, включая schema evolution.\n" + "Примечание: если вы повторно запустите Модуль 5 после Модуля 7, `CREATE OR REPLACE TABLE` заменит текущую схему silver, поэтому колонка `loaded_at` исчезнет из текущего состояния. Предыдущие snapshot-ы при этом сохранятся для time travel, но перестанут быть предками текущего snapshot-а.\n" ] }, { @@ -288,9 +288,13 @@ "metadata": {}, "outputs": [], "source": [ + "# Демо-таблица одноразовая: удаляем её вместе с историей,\n", + "# чтобы при повторном запуске снова получить ровно один snapshot.\n", + "spark.sql(f\"DROP TABLE IF EXISTS {DEMO_TABLE} PURGE\")\n", + "\n", "# Создание демо-таблицы\n", "spark.sql(f\"\"\"\n", - " CREATE OR REPLACE TABLE {DEMO_TABLE}\n", + " CREATE TABLE {DEMO_TABLE}\n", " USING iceberg AS\n", " SELECT VendorID, tpep_pickup_datetime, tpep_dropoff_datetime,\n", " passenger_count, trip_distance, fare_amount, total_amount,\n", @@ -504,7 +508,7 @@ "outputs": [], "source": [ "# Посмотрим на 3-й snapshot (нашу ошибку)\n", - "spark.sql(f\"SELECT snapshot_id, operation FROM {DEMO_TABLE}.snapshots\").show(truncate=False)\n" + "spark.sql(f\"SELECT snapshot_id, parent_id, operation FROM {DEMO_TABLE}.snapshots\").show(truncate=False)\n" ] }, { @@ -563,23 +567,32 @@ "metadata": {}, "outputs": [], "source": [ - "spark.sql(f\"SELECT snapshot_id, operation FROM {DEMO_TABLE}.snapshots\").show(truncate=False)\n" + "print(\"Сохранённые snapshot-ы:\")\n", + "spark.sql(f\"SELECT snapshot_id, parent_id, operation FROM {DEMO_TABLE}.snapshots\").show(truncate=False)\n", + "\n", + "print(\"История переключений текущего состояния:\")\n", + "spark.sql(\n", + " f\"SELECT made_current_at, snapshot_id, parent_id, is_current_ancestor \"\n", + " f\"FROM {DEMO_TABLE}.history ORDER BY made_current_at\"\n", + ").show(truncate=False)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Теперь в истории 4 записи. Четвёртый snapshot — это результат rollback, но его данные (набор файлов) в точности соответствуют snapshot 2.\n", + "В `.snapshots` по-прежнему три записи: rollback **не создал четвёртый snapshot**. Он переключил текущее состояние таблицы на уже существующий `snapshot_2`.\n", "\n", - "Rollback **не удаляет историю** и **не удаляет файлы**. Он создаёт новый snapshot, указывающий на данные предыдущего состояния. Аналогия:\n", + "В `.history` появилась ещё одна запись о переключении текущего состояния. Она снова указывает на `snapshot_2`; snapshot с «плохими» строками сохранился, но больше не является предком текущего состояния (`is_current_ancestor = false`).\n", "\n", - "| | git | Iceberg |\n", + "| Свойство | `rollback_to_snapshot` | `CREATE OR REPLACE TABLE ... AS SELECT` |\n", "|---|---|---|\n", - "| Откат с сохранением истории | `git revert` (новый коммит) | `rollback_to_snapshot` (новый snapshot) |\n", - "| Откат с удалением истории | `git reset --hard` | `CREATE OR REPLACE` (уничтожает snapshot-ы) |\n", + "| Создаёт новый snapshot | Нет | Да |\n", + "| Что становится текущим | Существующий snapshot-предок | Новый snapshot с результатом `SELECT` |\n", + "| Что происходит со старыми snapshot-ами | Сохраняются | Сохраняются для time travel |\n", + "| Цепочка предков | Возвращаемся к предку | Новый snapshot не имеет `parent_id`; прежние snapshot-ы перестают быть предками текущего |\n", "\n", - "Старые data files (включая файлы с «плохими» строками) остаются в хранилище (MinIO) до процедуры `expire_snapshots` — это тема Модуля 8. Rollback не чистит storage, он только меняет указатель текущего snapshot-а.\n" + "При `CREATE OR REPLACE` отдельная именованная ветка автоматически не создаётся: текущей остаётся ветка `main`. Старые data files сохраняются до `expire_snapshots` — эту процедуру разберём в Модуле 8.\n" ] }, { @@ -649,7 +662,7 @@ "### Секция 10: Типичные ошибки новичка\n", "\n", "**Ошибка 1: Слепой `CREATE OR REPLACE`.**\n", - "В Модулях 4 и 5 мы использовали `CREATE OR REPLACE TABLE ... AS SELECT` (CTAS) для создания bronze и silver. Это был осознанный компромисс: первичная загрузка, нет предыдущей истории, которую нужно сохранять. Но в рабочих сценариях `CREATE OR REPLACE` — **деструктивная операция**: она уничтожает ВСЮ snapshot-историю таблицы. Если у таблицы было 100 snapshot-ов, после `CREATE OR REPLACE` останется один. Time travel к предыдущим состояниям станет невозможен.\n", + "В Модулях 4 и 5 мы использовали `CREATE OR REPLACE TABLE ... AS SELECT` для воспроизводимой полной загрузки bronze и silver. В `SparkCatalog` эта операция атомарно создаёт новый snapshot и сохраняет прежние snapshot-ы для time travel. Но она заменяет текущее содержимое, схему и партиционирование таблицы. Новый snapshot не имеет `parent_id`, поэтому прежние snapshot-ы перестают быть предками текущего состояния. Слепая замена может неожиданно убрать колонку, изменить раскладку данных или усложнить обычный rollback к прежней цепочке. Если нужно перезаписать данные, сохранив текущую схему и партиционирование, используй `INSERT OVERWRITE`.\n", "\n", "**Ошибка 2: Изменение схемы без проверки downstream.**\n", "Мы только что видели, как `RENAME COLUMN` ломает запросы со старым именем. `ALTER COLUMN TYPE` (расширение INT -> BIGINT) безопасен для данных, но downstream-системы могут интерпретировать новый тип по-другому и сломаться на своей стороне. Перед любым изменением схемы нужно понимать, кто читает эту таблицу.\n", @@ -664,7 +677,7 @@ "source": [ "### Секция 11: Самостоятельное задание\n", "\n", - "Пересоздадим демо-таблицу с чистого листа. После демонстраций выше её схема изменена, а история содержит учебные эксперименты. Для самостоятельной работы нужна чистая таблица.\n" + "Подготовим демо-таблицу с чистого листа. После демонстраций выше её схема изменена, а история содержит учебные эксперименты. Поскольку `CREATE OR REPLACE` сохраняет старые snapshot-ы, для одноразовой учебной таблицы явно используем `DROP TABLE ... PURGE`, а затем новый `CREATE TABLE`. Так самостоятельная работа снова начнётся ровно с одного snapshot-а.\n" ] }, { @@ -673,9 +686,12 @@ "metadata": {}, "outputs": [], "source": [ - "# Пересоздаем демо-таблицу (это удалит всю ее snapshot-историю - Ошибка 1 в действии!)\n", + "# Эта таблица одноразовая: PURGE удаляет её данные и snapshot-историю.\n", + "spark.sql(f\"DROP TABLE IF EXISTS {DEMO_TABLE} PURGE\")\n", + "\n", + "# Создаём новую таблицу с одним начальным snapshot-ом.\n", "spark.sql(f\"\"\"\n", - " CREATE OR REPLACE TABLE {DEMO_TABLE}\n", + " CREATE TABLE {DEMO_TABLE}\n", " USING iceberg AS\n", " SELECT VendorID, tpep_pickup_datetime, tpep_dropoff_datetime,\n", " passenger_count, trip_distance, fare_amount, total_amount,\n", @@ -683,7 +699,9 @@ " FROM {SILVER_TABLE}\n", " LIMIT 1000\n", "\"\"\")\n", - "print(f\"Демо-таблица пересоздана начисто: {spark.table(DEMO_TABLE).count()} строк, 1 snapshot.\")\n" + "practice_snapshot_count = spark.sql(f\"SELECT * FROM {DEMO_TABLE}.snapshots\").count()\n", + "assert practice_snapshot_count == 1, f\"Ожидался 1 snapshot, получено {practice_snapshot_count}\"\n", + "print(f\"Демо-таблица создана начисто: {spark.table(DEMO_TABLE).count()} строк, 1 snapshot.\")\n" ] }, { @@ -804,7 +822,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "**Задача 5 (ответ).** Ответь текстом в ячейке ниже: чем `rollback_to_snapshot` отличается от `CREATE OR REPLACE TABLE ... AS SELECT * FROM table VERSION AS OF `? Что происходит с историей snapshot-ов в каждом случае?\n" + "**Задача 5 (ответ).** Сравни `rollback_to_snapshot` и `CREATE OR REPLACE TABLE ... AS SELECT * FROM table VERSION AS OF `. Какая операция создаёт новый snapshot? Что происходит с текущим состоянием, старыми snapshot-ами и цепочкой `parent_id`?\n" ] }, { @@ -843,7 +861,7 @@ "3. Чем `ADD COLUMN` отличается от `RENAME COLUMN` по влиянию на downstream?\n", "4. Как прочитать предыдущее состояние таблицы? (назови два способа)\n", "5. Что делает `rollback_to_snapshot`? Теряется ли при этом snapshot-история?\n", - "6. Почему `CREATE OR REPLACE` опаснее, чем `INSERT INTO` с последующим rollback?\n", + "6. Какие риски остаются у `CREATE OR REPLACE`, хотя старые snapshot-ы сохраняются?\n", "7. Какой аналог time travel в классических СУБД (PostgreSQL/Greenplum) и почему он сложнее в использовании?\n" ] }, @@ -862,8 +880,8 @@ "metadata": {}, "outputs": [], "source": [ - "spark.sql(f\"DROP TABLE IF EXISTS {DEMO_TABLE}\")\n", - "print(\"Демо-таблица удалена.\")\n" + "spark.sql(f\"DROP TABLE IF EXISTS {DEMO_TABLE} PURGE\")\n", + "print(\"Демо-таблица и её учебные данные удалены.\")\n" ] }, { @@ -874,7 +892,7 @@ "- Schema evolution (`ADD COLUMN`) — безопасная metadata-only операция, которая мгновенно видна из всех движков через общий каталог.\n", "- Snapshot-ы — автоматическая история изменений данных, встроенная в Iceberg.\n", "- Time travel — чтение предыдущих состояний без бэкапов.\n", - "- Rollback — восстановление после ошибки с сохранением истории.\n", + "- Rollback — переключение текущего состояния на существующий snapshot-предок без создания нового snapshot-а.\n", "\n", "В финальном Модуле 8 мы изучим базовое обслуживание таблиц: compaction (решение проблемы множества мелких файлов) и `expire_snapshots` (очистка старых файлов данных, оставшихся после time travel и rollback, которые мы научились применять в этом модуле).\n" ] @@ -901,4 +919,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} diff --git a/notebooks/08_maintenance_and_final_lab.ipynb b/notebooks/08_maintenance_and_final_lab.ipynb index 9c45274..b96f0c1 100644 --- a/notebooks/08_maintenance_and_final_lab.ipynb +++ b/notebooks/08_maintenance_and_final_lab.ipynb @@ -135,7 +135,7 @@ "\n", "Каждый `INSERT INTO` в Iceberg создаёт новые data files. Если вставлять данные мелкими порциями (частые микробатчи, ручные INSERT-ы, инкрементальные загрузки), таблица накапливает множество мелких файлов. Это замедляет чтение: движку приходится открывать и читать каждый файл отдельно. \n", "\n", - "Создадим демо-таблицу и сымитируем эту проблему: один изначальный `CTAS` (500 строк) и 8 маленьких `INSERT INTO` (по 100 строк).\n", + "Создадим демо-таблицу и сымитируем эту проблему: один изначальный `CTAS` (500 строк) и 8 маленьких `INSERT INTO` (по 100 строк). Перед созданием удалим одноразовую таблицу через `DROP TABLE ... PURGE`, чтобы при повторном запуске история снова начиналась с одного snapshot-а.\n", "*(Внимание: 8 INSERT-ов могут занять 1-2 минуты)*\n" ] }, @@ -145,9 +145,13 @@ "metadata": {}, "outputs": [], "source": [ - "# Создание демо-таблицы (создаст 1 файл)\n", + "# Демо-таблица одноразовая: удаляем данные и старую snapshot-историю,\n", + "# чтобы повторный запуск дал те же 9 snapshot-ов.\n", + "spark.sql(f\"DROP TABLE IF EXISTS {DEMO_TABLE} PURGE\")\n", + "\n", + "# Создание демо-таблицы (создаст 1 файл и 1 snapshot)\n", "spark.sql(f\"\"\"\n", - " CREATE OR REPLACE TABLE {DEMO_TABLE}\n", + " CREATE TABLE {DEMO_TABLE}\n", " USING iceberg AS\n", " SELECT VendorID, tpep_pickup_datetime, tpep_dropoff_datetime,\n", " passenger_count, trip_distance, fare_amount, total_amount,\n", @@ -529,8 +533,8 @@ "metadata": {}, "outputs": [], "source": [ - "spark.sql(f\"DROP TABLE IF EXISTS {DEMO_TABLE}\")\n", - "print(\"Демо-таблица удалена.\")\n", + "spark.sql(f\"DROP TABLE IF EXISTS {DEMO_TABLE} PURGE\")\n", + "print(\"Демо-таблица и её учебные данные удалены.\")\n", "\n" ] }, @@ -784,4 +788,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} diff --git a/plans/module-02-lakehouse-mental-model.md b/plans/module-02-lakehouse-mental-model.md index 6aa9ceb..f125fb0 100644 --- a/plans/module-02-lakehouse-mental-model.md +++ b/plans/module-02-lakehouse-mental-model.md @@ -1,7 +1,7 @@ # Модуль 2. Ментальная модель Lakehouse: storage, catalog, compute **Статус:** `Ready for validation` -**Последнее обновление:** `2026-03-07` +**Последнее обновление:** `2026-09-15` ## Цель @@ -33,7 +33,7 @@ - запрос к PostgreSQL (каталогу метаданных), чтобы увидеть регистрацию таблицы; - запрос к MinIO (например, s3a list или boto3), чтобы увидеть появление файлов данных (parquet) и метаданных (json/avro). 5. Сформулировать самостоятельное задание: студент создает свою таблицу, пишет в нее данные и находит её следы в хранилище и каталоге. -6. Включить в конец ноутбука обязательный шаг очистки (cleanup) — удаление таблиц и namespace `module_02`, чтобы не оставлять мусор для следующих модулей. +6. Включить в конец ноутбука обязательный шаг очистки (cleanup) — удаление одноразовых таблиц через `DROP TABLE ... PURGE` и namespace `module_02`, чтобы не оставлять данные и метаданные для следующих модулей. 7. Описать вопросы для Checkpoint. ## Checkpoint diff --git a/plans/module-04-bronze-with-iceberg.md b/plans/module-04-bronze-with-iceberg.md index d8b92cf..50aad41 100644 --- a/plans/module-04-bronze-with-iceberg.md +++ b/plans/module-04-bronze-with-iceberg.md @@ -1,7 +1,7 @@ # Модуль 4. Первая рабочая Iceberg-таблица и слой bronze **Статус:** `Draft` -**Последнее обновление:** `2026-03-07` +**Последнее обновление:** `2026-09-15` ## Цель @@ -50,11 +50,12 @@ ### Создание и наполнение таблицы: CTAS с явным списком колонок -Используем `CREATE OR REPLACE TABLE ... USING iceberg AS SELECT col1, col2, ... FROM raw_view` (CTAS). Это: +Используем `CREATE OR REPLACE TABLE ... USING iceberg AS SELECT col1, col2, ... FROM raw_view` (CTAS). В настроенном `SparkCatalog` это атомарная замена. Она: - объединяет создание и наполнение в один шаг — понятнее и ближе к `CREATE TABLE AS SELECT` в PostgreSQL; - явный список колонок в SELECT исключает ошибки из-за несовпадения порядка или состава колонок (PRD Risk #3, #5); -- `CREATE OR REPLACE` обеспечивает идемпотентность: повторный запуск пересоздаёт таблицу, snapshot history остаётся чистой; -- trade-off: `CREATE OR REPLACE` уничтожает историю — в production это опасно, но для первичной загрузки учебного bronze допустимо. В Модуле 7 студент увидит, почему в рабочих сценариях нужен другой подход. +- повторный запуск приводит текущее содержимое к тому же результату и не дублирует строки; +- создаёт новый snapshot и сохраняет прежние snapshot-ы для time travel; +- заменяет текущие данные, схему и partition spec; новый snapshot не имеет `parent_id`, поэтому прежние snapshot-ы перестают быть предками текущего состояния. Отдельная именованная ветка при этом не создаётся. В Секции 2 DDL с явной схемой показывается как справочный пример (не выполняется) — чтобы студент увидел знакомую форму `CREATE TABLE (col type, ...)`. @@ -105,7 +106,7 @@ Bronze нужен Модулю 5. Spark-сессия останавливает ### Секция 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 увидим, почему в рабочих сценариях нужен другой подход. +- **[md]** Повторяемость: `CREATE OR REPLACE` при повторном запуске атомарно заменяет текущее содержимое и не дублирует строки, но добавляет новый snapshot. Прежние snapshot-ы сохраняются для time travel и перестают быть предками текущего состояния. Trade-off: операция может заменить схему и partition spec, поэтому для учебной полной загрузки она удобна, а в рабочих инкрементальных сценариях требует осторожности. В Модуле 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 — должны совпасть. @@ -216,7 +217,7 @@ Bronze нужен Модулю 5. Spark-сессия останавливает - **Схема 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. +- **Повторный запуск.** Используем `CREATE OR REPLACE TABLE ... AS SELECT` (CTAS): текущее содержимое заменяется атомарно и строки не дублируются. При каждом запуске появляется новый snapshot; предыдущие snapshot-ы сохраняются для time travel, но перестают быть предками текущего. В markdown явно объяснить этот trade-off и риск замены схемы и partition spec. ## Out of Scope diff --git a/plans/module-05-silver-layer.md b/plans/module-05-silver-layer.md index 225307f..54cb4f4 100644 --- a/plans/module-05-silver-layer.md +++ b/plans/module-05-silver-layer.md @@ -1,7 +1,7 @@ # Модуль 5. Слой silver и воспроизводимые трансформации **Статус:** `Ready for validation` -**Последнее обновление:** `2026-03-07` +**Последнее обновление:** `2026-09-15` ## Цель @@ -148,7 +148,7 @@ Silver нужен Модулям 6 и 8. Spark-сессия останавлив ### Секция 7: Создание silver-таблицы - **[md]** Используем CTAS с `CREATE OR REPLACE`. Namespace `lakehouse.silver`. -- **[md]** Явное предупреждение о `CREATE OR REPLACE`: это полная перезапись таблицы с потерей предыдущего состояния и snapshot-истории. Для учебного silver это осознанный компромисс ради идемпотентности (повторный запуск ноутбука пересоздаёт таблицу с чистого листа). В рабочих сценариях это разрушительная операция — в Модуле 7 студент увидит, почему нужен другой подход. Параллель: `DROP TABLE + CREATE TABLE AS SELECT` в PostgreSQL. +- **[md]** Явное предупреждение о `CREATE OR REPLACE`: операция атомарно заменяет текущее содержимое, схему и partition spec, но сохраняет прежние snapshot-ы для time travel. Новый snapshot не имеет `parent_id`, поэтому старые snapshot-ы перестают быть предками текущего состояния. Для учебного silver это осознанный компромисс ради воспроизводимого результата без дублирования строк. В Модуле 7 студент подробно сравнит замену и rollback. - **[code]** `CREATE NAMESPACE IF NOT EXISTS lakehouse.silver`. Регистрация temp view. CTAS с явным списком демо-схемы: 24 колонки (19 оригинальных + 5 новых). Это каноническая схема, на которую опираются Модули 6 и 8. - **[md]** Предупреждение о времени выполнения (2-4 мин на 3 мес данных). - **[code]** Верификация: count, printSchema. @@ -276,7 +276,7 @@ speed_mph DOUBLE (E4: новая, trip_distance / (tr - **Время CTAS.** JOIN + трансформации — на 30-50% медленнее bronze CTAS. На 3 мес (~7 млн строк): 2-4 мин. Extended: до 5-10 мин. Предупреждение в markdown. - **Column case sensitivity.** Новые колонки (lowercase) рядом с оригинальными (mixed case: VendorID, PULocationID). Нормально для Iceberg, но добавить пояснение. - **trip_duration_minutes отрицательные.** Если dropoff < pickup — длительность отрицательна. Упомянуть как наблюдение, не фильтровать в демо (потенциально — для самостоятельного задания или будущего правила). -- **Идемпотентность.** `CREATE OR REPLACE TABLE ... AS SELECT` (CTAS) для повторных запусков — тот же паттерн, что в Модуле 4. Пересоздаёт таблицу целиком. +- **Повторный запуск.** `CREATE OR REPLACE TABLE ... AS SELECT` (CTAS) — тот же паттерн, что в Модуле 4. Атомарно заменяет текущее содержимое, не дублирует строки и добавляет новый snapshot, сохраняя прежние для time travel. ## Out of Scope diff --git a/plans/module-07-safe-table-changes.md b/plans/module-07-safe-table-changes.md index 77f2895..f317a76 100644 --- a/plans/module-07-safe-table-changes.md +++ b/plans/module-07-safe-table-changes.md @@ -1,7 +1,7 @@ # Модуль 7. Безопасная работа с таблицами: schema evolution и time travel **Статус:** `Ready for validation` -**Последнее обновление:** `2026-03-08` +**Последнее обновление:** `2026-09-15` ## Цель @@ -69,9 +69,10 @@ Iceberg предоставляет stored procedure `CALL system.rollback_to_snapshot(table, snapshot_id)`. Это: -- создаёт НОВЫЙ snapshot, указывающий на данные старого — история не теряется; +- переключает текущее состояние на существующий snapshot-предок и не создаёт новый snapshot; +- добавляет запись в `.history` о смене текущего состояния, а прежние snapshot-ы остаются доступными; - старые data files не удаляются — они остаются до процедуры `expire_snapshots` / vacuum (Модуль 8); -- семантически аналогично `git revert` (новый коммит, отменяющий изменение), а не `git reset --hard` (удаление истории). +- отдалённо напоминает перемещение указателя ветки в git, но snapshot-ы Iceberg остаются доступными до явного `expire_snapshots`. Точный синтаксис stored procedure зависит от версии Iceberg и Spark — нужна проверка на стенде при реализации. @@ -171,11 +172,12 @@ except Exception as e: ### Секция 6: Подготовка демо-таблицы с несколькими snapshot-ами - **[md]** Для экспериментов с time travel и восстановлением создаём отдельную таблицу. Silver не трогаем — он нужен для Модуля 8. Создаём таблицу с небольшим подмножеством silver и наращиваем snapshot-историю через `INSERT INTO`. -- **[code]** Создание `lakehouse.default.taxi_changes_demo` через CTAS (1000 строк из silver): +- **[code]** Удаление одноразовой демо-таблицы через `DROP TABLE IF EXISTS ... PURGE`, затем создание `lakehouse.default.taxi_changes_demo` через CTAS (1000 строк из silver). Явное удаление гарантирует один начальный snapshot при повторном запуске: ```python +spark.sql("DROP TABLE IF EXISTS lakehouse.default.taxi_changes_demo PURGE") spark.sql(""" - CREATE OR REPLACE TABLE lakehouse.default.taxi_changes_demo + CREATE TABLE lakehouse.default.taxi_changes_demo USING iceberg AS SELECT VendorID, tpep_pickup_datetime, tpep_dropoff_datetime, passenger_count, trip_distance, fare_amount, total_amount, @@ -185,7 +187,7 @@ spark.sql(""" """) ``` -- **[code]** Проверка: count = 1000. Snapshot-ы: 1 snapshot (overwrite — от CTAS). +- **[code]** Проверка: count = 1000. Snapshot-ы: 1 snapshot (`append` от CTAS). - **[code]** INSERT ещё 500 строк -> snapshot 2: ```python @@ -293,15 +295,17 @@ spark.sql(f""" #### 8c: Что произошло со snapshot-историей -- **[code]** Snapshot-история: теперь 4 записи. Четвёртый snapshot — rollback, но его данные соответствуют snapshot 2. -- **[md]** Rollback не удаляет историю и не удаляет файлы. Он создаёт новый snapshot, указывающий на данные предыдущего состояния. Аналогия: +- **[code]** `.snapshots`: по-прежнему 3 snapshot-а. `.history`: появилась четвёртая запись, снова указывающая на snapshot 2; ошибочный snapshot помечен `is_current_ancestor = false`. +- **[md]** Rollback не удаляет историю и файлы, но и не создаёт новый snapshot. Он переключает текущее состояние на существующий snapshot-предок. Сравнение: -| | git | Iceberg | +| Свойство | `rollback_to_snapshot` | `CREATE OR REPLACE TABLE ... AS SELECT` | |---|---|---| -| Откат с сохранением истории | `git revert` (новый коммит) | `rollback_to_snapshot` (новый snapshot) | -| Откат с удалением истории | `git reset --hard` | `CREATE OR REPLACE` (уничтожает snapshot-ы) | +| Создаёт новый snapshot | Нет | Да | +| Что становится текущим | Существующий snapshot-предок | Новый snapshot с результатом `SELECT` | +| Что происходит со старыми snapshot-ами | Сохраняются | Сохраняются для time travel | +| Цепочка предков | Возвращаемся к предку | Новый snapshot не имеет `parent_id`; прежние snapshot-ы перестают быть предками текущего | -- **[md]** Старые data files (включая «плохие» строки) остаются в MinIO до процедуры `expire_snapshots` — это тема Модуля 8. Rollback не чистит storage, он только меняет указатель текущего snapshot-а. +- **[md]** `CREATE OR REPLACE` не создаёт отдельную именованную ветку: текущей остаётся `main`. Старые data files сохраняются до процедуры `expire_snapshots` — это тема Модуля 8. ### Секция 9: RENAME COLUMN и влияние на downstream @@ -325,7 +329,7 @@ except Exception as e: - **[md]** Три ошибки, на которые обращает внимание PRD курса: **Ошибка 1: Слепой `CREATE OR REPLACE`.** -В Модулях 4 и 5 мы использовали `CREATE OR REPLACE TABLE ... AS SELECT` для создания bronze и silver. Это был осознанный компромисс: первичная загрузка, нет предыдущей истории, которую нужно сохранять. Но в рабочих сценариях `CREATE OR REPLACE` — деструктивная операция: она уничтожает ВСЮ snapshot-историю таблицы. Если у таблицы было 100 snapshot-ов, после `CREATE OR REPLACE` останется один. Time travel к предыдущим состояниям станет невозможен. +В Модулях 4 и 5 мы использовали `CREATE OR REPLACE TABLE ... AS SELECT` для воспроизводимой полной загрузки bronze и silver. В `SparkCatalog` операция атомарно создаёт новый snapshot и сохраняет прежние snapshot-ы для time travel. Но она заменяет текущее содержимое, схему и partition spec. Новый snapshot не имеет `parent_id`, поэтому прежние snapshot-ы перестают быть предками текущего состояния. Если нужно перезаписать данные, сохранив текущую схему и партиционирование, следует использовать `INSERT OVERWRITE`. **Ошибка 2: Изменение схемы без проверки downstream.** `RENAME COLUMN` мы только что видели: данные целы, но запросы со старым именем ломаются. `ALTER COLUMN TYPE` (расширение INT -> BIGINT) безопасен для данных, но downstream-системы могут интерпретировать новый тип по-другому. Перед любым изменением схемы нужно понимать, кто читает эту таблицу. @@ -335,12 +339,13 @@ except Exception as e: ### Секция 11: Самостоятельное задание -- **[md]** Пересоздаём демо-таблицу с чистого листа. После демонстраций в Секциях 6-9 её схема изменена (RENAME), а snapshot-история содержит наши учебные эксперименты. Для самостоятельной работы нужна чистая таблица — студент сам построит snapshot-историю и выполнит rollback. -- **[code]** Пересоздание демо-таблицы (тот же CTAS, что в Секции 6): +- **[md]** Готовим демо-таблицу с чистого листа. После демонстраций в Секциях 6-9 её схема изменена (RENAME), а snapshot-история содержит учебные эксперименты. Поскольку `CREATE OR REPLACE` сохраняет snapshot-ы, одноразовую таблицу удаляем через `PURGE` и создаём заново. +- **[code]** Сброс и новое создание демо-таблицы (тот же CTAS, что в Секции 6): ```python +spark.sql(f"DROP TABLE IF EXISTS {DEMO_TABLE} PURGE") spark.sql(""" - CREATE OR REPLACE TABLE lakehouse.default.taxi_changes_demo + CREATE TABLE lakehouse.default.taxi_changes_demo USING iceberg AS SELECT VendorID, tpep_pickup_datetime, tpep_dropoff_datetime, passenger_count, trip_distance, fare_amount, total_amount, @@ -361,7 +366,7 @@ print(f"Демо-таблица пересоздана: {spark.table(DEMO_TABLE) **Задача 4 (recovery).** INSERT 100 строк с `fare_amount = -1` и `pickup_zone = 'STUDENT_ERROR'` в демо-таблицу. Убедись, что «плохие» строки появились (`SELECT count(*) WHERE fare_amount < 0`). Затем выполни `rollback_to_snapshot` к snapshot-у ДО этого INSERT-а. Проверь, что таблица восстановлена: `fare_amount < 0` -> 0 строк, общий count вернулся к предыдущему значению. - **Задача 5 (ответ).** Ответь в markdown-ячейке: чем `rollback_to_snapshot` отличается от `CREATE OR REPLACE TABLE ... AS SELECT * FROM table VERSION AS OF `? Что происходит с историей snapshot-ов в каждом случае? + **Задача 5 (ответ).** Сравни `rollback_to_snapshot` и `CREATE OR REPLACE TABLE ... AS SELECT * FROM table VERSION AS OF `: какая операция создаёт новый snapshot, что становится текущим и как меняется цепочка `parent_id`? - **[code]** Пустая ячейка: `# Ваш код: ALTER TABLE ADD COLUMNS (quality_flag)` - **[code]** Пустая ячейка: `# Ваш код: проверка из Spark и Trino` @@ -390,7 +395,7 @@ print(f"Демо-таблица пересоздана: {spark.table(DEMO_TABLE) ### Секция 13: Завершение - **[md]** Удаляем демо-таблицу — она больше не нужна. Silver остаётся для Модуля 8 (колонка `loaded_at` — демонстрационная; Модуль 8 не зависит от её наличия). -- **[code]** `spark.sql("DROP TABLE IF EXISTS lakehouse.default.taxi_changes_demo")`. +- **[code]** `spark.sql("DROP TABLE IF EXISTS lakehouse.default.taxi_changes_demo PURGE")`. - **[md]** Итог модуля: - Schema evolution (ADD COLUMN) — безопасная metadata-only операция, видна из всех движков. - Snapshot-ы — автоматическая история изменений данных, встроенная в Iceberg. @@ -433,13 +438,14 @@ print(f"Демо-таблица пересоздана: {spark.table(DEMO_TABLE) ## Риски -- **`rollback_to_snapshot` синтаксис.** Точный синтаксис stored procedure зависит от версии Iceberg и Spark. Варианты: именованные параметры (`table => '...', snapshot_id => ...`), позиционные (`'table', snapshot_id`). Нужна проверка на стенде. Fallback: `CREATE OR REPLACE TABLE ... AS SELECT * FROM table VERSION AS OF ` (менее элегантно, уничтожает snapshot-историю, но работает). +- **`rollback_to_snapshot` синтаксис.** Именованные параметры (`table => '...', snapshot_id => ...`) проверены на Spark 3.5.1 и Iceberg 1.10.0. `CREATE OR REPLACE TABLE ... AS SELECT * FROM table VERSION AS OF ` не является эквивалентом rollback: он переписывает данные в новый snapshot без родителя и может заменить схему и partition spec. - **`ALTER TABLE ADD COLUMNS` vs `ADD COLUMN`.** В Spark SQL с Iceberg синтаксис — `ADD COLUMNS` (множественное число). Проверить на стенде. -- **Количество snapshot-ов silver.** Если студент перезапускал Модуль 5 через `CREATE OR REPLACE`, у silver может быть только 1 snapshot. Это нормально — `ADD COLUMN` не создаёт snapshot, но демо-таблица наращивает snapshot-историю через INSERT. -- **Time travel через Trino.** Синтаксис `FOR VERSION AS OF` может отличаться в зависимости от версии Trino. Проверить на стенде. +- **Количество snapshot-ов silver.** После первого запуска Модуля 5 у silver будет 1 snapshot; каждый повторный `CREATE OR REPLACE` добавляет новый, сохраняя старые для time travel. `ADD COLUMN` не создаёт data snapshot, а демо-таблица наращивает snapshot-историю через INSERT. +- **Time travel через Trino.** Синтаксис `FOR VERSION AS OF ` проверен на Trino 478 с общим JDBC-каталогом и MinIO. - **Floating-point в snapshot_id.** Snapshot ID — длинное число (long). При передаче в f-string и SQL нужно убедиться, что Python не конвертирует его в float. Использовать `int(snapshot_id)`. - **RENAME COLUMN ошибка в Trino.** Trino-запрос со старым именем колонки должен упасть с ошибкой. Обернуть в try/except с пояснением в markdown. -- **Идемпотентность.** Повторный запуск ноутбука: `ALTER TABLE ADD COLUMNS (loaded_at TIMESTAMP)` упадёт, если колонка уже существует. Решение зафиксировано: обернуть в try/except с проверкой `"already exists"` в тексте ошибки (см. Секцию 3). Это переносимый подход, не зависящий от поддержки `IF NOT EXISTS` конкретной версией Iceberg. Демо-таблица пересоздаётся через `CREATE OR REPLACE` — идемпотентна. +- **Повторный запуск.** `ALTER TABLE ADD COLUMNS (loaded_at TIMESTAMP)` упадёт, если колонка уже существует. Решение зафиксировано: обернуть в try/except с проверкой `"already exists"` в тексте ошибки (см. Секцию 3). Одноразовая демо-таблица перед каждой исходной CTAS удаляется через `DROP TABLE ... PURGE`, чтобы проверки числа snapshot-ов были воспроизводимы. +- **Сброс демо-таблицы.** Два последовательных цикла `DROP TABLE ... PURGE` + `CREATE TABLE ... AS SELECT` проверены на стенде: каждый цикл начинает историю ровно с одного snapshot-а. - **Влияние на Модуль 8.** Колонка `loaded_at` — демонстрационная. Модуль 8 не должен от неё зависеть. Если студент перезапустит Модуль 5 после Модуля 7, silver будет без `loaded_at` — Модуль 8 должен работать в обоих случаях. ## Out of Scope diff --git a/plans/module-08-maintenance-and-final-lab.md b/plans/module-08-maintenance-and-final-lab.md index 8e25d42..bc868be 100644 --- a/plans/module-08-maintenance-and-final-lab.md +++ b/plans/module-08-maintenance-and-final-lab.md @@ -1,7 +1,7 @@ # Модуль 8. Базовое обслуживание таблиц и финальная практика **Статус:** `Ready for validation` -**Последнее обновление:** `2026-03-08` +**Последнее обновление:** `2026-09-15` ## Цель @@ -166,11 +166,12 @@ Greenplum AppendOnly-таблицы имеют аналогичные пробл - **[md]** Каждый `INSERT INTO` в Iceberg создаёт новые data files. Если вставлять данные мелкими порциями (частые микробатчи, ручные INSERT-ы, инкрементальные загрузки), таблица накапливает множество мелких файлов. Это замедляет чтение: движку приходится открывать и читать каждый файл отдельно. Проблема знакома в мире Hadoop/Hive — и в Iceberg она решается через compaction. -- **[code]** Создание демо-таблицы с 500 строками (CTAS из silver): +- **[code]** Удаление одноразовой демо-таблицы через `DROP TABLE IF EXISTS ... PURGE`, затем создание с 500 строками (CTAS из silver). Это гарантирует один начальный snapshot и воспроизводимое число snapshot-ов при повторном запуске: ```python +spark.sql(f"DROP TABLE IF EXISTS {DEMO_TABLE} PURGE") spark.sql(f""" - CREATE OR REPLACE TABLE {DEMO_TABLE} + CREATE TABLE {DEMO_TABLE} USING iceberg AS SELECT VendorID, tpep_pickup_datetime, tpep_dropoff_datetime, passenger_count, trip_distance, fare_amount, total_amount, @@ -378,7 +379,7 @@ except Exception as e: ### Секция 8: Удаление демо-таблицы - **[md]** Демо-таблица для обслуживания больше не нужна. -- **[code]** `spark.sql(f"DROP TABLE IF EXISTS {DEMO_TABLE}")`. Вывод подтверждения. +- **[code]** `spark.sql(f"DROP TABLE IF EXISTS {DEMO_TABLE} PURGE")`. Вывод подтверждения. ### Секция 9: Финальная практика — сквозной end-to-end сценарий @@ -467,7 +468,7 @@ except Exception as e: - **Исполняемый формат:** 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 сохраняются. +- **Cleanup:** `DROP TABLE ... PURGE` для одноразовой демо-таблицы `taxi_maintenance_demo`. Финальная практика: DROP таблиц в namespace `final`, DROP namespace. Silver и bronze сохраняются. - **Порядок:** проблема мелких файлов → compaction → expire_snapshots → Greenplum-параллель → ошибки → самостоятельное задание (обслуживание) → финальная практика (end-to-end) → checkpoint (модуль + курс) → итог курса. ## Checkpoint @@ -505,7 +506,7 @@ except Exception as e: - **Время выполнения 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`. +- **Повторный запуск.** Перед созданием одноразовой демо-таблицы выполняется `DROP TABLE ... PURGE`, иначе `CREATE OR REPLACE` сохранил бы прежние snapshot-ы и сломал ожидаемое число 9. В финальной практике студент должен учитывать уже существующие namespace и таблицы после прерванного запуска; cleanup в конце выполняется через `IF EXISTS`. - **Независимость от Модуля 7.** Ноутбук не должен ссылаться на колонку `loaded_at` или результаты Модуля 7 как на обязательные предусловия. Snapshot-концепции кратко повторяются в контексте expire_snapshots. ## Out of Scope