fix(course): исправлена семантика Iceberg snapshots
- Зачем: объяснения 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-а.
This commit is contained in:
1 parent
ff474f05f6
commit
a085b2fc55
11 files changed
+106
-76
No files matched your search
@@ -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. Обслуживание и финальная практика
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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_id>`? Что происходит с историей snapshot-ов в каждом случае?\n"
|
||||
"**Задача 5 (ответ).** Сравни `rollback_to_snapshot` и `CREATE OR REPLACE TABLE ... AS SELECT * FROM table VERSION AS OF <snapshot_id>`. Какая операция создаёт новый 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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_id>`? Что происходит с историей snapshot-ов в каждом случае?
|
||||
**Задача 5 (ответ).** Сравни `rollback_to_snapshot` и `CREATE OR REPLACE TABLE ... AS SELECT * FROM table VERSION AS OF <snapshot_id>`: какая операция создаёт новый 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_id>` (менее элегантно, уничтожает 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 <snapshot_id>` не является эквивалентом 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 <snapshot_id>` проверен на 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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user