- Зачем:
- продемонстрировать возможность доступа нескольких движков к одной Iceberg-таблице через общий каталог.
- Что:
- создан notebooks/06_spark_and_trino_on_same_table.ipynb с примерами write (Spark) -> read (Trino).
- в jupyter/Dockerfile добавлен клиент trino.
- в START_HERE.md и docs/stack_reference.md добавлена инструкция по подключению DBeaver к Trino.
- статус модуля 6 в планах обновлен до Ready for validation.
- Проверка:
- ручная проверка выполнения ячеек в ноутбуке (Python trino client).
- проверка наличия всех инструкций в документации.
"# Модуль 6. Одна таблица, два движка: Spark и Trino\n",
"\n",
"В этом модуле мы увидим главное практическое следствие архитектуры Lakehouse: таблица, записанная одним движком, может быть прочитана другим без копирования данных.\n",
"\n",
"**Цели модуля:**\n",
"- записать таблицу через Spark и тут же прочитать её через Trino;\n",
"- выполнить SQL-запросы к Iceberg-таблицам из Trino;\n",
"- понять роль общего каталога и общего хранилища;\n",
"- осознать отличие Lakehouse-модели (decoupled compute) от классического DWH.\n",
"| Где лежат данные | Внутри СУБД, на управляемых дисках | В объектном хранилище (MinIO), отдельно от движков |\n",
"| Где лежат метаданные | `pg_catalog` внутри той же СУБД | Внешний JDBC-каталог (PostgreSQL) + metadata в MinIO |\n",
"| Кто может читать таблицу | Только сама СУБД | Любой движок с доступом к каталогу и хранилищу |\n",
"| Чтобы дать доступ другому инструменту | Подключиться к СУБД или скопировать данные | Подключить тот же каталог — данные уже доступны |\n",
"\n",
"**Ключевая идея:** в Lakehouse данные не заперты в одном движке. Это называется *decoupled compute*. \n",
"\n",
"**Как устроен этот ноутбук:** Spark-код и Trino-запросы выполняются здесь. Для Trino используется Python-клиент `trino`. Если у вас установлен DBeaver (и подключен по инструкции из START_HERE.md) — каждый Trino-запрос можно выполнить и там (в markdown будут подсказки)."
]
},
{
"cell_type": "markdown",
"id": "section_1_title",
"metadata": {},
"source": [
"## 1. Spark-сессия, Trino-клиент и проверка таблиц\n",
"\n",
"Инициализируем Spark и настраиваем helper для отправки запросов в Trino."
"*Если ячейка выше упала с ImportError: trino — нужно пересобрать образ Jupyter (в терминале: `docker compose build jupyter && docker compose up -d`).*\n",
"\n",
"Обе таблицы на месте. Spark может их читать. Trino-клиент готов. Вопрос: может ли Trino прочитать те же таблицы?"
]
},
{
"cell_type": "markdown",
"id": "section_2_title",
"metadata": {},
"source": [
"## 2. Читаем silver через Spark — фиксируем метрики\n",
"\n",
"Сначала получим числа из Spark. Потом сравним с Trino."
"Trino видит те же namespace (bronze, silver, default) и ту же таблицу с не менее чем 24 колонками. Различия в нотации типов (varchar vs STRING, double vs DOUBLE) нормальны — это разница синтаксиса движков, а не данных."
]
},
{
"cell_type": "markdown",
"id": "section_4_title",
"metadata": {},
"source": [
"## 4. «Aha-момент» — одни и те же данные\n",
"\n",
"Центральный момент модуля. Trino читает таблицу, записанную Spark в Модуле 5.\n",
"\n",
"*DBeaver: `SELECT * FROM lakehouse.silver.nyc_taxi_yellow LIMIT 10;`*"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "section_4_select",
"metadata": {},
"outputs": [],
"source": [
"trino_query(\"SELECT tpep_pickup_datetime, fare_amount, pickup_borough FROM lakehouse.silver.nyc_taxi_yellow LIMIT 5\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "section_4_metrics",
"metadata": {},
"outputs": [],
"source": [
"trino_metrics = trino_query(\"\"\"\n",
" SELECT \n",
" count(*) AS row_count, \n",
" avg(fare_amount) AS avg_fare, \n",
" avg(trip_distance) AS avg_distance, \n",
" count(DISTINCT pickup_borough) AS boroughs \n",
" FROM lakehouse.silver.nyc_taxi_yellow\n",
"\"\"\")\n",
"\n",
"print(\"Метрики Trino:\")\n",
"display(trino_metrics)\n",
"\n",
"print(\"Метрики Spark (для сравнения):\")\n",
"display(spark_metrics)"
]
},
{
"cell_type": "markdown",
"id": "section_4_note",
"metadata": {},
"source": [
"Строки совпадают. Оба движка читают одни и те же data files из MinIO (с точностью до floating-point округления в функции `avg`)."
]
},
{
"cell_type": "markdown",
"id": "section_5_title",
"metadata": {},
"source": [
"## 5. Почему это работает — роль общего каталога\n",
"\n",
"**Общий каталог:** оба движка подключены к одному инстансу PostgreSQL (`postgres-iceberg:5432/iceberg`). Когда Spark создаёт таблицу, он записывает ссылку на неё в PostgreSQL. Trino читает тот же PostgreSQL и находит эту ссылку.\n",
"\n",
"**Общее хранилище:** data files (Parquet) и metadata-файлы лежат в MinIO (`s3://lakehouse/warehouse`). Оба движка имеют сетевой доступ к бакету и могут прочитать файлы.\n",
"\n",
"**Параллель с Greenplum:** В классическом DWH данные лежат внутри СУБД на её собственных дисках. Доступ возможен только через саму СУБД. В Lakehouse данные лежат снаружи, а движки взаимозаменяемы.\n",
"\n",
"**Decoupled compute:** Вычислительные ресурсы отделены от хранения. Если нам потребуется добавить третий движок (например, Apache Flink для streaming), нужно будет просто подключить его к тому же каталогу и хранилищу.\n",
"Spark записал таблицу. Мы не делали никакой синхронизации. Видит ли её Trino?\n",
"\n",
"*DBeaver: `SELECT * FROM lakehouse.default.borough_summary ORDER BY trips DESC;`*"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "section_6_read",
"metadata": {},
"outputs": [],
"source": [
"trino_query(\"SELECT * FROM lakehouse.default.borough_summary ORDER BY trips DESC\")"
]
},
{
"cell_type": "markdown",
"id": "section_6_note_2",
"metadata": {},
"source": [
"Trino видит таблицу мгновенно. Spark записал metadata в PostgreSQL и data files в MinIO. Trino прочитал тот же каталог — увидел таблицу. Никакого копирования, никакой синхронизации.\n",
"\n",
"Это и есть decoupled compute: один движок создал, другой прочитал. В Greenplum для этого пришлось бы либо подключиться к тому же движку, либо скопировать данные."
]
},
{
"cell_type": "markdown",
"id": "section_7_title",
"metadata": {},
"source": [
"## 7. Аналитические запросы в Trino\n",
"\n",
"Trino как аналитический SQL-движок. Выполним несколько аналитических запросов.\n",
"\n",
"*DBeaver: скопируй запрос ниже без обёртки trino_query().*"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "section_7_query1",
"metadata": {},
"outputs": [],
"source": [
"trino_query(\"\"\"\n",
" SELECT \n",
" pickup_borough, \n",
" count(*) AS trips, \n",
" avg(fare_amount) AS avg_fare, \n",
" avg(trip_distance) AS avg_distance, \n",
" avg(tip_amount) AS avg_tip \n",
" FROM lakehouse.silver.nyc_taxi_yellow \n",
" WHERE pickup_borough IS NOT NULL \n",
" GROUP BY pickup_borough \n",
" ORDER BY trips DESC\n",
"\"\"\")"
]
},
{
"cell_type": "markdown",
"id": "section_7_note_1",
"metadata": {},
"source": [
"Топ зон посадки:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "section_7_query2_trino",
"metadata": {},
"outputs": [],
"source": [
"trino_top_zones = trino_query(\"\"\"\n",
" SELECT \n",
" pickup_zone, \n",
" count(*) AS trips, \n",
" avg(total_amount) AS avg_total \n",
" FROM lakehouse.silver.nyc_taxi_yellow \n",
" WHERE pickup_zone IS NOT NULL \n",
" GROUP BY pickup_zone \n",
" ORDER BY trips DESC \n",
" LIMIT 10\n",
"\"\"\")\n",
"display(trino_top_zones)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "section_7_query2_spark",
"metadata": {},
"outputs": [],
"source": [
"spark_top_zones = spark.sql(\"\"\"\n",
" SELECT \n",
" pickup_zone, \n",
" count(*) AS trips, \n",
" avg(total_amount) AS avg_total \n",
" FROM lakehouse.silver.nyc_taxi_yellow \n",
" WHERE pickup_zone IS NOT NULL \n",
" GROUP BY pickup_zone \n",
" ORDER BY trips DESC \n",
" LIMIT 10\n",
"\"\"\").toPandas()\n",
"\n",
"display(spark_top_zones)"
]
},
{
"cell_type": "markdown",
"id": "section_7_note_2",
"metadata": {},
"source": [
"Результаты совпадают (с точностью до floating-point). Два движка, одна таблица."
]
},
{
"cell_type": "markdown",
"id": "section_8_title",
"metadata": {},
"source": [
"## 8. Навигация по bronze из Trino\n",
"\n",
"До этого bronze читали только из Spark. Проверим через Trino."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "section_8_code1",
"metadata": {},
"outputs": [],
"source": [
"trino_query(\"SHOW TABLES FROM lakehouse.bronze\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "section_8_code2",
"metadata": {},
"outputs": [],
"source": [
"trino_query(\"SELECT count(*) AS row_count FROM lakehouse.bronze.nyc_taxi_yellow\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "section_8_code3",
"metadata": {},
"outputs": [],
"source": [
"trino_query(\"SELECT * FROM lakehouse.bronze.nyc_taxi_yellow LIMIT 5\")"
]
},
{
"cell_type": "markdown",
"id": "section_8_note",
"metadata": {},
"source": [
"Trino видит все таблицы из всех namespace каталога. Каталог — общий, хранилище — общее. \n",
"\n",
"*DBeaver: те же запросы работают аналогично.*"
]
},
{
"cell_type": "markdown",
"id": "section_9",
"metadata": {},
"source": [
"## 9. DBeaver — SQL-клиент для Trino (рекомендация)\n",
"\n",
"В этом ноутбуке мы работали с Trino через Python-клиент — для воспроизводимости. В реальной аналитической работе Trino чаще используют через SQL-клиенты: DBeaver, DataGrip, DbVisualizer. DBeaver — де-факто стандарт для SQL, знакомый всем, кто работал с PostgreSQL/Greenplum.\n",
"\n",
"Если DBeaver установлен и подключён (инструкция в START_HERE.md), попробуйте выполнить в нём любой запрос из этого модуля. Результат будет тот же — тот же протокол, тот же движок, та же таблица. Разница: DBeaver — для интерактивной SQL-работы, Python-клиент — для автоматизации и ноутбуков."
]
},
{
"cell_type": "markdown",
"id": "section_10_title",
"metadata": {},
"source": [
"## 10. Самостоятельное задание\n",
"\n",
"Четыре задачи. Центральная — самостоятельный цикл write → read, как в Секции 6.\n",
"\n",
"**Задача 1 (Spark → write).** Создай через Spark новую агрегатную таблицу `lakehouse.default.zone_summary` с CTAS: для каждой `pickup_zone` посчитай `count(*)`, `avg(total_amount)`, `avg(tip_amount)` по silver-таблице (WHERE pickup_zone IS NOT NULL).\n",
"\n",
"**Задача 2 (Trino → read).** Прочитай `lakehouse.default.zone_summary` через Trino (Python-клиент или DBeaver). Убедись, что Trino видит таблицу без синхронизации.\n",
"\n",
"**Задача 3 (сравнение).** Выполни тот же агрегатный запрос напрямую по silver через Trino (без промежуточной таблицы). Сравни результат с `zone_summary`. Числа должны совпасть.\n",
"\n",
"**Задача 4 (ответ).** Ответь в markdown-ячейке ниже: почему Trino увидел `zone_summary` сразу после создания через Spark? Какие три компонента стенда это обеспечивают? Что произошло бы, если бы у Trino был другой каталог?"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "section_10_task1",
"metadata": {},
"outputs": [],
"source": [
"# Ваш код (Spark): CREATE TABLE zone_summary\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "section_10_task2",
"metadata": {},
"outputs": [],
"source": [
"# Ваш код (Trino): чтение zone_summary\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "section_10_task3",
"metadata": {},
"outputs": [],
"source": [
"# Ваш код (Trino): тот же агрегат напрямую по silver\n"
]
},
{
"cell_type": "markdown",
"id": "section_10_task4",
"metadata": {},
"source": [
"Ваш ответ на Задачу 4: ...\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "section_10_cleanup",
"metadata": {},
"outputs": [],
"source": [
"spark.sql(\"DROP TABLE IF EXISTS lakehouse.default.zone_summary\")"
]
},
{
"cell_type": "markdown",
"id": "section_10_optional",
"metadata": {},
"source": [
"Дополнительная задача (по желанию): аналитический запрос по bronze через Trino и через Spark, сравнение."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "section_10_opt_trino",
"metadata": {},
"outputs": [],
"source": [
"# Ваш код (Trino): аналитический запрос по bronze\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "section_10_opt_spark",
"metadata": {},
"outputs": [],
"source": [
"# Ваш код (Spark): тот же запрос по bronze\n"
]
},
{
"cell_type": "markdown",
"id": "section_11",
"metadata": {},
"source": [
"## 11. Что мы НЕ сравниваем\n",
"\n",
"Ограничение модуля: мы не сравниваем Spark и Trino как движки. Не обсуждаем: какой быстрее, какой лучше, каковы их внутренние различия оптимизаторов. \n",
"\n",
"Цель — показать, что оба работают с одной таблицей через общий каталог. Это фундаментальное свойство архитектуры Lakehouse, а не конкретного движка."
]
},
{
"cell_type": "markdown",
"id": "section_12",
"metadata": {},
"source": [
"## 12. Checkpoint\n",
"\n",
"Проверь себя:\n",
"1. Почему Trino видит таблицы, созданные Spark, без синхронизации?\n",
"2. Какие два компонента стенда общие для Spark и Trino?\n",
"3. Чем доступ к данным в Lakehouse отличается от Greenplum?\n",
"4. Нужно ли копировать данные, чтобы Trino прочитал таблицу Spark?\n",
"5. Что такое «decoupled compute» в одном предложении?\n",
"6. Если добавить третий движок (Flink), что нужно сделать, чтобы он увидел те же таблицы?\n",
"7. Что произошло, когда Spark создал `borough_summary`, а Trino её тут же прочитал?"
]
},
{
"cell_type": "markdown",
"id": "section_13",
"metadata": {},
"source": [
"## 13. Завершение\n",
"\n",
"Удаляем демо-таблицу, она больше не нужна. Таблицы `bronze` и `silver` остаются для следующих модулей.\n",
"\n",
"В Модуле 7 — schema evolution и time travel. В Модуле 8 — финальная практика."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "section_13_stop",
"metadata": {},
"outputs": [],
"source": [
"spark.sql(\"DROP TABLE IF EXISTS lakehouse.default.borough_summary\")\n",
| 3. Raw-данные и первое чтение в Spark | [module-03-raw-ingest-and-first-read.md](./module-03-raw-ingest-and-first-read.md) | Ready for validation | `docker-compose.yml`, `.gitignore`, `START_HERE.md`, `docs/stack_reference.md`, `notebooks/03_raw_ingest_and_first_read.ipynb` | 2026-03-07 |
| 4. Первая рабочая Iceberg-таблица и слой bronze | [module-04-bronze-with-iceberg.md](./module-04-bronze-with-iceberg.md) | Draft | `notebooks/04_bronze_with_iceberg.ipynb` | 2026-03-07 |
| 5. Слой silver и воспроизводимые трансформации | [module-05-silver-layer.md](./module-05-silver-layer.md) | Ready for validation | `notebooks/05_silver_layer.ipynb` | 2026-03-07 |
| 6. Одна таблица, два движка: Spark и Trino | [module-06-spark-and-trino-on-same-table.md](./module-06-spark-and-trino-on-same-table.md) | Draft | `jupyter/Dockerfile`, `START_HERE.md`, `docs/stack_reference.md`, `notebooks/06_spark_and_trino_on_same_table.ipynb` | 2026-03-07 |
| 6. Одна таблица, два движка: Spark и Trino | [module-06-spark-and-trino-on-same-table.md](./module-06-spark-and-trino-on-same-table.md) | Ready for validation | `jupyter/Dockerfile`, `START_HERE.md`, `docs/stack_reference.md`, `notebooks/06_spark_and_trino_on_same_table.ipynb` | 2026-03-07 |
| 7. Безопасная работа с таблицами: schema evolution и time travel | [module-07-safe-table-changes.md](./module-07-safe-table-changes.md) | Draft | `notebooks/07_safe_table_changes.ipynb` | 2026-03-07 |
| 8. Базовое обслуживание таблиц и финальная практика | [module-08-maintenance-and-final-lab.md](./module-08-maintenance-and-final-lab.md) | Draft | `notebooks/08_maintenance_and_final_lab.ipynb` | 2026-03-07 |
# Модуль 6. Одна таблица, два движка: Spark и Trino
**Статус:** `Draft`
**Статус:** `Ready for validation`
**Последнее обновление:** `2026-03-07`
## Цель
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.