docs: связаны карта DWH, разбор SQL и задания

This commit is contained in:
ddadmin committed 2026-09-20 14:31:49 -04:00
1 parent b6e950f52c
commit b7ecfb1bb7
8 files changed
+342 -149

No files matched your search

+69 -60
View File
@@ -1,85 +1,94 @@
# Учебные задания
## О чём задание
В стенде уже работает цепочка от Bookings до `dm.sales_report`.
Вы дополните ее загрузками ODS, измерениями DDS и четырьмя витринами DM.
Поля, преобразования и бизнес-правила описаны в [ТЗ](analyst_spec.md).
В стенде уже работает **эталонный вертикальный срез** — полная цепочка
`STG → ODS → DDS → DM` для витрины `dm.sales_report`. Ваша задача —
реализовать недостающие загрузки по аналогии с эталоном.
## Подготовка
На месте ваших заданий сейчас стоят **заглушки** (`SELECT 1;`). Вы замените
их на реальную SQL-логику, и после этого весь DWH будет заполнен.
Пройдите шаги 1–6 [быстрого старта](../../README.md#быстрый-старт-основной-сценарий-bookings--stg--ods--dds--dm):
поднимите стенд, создайте таблицы, запустите эталонную цепочку и проверьте данные.
Затем прочитайте [руководство о структуре DWH](../design/db_schema.md).
Открывайте [карту](../design/architecture-map.html) рядом с заданием;
для Gitea есть [инструкция открытия локального HTML](../design/db_schema.md#как-открыть-карту).
## С чего начать чтение
Таблицы уже созданы, задачи Airflow подключены к SQL-файлам.
В учебных `*_load.sql` и `*_dq.sql` пока стоит `SELECT 1;`.
Вы заменяете эти заглушки; менять DAG и создавать таблицы заново не требуется.
Сначала посмотрите [руководство о структуре DWH и карту](../design/db_schema.md).
Пока пишете свой слой, держите рядом [метод чтения скрипта загрузки](../design/reading_the_pipeline.md).
Для `*_dq.sql` сверяйтесь с [классами DQ-проверок](../reference/dq_taxonomy.md).
## Первая загрузка: ODS
## Перед началом
Начните с `ods.airplanes`, затем добавьте `ods.seats`.
На карте откройте [модели самолетов](../design/architecture-map.html#node=ods.airplanes)
и [места](../design/architecture-map.html#node=ods.seats): источники STG уже заполнены.
Позже обе таблицы понадобятся `dds.dim_airplanes` для характеристик модели
и расчета ее вместимости.
Убедитесь, что стенд запущен и данные загружены —
пройдите шаги 1–6 из [README (Быстрый старт)](../../README.md#быстрый-старт-основной-сценарий-bookings--stg--ods--dds--dm).
### Готовый пример
После этого в Greenplum будут данные в эталонных таблицах (`stg.*`, `ods.bookings`,
`dds.dim_airports`, `dds.fact_flight_sales`, `dm.sales_report` и др.).
Пройдите [разбор stg.airports → ods.airports](../design/reading_the_pipeline.md#ods-airports).
Это близкий пример полного снимка с выбором батча, преобразованиями и дедупликацией.
Для первого задания этого разбора достаточно.
## Шаг 1. Изучите эталон
### Самостоятельная работа
Прежде чем писать код, разберите, как устроены эталонные загрузки.
По одному файлу на каждый ключевой паттерн:
Откройте [часть 1 ТЗ](analyst_spec.md#часть-1-ods-слой-operational-data-store).
Сверяйте с ней поля и правила каждой таблицы:
| Паттерн | Файл для изучения | Что посмотреть |
|---------|-------------------|----------------|
| ODS: TRUNCATE + INSERT (snapshot-справочник) | `sql/ods/airports_load.sql` | Фильтрация по `stg_batch_id`, дедупликация через `ROW_NUMBER()` |
| DDS: SCD1 UPSERT (измерение) | `sql/dds/dim_airports_load.sql` | TEMP TABLE → UPDATE (`IS DISTINCT FROM`) → INSERT |
| DM: витрина с UPSERT | `sql/dm/sales_report_load.sql` | HWM-инкремент, агрегация фактов по измерениям |
| Объект и требования | Файлы для реализации | DDL для сверки |
|---|---|---|
| [ods.airplanes](analyst_spec.md#11-odsairplanes) | [airplanes_load.sql](../../sql/ods/airplanes_load.sql), [airplanes_dq.sql](../../sql/ods/airplanes_dq.sql) | [airplanes_ddl.sql](../../sql/ods/airplanes_ddl.sql) |
| [ods.seats](analyst_spec.md#12-odsseats) | [seats_load.sql](../../sql/ods/seats_load.sql), [seats_dq.sql](../../sql/ods/seats_dq.sql) | [seats_ddl.sql](../../sql/ods/seats_ddl.sql) |
Описания каждого DAG — в `docs/`:
[STG](../bookings_to_gp_stage.md) ·
[ODS](../bookings_to_gp_ods.md) ·
[DDS](../bookings_to_gp_dds.md) ·
[DM](../bookings_to_gp_dm.md)
Пример проверок: [airports_dq.sql](../../sql/ods/airports_dq.sql).
Учтите разницу ключей: у модели это `airplane_code`, у места -
`(airplane_code, seat_no)`.
## Шаг 2. Откройте ТЗ
### Запуск и проверка
Техническое задание от аналитика: **[analyst_spec.md](analyst_spec.md)**.
1. Дождитесь успешного `bookings_to_gp_stage` из подготовки.
Если STG уже заполнен, новый запуск для этой работы не нужен.
2. Запустите `bookings_to_gp_ods` целиком. Он выберет согласованный снимок
в `resolve_stg_batch_id`, затем выполнит
`load_ods_airplanes → dq_ods_airplanes → load_ods_seats → dq_ods_seats`.
SQL использует XCom этого запуска, поэтому отдельно от выбора батча
загрузку запускать не стоит.
3. В Greenplum посмотрите строки обеих таблиц: модели, типы числовых полей,
ключи мест и метки загрузки. Повторите ODS без нового запуска STG:
бизнес-строки и количество ключей должны сохраниться без дублей.
4. После реализации обеих таблиц запустите `bookings_validate` и откройте
группу `validate_ods`. Она проверит наличие данных, совпадение ключей
с выбранным STG-батчем, отсутствие дублей и NULL в ключах,
а также общий `_load_id` двух таблиц.
Там описаны все таблицы, которые нужно реализовать, с маппингами, бизнес-правилами
и подсказками. Рекомендуемый порядок (от простого к сложному):
В `bookings_validate` автоматически запускаются все три группы.
Пока готовы только задания ODS, оценивайте `validate_ods`: ошибки DDS и DM
на этом этапе ожидаемы. После выполнения всех заданий должны пройти все группы.
`*_dq.sql` тоже нужно реализовать: успешный `SELECT 1;` ничего не проверяет.
1. ODS: `airplanes`, `seats`
2. DDS: `dim_airplanes`, `dim_passengers` (SCD1)
3. DDS: `dim_routes` (SCD2 — ключевой вызов курсовой)
4. DM: `airport_traffic`, `route_performance`, `monthly_overview`, `passenger_loyalty`
## Следующие этапы
## Шаг 3. Реализуйте задания
| Этап | Требования | Готовый пример рядом |
|---|---|---|
| DDS: `dim_airplanes`, `dim_passengers` | [Модели](analyst_spec.md#21-ddsdim_airplanes-scd1), [пассажиры](analyst_spec.md#22-ddsdim_passengers-scd1) | [dim_airports_load.sql](../../sql/dds/dim_airports_load.sql) |
| DDS: `dim_routes` | [Версии маршрута SCD2](analyst_spec.md#23-ddsdim_routes-scd2) | [Разбор сборки факта](../design/reading_the_pipeline.md#fact-flight-sales) показывает, как используется версия |
| DM: `airport_traffic` → `route_performance` → `monthly_overview` → `passenger_loyalty` | [Часть 3 ТЗ](analyst_spec.md#часть-3-dm-слой-data-marts--витрины) | [sales_report_load.sql](../../sql/dm/sales_report_load.sql) |
- **Файлы-заглушки уже на месте** — например, `sql/ods/airplanes_load.sql`
содержит `SELECT 1;`. Ваша задача: заменить `SELECT 1;` на реальную SQL-логику.
- **DDL уже создан** — таблицы существуют, создавать их не нужно.
- **Таски в DAG-ах уже подключены** — `PostgresOperator` ссылается на ваши SQL-файлы.
Менять Python-код DAG-ов не нужно.
- **Нейминг полей:** сверяйтесь с `docs/design/naming_conventions.md`.
## Шаг 4. Проверьте себя валидационным DAG-ом
DAG **`bookings_validate`** — автоматическая проверка вашей реализации.
- Три группы проверок: `validate_ods`, `validate_dds`, `validate_dm` — запускаются
параллельно. Можно проверять ODS, пока DDS ещё не готов.
- Включает активный SCD2-тест для `dim_routes`: загружает изменённые данные
и проверяет, что версионирование работает корректно.
- Сообщения об ошибках подскажут, что именно не так и что делать дальше.
**Цель: все проверки зелёные.**
Перед DDS разберите [загрузку факта](../design/reading_the_pipeline.md#fact-flight-sales).
После реализации измерений выполните [пересчет факта и витрин](analyst_spec.md#пересчёт-факта-после-реализации-измерений).
Проверяйте готовый слой своей группой в `bookings_validate`.
В DDS есть активный SCD2-тест: он временно меняет данные маршрута,
запускает вашу загрузку и затем восстанавливает тестируемые данные.
## Если застряли
- Изучите эталонные SQL-скрипты — в них те же паттерны, что нужны для заданий.
- Загляните в `docs/design/naming_conventions.md` — единый источник нейминга полей.
- Сверьтесь с **веткой `solution`** — там полная рабочая реализация.
Вернитесь к карточке объекта на карте: там есть DDL, загрузка, DQ и пункт ТЗ.
Для конкретного вопроса используйте [соглашения об именах](../design/naming_conventions.md),
[справочник DQ](../reference/dq_taxonomy.md) и руководства DAG:
[ODS](../bookings_to_gp_ods.md), [DDS](../bookings_to_gp_dds.md), [DM](../bookings_to_gp_dm.md).
Полную реализацию можно посмотреть в ветке `solution`.
## Для менторов
Дизайн заданий и педагогическая логика — в ветке `solution`
(`docs/design/assignment_design.md`).
Дизайн заданий и педагогическая логика находятся в ветке `solution`,
в `docs/design/assignment_design.md`.
+73 -19
View File
@@ -13,6 +13,11 @@
>
> SQL-скрипты эталона лежат в `sql/stg/`, `sql/ods/`, `sql/dds/`, `sql/dm/`.
Порядок запуска и проверки первой загрузки находится во
[входе в задания](README.md#первая-загрузка-ods). В каждом разделе ниже есть
ссылка на объект карты и близкий готовый пример. Карту открывайте
[локально в браузере](../design/db_schema.md#как-открыть-карту).
---
## Рекомендуемый порядок выполнения
@@ -36,7 +41,7 @@
## Общие правила
- **Нейминг полей:** см. `docs/design/naming_conventions.md` — единый источник
- **Нейминг полей:** см. [соглашения об именах](../design/naming_conventions.md) — единый источник
истины для служебных полей (`_load_id`, `_load_ts`, `created_at`, `updated_at`,
`valid_from`, `valid_to`, `hashdiff`, суффиксы `_bk` / `_sk`).
- **SQL-файлы:** располагайте в `sql/{слой}/{объект}_{роль}.sql`
@@ -46,7 +51,10 @@
Вам нужно только заменить содержимое SQL-файлов — DAG менять не нужно.
- **Идемпотентность:** каждый скрипт загрузки должен быть безопасен
при повторном запуске (не создавать дубликатов).
- **Шаблон `{{ run_id }}`:** используйте Jinja-шаблон Airflow для `_load_id`.
- **Метки загрузки:** в ODS-снимках `airplanes` и `seats` сохраняйте в `_load_id`
идентификатор исходного STG-батча из `resolve_stg_batch_id`, как в `ods.airports`.
Так проверка сможет сопоставить ODS с тем снимком, из которого он загружен.
В заданиях DDS и DM используйте `'{{ run_id }}'` текущего запуска слоя.
---
@@ -67,11 +75,11 @@ STG-слой (все 9 таблиц) и `ods.routes` полностью реал
`ods.routes` тоже эталонный: факт `fact_flight_sales` использует его для резолвинга
аэропортов вылета/прилёта, независимо от студенческого `dds.dim_routes`.
**Что вам делать:** Изучите эталонные скрипты как образец — именно так написан
«боевой» код загрузки:
- `sql/stg/airports_load.sql` — инкрементальная загрузка (HWM)
- `sql/stg/airplanes_load.sql` — full snapshot с проверкой `_load_id`
- `sql/ods/airports_load.sql` — TRUNCATE+INSERT из STG
Для первого задания разберите [загрузку `ods.airports`](../design/reading_the_pipeline.md#ods-airports).
При необходимости проследить получение снимка откройте
[sql/stg/airports_load.sql](../../sql/stg/airports_load.sql) или
[sql/stg/airplanes_load.sql](../../sql/stg/airplanes_load.sql): оба читают полный
снимок источника. `NOT EXISTS` защищает от повторной вставки ключа в том же `_load_id`.
---
@@ -81,10 +89,14 @@ STG-слой (все 9 таблиц) и `ods.routes` полностью реал
> Справочники (`airplanes`, `seats`) загружаются стратегией TRUNCATE + INSERT
> из последнего согласованного батча STG. (`ods.routes` реализован в эталоне.)
>
> **Аналог для изучения:** `sql/ods/airports_ddl.sql`, `sql/ods/airports_load.sql`
> **Аналог для изучения:** [DDL airports](../../sql/ods/airports_ddl.sql),
> [загрузка airports](../../sql/ods/airports_load.sql) и [ее разбор](../design/reading_the_pipeline.md#ods-airports).
### 1.1. ods.airplanes
[На карте: модель самолета и ее источник](../design/architecture-map.html#node=ods.airplanes).
Готовый пример полного снимка: [ods.airports](../design/reading_the_pipeline.md#ods-airports).
**Описание:** Очищенный справочник моделей воздушных судов с правильными типами.
**Источник:** `stg.airplanes`
@@ -95,7 +107,7 @@ STG-слой (все 9 таблиц) и `ods.routes` полностью реал
| `model` | TEXT NOT NULL | Наименование модели | `model` (парсинг JSON: `model::JSON->>'ru'` — если JSON, иначе `model` как есть) |
| `range_km` | INTEGER | Дальность полёта, км | `range::INTEGER` |
| `speed_kmh` | INTEGER | Крейсерская скорость, км/ч | `speed::INTEGER` |
| `_load_id` | TEXT NOT NULL | Идентификатор батча | `'{{ run_id }}'` |
| `_load_id` | TEXT NOT NULL | Идентификатор исходного STG-батча | `'{{ ti.xcom_pull(task_ids="resolve_stg_batch_id") }}'` |
| `_load_ts` | TIMESTAMP NOT NULL | Момент загрузки | `now()` |
**Тип историзации:** Нет (текущее состояние справочника, TRUNCATE + INSERT).
@@ -115,6 +127,9 @@ STG-слой (все 9 таблиц) и `ods.routes` полностью реал
### 1.2. ods.seats
[На карте: места и связь с моделью](../design/architecture-map.html#node=ods.seats).
Пример выбора снимка и дедупликации: [ods.airports](../design/reading_the_pipeline.md#ods-airports).
**Описание:** Карта посадочных мест с корректными типами.
**Источник:** `stg.seats`
@@ -124,7 +139,7 @@ STG-слой (все 9 таблиц) и `ods.routes` полностью реал
| `airplane_code` | TEXT NOT NULL | Код модели (PK, часть 1) | `airplane_code` |
| `seat_no` | TEXT NOT NULL | Номер места (PK, часть 2) | `seat_no` |
| `fare_conditions` | TEXT NOT NULL | Класс обслуживания | `fare_conditions` |
| `_load_id` | TEXT NOT NULL | Идентификатор батча | `'{{ run_id }}'` |
| `_load_id` | TEXT NOT NULL | Идентификатор исходного STG-батча | `'{{ ti.xcom_pull(task_ids="resolve_stg_batch_id") }}'` |
| `_load_ts` | TIMESTAMP NOT NULL | Момент загрузки | `now()` |
**Тип историзации:** Нет (текущее состояние справочника, TRUNCATE + INSERT).
@@ -149,10 +164,14 @@ STG-слой (все 9 таблиц) и `ods.routes` полностью реал
> ключами. SCD1-измерения обновляют атрибуты «на месте». SCD2-измерение
> хранит историю изменений через версионирование.
>
> **Аналог для SCD1:** `sql/dds/dim_airports_ddl.sql`, `sql/dds/dim_airports_load.sql`
> **Аналог для SCD1:** [DDL dim_airports](../../sql/dds/dim_airports_ddl.sql),
> [загрузка dim_airports](../../sql/dds/dim_airports_load.sql).
### 2.1. dds.dim_airplanes (SCD1)
[На карте: характеристики и вместимость модели](../design/architecture-map.html#node=dds.dim_airplanes).
Пример SCD1: [dim_airports_load.sql](../../sql/dds/dim_airports_load.sql).
**Описание:** Измерение моделей самолётов. Содержит технические характеристики
и рассчитанное общее количество мест (обогащение из `ods.seats`).
@@ -184,7 +203,8 @@ STG-слой (все 9 таблиц) и `ods.routes` полностью реал
в `ods.seats` для данного `airplane_code` (LEFT JOIN).
**Генерация суррогатного ключа:** `MAX(airplane_sk) + ROW_NUMBER()`.
Безопасно при `concurrency=1` в DAG.
Загрузки одной таблицы должны выполняться последовательно. В DDS DAG
параллельные запуски ограничены параметром `max_active_runs=1`.
**Distribution Key:** `airplane_sk`
@@ -192,6 +212,9 @@ STG-слой (все 9 таблиц) и `ods.routes` полностью реал
### 2.2. dds.dim_passengers (SCD1)
[На карте: пассажир из билетов](../design/architecture-map.html#node=dds.dim_passengers).
Пример обновления атрибутов и вставки новых ключей: [dim_airports_load.sql](../../sql/dds/dim_airports_load.sql).
**Описание:** Измерение пассажиров. Извлекается из таблицы билетов — каждый
уникальный `passenger_id` становится строкой измерения.
@@ -230,6 +253,10 @@ STG-слой (все 9 таблиц) и `ods.routes` полностью реал
### 2.3. dds.dim_routes (SCD2)
[На карте: версии маршрута](../design/architecture-map.html#node=dds.dim_routes).
В [разборе готового факта](../design/reading_the_pipeline.md#fact-joins) видно,
как загрузчик выбирает версию на дату рейса. Алгоритм самой SCD2-загрузки дан ниже.
> **Это ключевой вызов курсовой.** Реализация SCD Type 2 — обязательный навык
> для Data Engineer. Ниже — алгоритм текстом; SQL вы пишете самостоятельно.
> Если застряли — сверьтесь с веткой `solution`.
@@ -317,10 +344,15 @@ md5(concat_ws('|',
> **Цель:** Построить аналитические витрины поверх DDS.
> Каждая витрина отвечает на конкретный бизнес-вопрос.
>
> **Аналог для изучения:** `sql/dm/sales_report_ddl.sql`, `sql/dm/sales_report_load.sql`
> **Аналог для изучения:** [DDL sales_report](../../sql/dm/sales_report_ddl.sql),
> [загрузка sales_report](../../sql/dm/sales_report_load.sql).
### 3.1. dm.airport_traffic
[На карте: аэропорт за день](../design/architecture-map.html#node=dm.airport_traffic).
Пример пересчета затронутых дат: [sales_report_load.sql](../../sql/dm/sales_report_load.sql).
Две роли аэропорта проследите в [готовом факте](../design/reading_the_pipeline.md#fact-joins).
**Бизнес-вопрос:** «Какой пассажиропоток и выручка у каждого аэропорта по дням?»
**Описание:** Ежедневная статистика по каждому аэропорту: сколько рейсов
@@ -359,6 +391,11 @@ md5(concat_ws('|',
и `arrival_airport_sk`. Затем сгруппируйте по `(traffic_date, airport_sk)`.
**Метрики:**
Здесь `SUM(is_boarded)` означает число строк с признаком посадки.
Поле булево; в SQL используйте `SUM(CASE WHEN is_boarded THEN 1 ELSE 0 END)`,
как в `sales_report_load.sql`.
- `departures_flights` — `COUNT(DISTINCT flight_id)` для роли «вылет»
- `departures_passengers` — `SUM(is_boarded)` для роли «вылет»
- `departures_revenue` — `SUM(price)` для роли «вылет»
@@ -377,6 +414,10 @@ md5(concat_ws('|',
### 3.2. dm.route_performance
[На карте: статистика маршрута](../design/architecture-map.html#node=dm.route_performance).
Соединение факта с измерениями и агрегация показаны в
[sales_report_load.sql](../../sql/dm/sales_report_load.sql); здесь по ТЗ нужен полный пересчет.
**Бизнес-вопрос:** «Какие маршруты самые эффективные? Где высокий load factor,
а где теряем пассажиров?»
@@ -427,9 +468,8 @@ md5(concat_ws('|',
- `avg_boarding_rate` — `AVG(CASE WHEN is_boarded THEN 1 ELSE 0 END)`
- `avg_load_factor` — `total_boarded / (total_flights * total_seats)`
**Тип хранения Greenplum:** AO Column Store (zstd). Идеален для аналитики:
отличное сжатие, чтение только нужных колонок. AO не поддерживает UPDATE —
поэтому используем Full Rebuild.
**Тип хранения Greenplum:** AO Column Store (zstd), сжатое колоночное хранение.
В этом задании небольшой результат полностью пересоздается при каждом запуске.
**Distribution Key:** `route_bk`
@@ -440,6 +480,10 @@ md5(concat_ws('|',
### 3.3. dm.monthly_overview
[На карте: месяц и модель самолета](../design/architecture-map.html#node=dm.monthly_overview).
Пример отбора затронутых периодов: [sales_report_load.sql](../../sql/dm/sales_report_load.sql).
В этом задании период и уровни агрегации определены ниже.
**Бизнес-вопрос:** «Какова помесячная динамика: рейсы, выручка, load factor
в разрезе типов самолётов?»
@@ -479,9 +523,11 @@ md5(concat_ws('|',
**Ключевой приём — Двухуровневая агрегация:**
Чтобы честно посчитать среднюю заполняемость (`avg_load_factor`), нельзя просто
поделить `SUM(boarded)` на `SUM(seats)` — это даёт ошибку (парадокс Симпсона).
Правильный путь:
В этом задании `avg_load_factor` - среднее долей занятых мест по рейсам:
каждый рейс имеет одинаковый вес. Отношение `SUM(boarded) / SUM(seats)`
дает среднее с весом по вместимости и при разной вместимости может отличаться.
Нужный расчет:
1. **Уровень 1 (рейс):** Для каждого `flight_id` посчитайте `load_factor = boarded / total_seats`.
2. **Уровень 2 (месяц):** Возьмите `AVG(load_factor)` по всем рейсам месяца.
@@ -502,6 +548,10 @@ md5(concat_ws('|',
### 3.4. dm.passenger_loyalty
[На карте: история пассажира](../design/architecture-map.html#node=dm.passenger_loyalty).
В [sales_report_load.sql](../../sql/dm/sales_report_load.sql) посмотрите отделение
затронутых дат от полной истории за эти даты. Здесь единица пересчета - пассажир.
**Бизнес-вопрос:** «Кто наши самые лояльные пассажиры? Сколько они летают,
тратят, какой класс предпочитают?»
@@ -568,6 +618,10 @@ md5(concat_ws('|',
## Пересчёт факта после реализации измерений
[Факт на карте](../design/architecture-map.html#node=dds.fact_flight_sales) и
[разбор его INSERT и UPDATE](../design/reading_the_pipeline.md#найдите-запись-результата)
помогут проследить, когда заполняются ключи измерений.
После того, как вы реализуете все DDS-измерения (`dim_airplanes`, `dim_passengers`,
`dim_routes`), нужно пересчитать факт — он загружался ещё без ваших SK: