docs(main): онбординг студента — гид, маркетинг, адаптация docs и docstrings

- Зачем:
  - на main docs и DAG-docstrings утверждали «все реализовано», хотя 18 заглушек;
    студент застревал после шага 6 без явного «что дальше».
- Что:
  - README: маркетинг-баннер, шаг 6 (только эталонные таблицы), шаг 7 → задания.
  - docs/assignment/README: полный гид студента (эталон → ТЗ → заглушки → validate).
  - analyst_spec: DAG-интеграция — «таски уже подключены, менять DAG не нужно».
  - docs ODS/DDS/DM: пометки заглушек, адаптация секций проверки результата.
  - 4 DAG docstrings: эталон vs задания (заглушки).
- Проверка:
  - make test (4 passed, 14 skipped), make lint (clean).
  - grep «все 5 витрин|все реализован» — ложных утверждений без оговорок нет.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 10:49:40 +03:00
co-authored by Claude Opus 4.6
parent 5b98a5b203
commit af27314878
10 changed files with 139 additions and 41 deletions
+68 -9
View File
@@ -1,18 +1,77 @@
# Учебные задания
## Техническое задание
## О чём задание
Основной документ: **[analyst_spec.md](analyst_spec.md)** — ТЗ от аналитика
с описанием всех таблиц, маппингами, бизнес-правилами и подсказками.
В стенде уже работает **эталонный вертикальный срез**полная цепочка
`STG → ODS → DDS → DM` для витрины `dm.sales_report`. Ваша задача —
реализовать недостающие загрузки по аналогии с эталоном.
## Эталон для изучения
На месте ваших заданий сейчас стоят **заглушки** (`SELECT 1;`). Вы замените
их на реальную SQL-логику, и после этого весь DWH будет заполнен.
Перед началом работы изучите эталонный срез (витрина `dm.sales_report`
и вся её цепочка STG → ODS → DDS → DM):
## Перед началом
- SQL-скрипты: `sql/stg/`, `sql/ods/`, `sql/dds/`, `sql/dm/`
- DAG-файлы: `airflow/dags/`
- [Порядок запуска DAG-ов](../dag_execution_order.md)
Убедитесь, что стенд запущен и данные загружены —
пройдите шаги 1–6 из [README (Быстрый старт)](../../README.md#быстрый-старт-основной-сценарий-bookings--stg--ods--dds--dm).
После этого в Greenplum будут данные в эталонных таблицах (`stg.*`, `ods.bookings`,
`dds.dim_airports`, `dds.fact_flight_sales`, `dm.sales_report` и др.).
## Шаг 1. Изучите эталон
Прежде чем писать код, разберите, как устроены эталонные загрузки.
По одному файлу на каждый ключевой паттерн:
| Паттерн | Файл для изучения | Что посмотреть |
|---------|-------------------|----------------|
| 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-инкремент, агрегация фактов по измерениям |
Описания каждого 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)
## Шаг 2. Откройте ТЗ
Техническое задание от аналитика: **[analyst_spec.md](analyst_spec.md)**.
Там описаны все таблицы, которые нужно реализовать, с маппингами, бизнес-правилами
и подсказками. Рекомендуемый порядок (от простого к сложному):
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. Реализуйте задания
- **Файлы-заглушки уже на месте** — например, `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`: загружает изменённые данные
и проверяет, что версионирование работает корректно.
- Сообщения об ошибках подскажут, что именно не так и что делать дальше.
**Цель: все проверки зелёные.**
## Если застряли
- Изучите эталонные SQL-скрипты — в них те же паттерны, что нужны для заданий.
- Загляните в `docs/design/naming_conventions.md` — единый источник нейминга полей.
- Сверьтесь с **веткой `solution`** — там полная рабочая реализация.
## Для менторов
+3 -2
View File
@@ -41,8 +41,9 @@
`valid_from`, `valid_to`, `hashdiff`, суффиксы `_bk` / `_sk`).
- **SQL-файлы:** располагайте в `sql/{слой}/{объект}_{роль}.sql`
(например, `sql/stg/airplanes_ddl.sql`, `sql/stg/airplanes_load.sql`).
- **DAG-интеграция:** используйте `PostgresOperator` + путь к SQL-файлу.
Добавьте таски в существующие DAG-файлы соответствующего слоя.
- **DAG-интеграция:** таски для всех заданий уже подключены в DAG-файлах
соответствующего слоя (`PostgresOperator` + путь к SQL-файлу).
Вам нужно только заменить содержимое SQL-файлов — DAG менять не нужно.
- **Идемпотентность:** каждый скрипт загрузки должен быть безопасен
при повторном запуске (не создавать дубликатов).
- **Шаблон `{{ run_id }}`:** используйте Jinja-шаблон Airflow для `_load_id`.
+18 -5
View File
@@ -11,6 +11,9 @@ point-in-time join, защитные LEFT JOIN для устойчивости
- `dds.dim_airports`, `dds.dim_airplanes`, `dds.dim_tariffs`, `dds.dim_passengers` — SCD1 UPSERT;
- `dds.dim_routes`**SCD2** с `hashdiff`, `valid_from`, `valid_to` + денормализация.
- Загружает факт `dds.fact_flight_sales` — инкрементальный UPSERT по зерну `(ticket_no, flight_id)`.
> На ветке `main` измерения `dim_routes`, `dim_passengers`, `dim_airplanes`
> заглушки. Эталон: `dim_calendar`, `dim_airports`, `dim_tariffs`, `fact_flight_sales`.
- Для каждой таблицы выполняет пару задач `load → dq`.
- Использует `_load_id = {{ run_id }}`. DDS не требует `stg_batch_id`, потому что читает
текущее состояние ODS.
@@ -68,12 +71,15 @@ load_dds_dim_calendar → dq_dds_dim_calendar
| # | Задача | SQL-файлы | Что загружает |
|---|--------|-----------|---------------|
| 2 | `load_dds_dim_airports``dq_dds_dim_airports` | `sql/dds/dim_airports_load.sql`, `sql/dds/dim_airports_dq.sql` | Аэропорты (код, город, координаты) |
| 3 | `load_dds_dim_airplanes``dq_dds_dim_airplanes` | `sql/dds/dim_airplanes_load.sql`, `sql/dds/dim_airplanes_dq.sql` | Самолёты (код, модель, кол-во мест) |
| 3 | `load_dds_dim_airplanes``dq_dds_dim_airplanes` | `sql/dds/dim_airplanes_load.sql`, `sql/dds/dim_airplanes_dq.sql` | Самолёты (код, модель, кол-во мест) ⚠️ заглушка на main |
| 4 | `load_dds_dim_tariffs``dq_dds_dim_tariffs` | `sql/dds/dim_tariffs_load.sql`, `sql/dds/dim_tariffs_dq.sql` | Тарифы (класс обслуживания) |
| 5 | `load_dds_dim_passengers``dq_dds_dim_passengers` | `sql/dds/dim_passengers_load.sql`, `sql/dds/dim_passengers_dq.sql` | Пассажиры (ID, имя, контакты) |
| 5 | `load_dds_dim_passengers``dq_dds_dim_passengers` | `sql/dds/dim_passengers_load.sql`, `sql/dds/dim_passengers_dq.sql` | Пассажиры (ID, имя, контакты) ⚠️ заглушка на main |
Паттерн загрузки — SCD1 UPSERT: TEMP TABLE → UPDATE (IS DISTINCT FROM) → INSERT.
> **Задание.** На ветке `main` этот скрипт — заглушка (`SELECT 1;`).
> Описание ниже — спецификация того, что нужно реализовать. Образец SCD2 — в ветке `solution`.
### 6) `load_dds_dim_routes``dq_dds_dim_routes` (SCD2)
- **SQL:** `sql/dds/dim_routes_load.sql`, `sql/dds/dim_routes_dq.sql`
@@ -156,7 +162,8 @@ make gp-psql
```sql
SELECT COUNT(*) FROM dds.dim_calendar;
SELECT COUNT(*) FROM dds.dim_routes;
SELECT COUNT(*) FROM dds.dim_airports;
SELECT COUNT(*) FROM dds.dim_tariffs;
SELECT COUNT(*) FROM dds.fact_flight_sales;
-- Проверка: кол-во строк факта ≈ кол-во строк ODS segments
@@ -164,14 +171,20 @@ SELECT
(SELECT COUNT(*) FROM dds.fact_flight_sales) AS fact_rows,
(SELECT COUNT(*) FROM ods.segments) AS ods_rows;
-- Проверка SCD2: текущие версии маршрутов (valid_to IS NULL)
-- dim_routes, dim_passengers, dim_airplanes — заглушки на main.
-- Проверки ниже станут осмысленны после реализации задания.
SELECT COUNT(*) FROM dds.dim_routes;
-- Проверка SCD2 (после реализации dim_routes):
-- текущие версии маршрутов (valid_to IS NULL)
SELECT COUNT(*) AS current_versions,
(SELECT COUNT(*) FROM dds.dim_routes) AS total_versions
FROM dds.dim_routes
WHERE valid_to IS NULL;
```
Ожидаемо: `fact_rows = ods_rows`, `current_versions ≤ total_versions`.
Ожидаемо: `fact_rows ods_rows`, `dim_calendar` и `dim_airports` непусты.
`dim_routes` — после реализации задания: `current_versions ≤ total_versions`.
## Типичные ошибки
+25 -15
View File
@@ -1,6 +1,7 @@
# DAG `bookings_to_gp_dm`: `dds``dm` в Greenplum
Этот DAG — учебный пример загрузки слоя **DM** (Data Mart / витрины) из текущего состояния **DDS**.
Эталон: витрина `sales_report`. Остальные 4 витрины — задания (заглушки `SELECT 1;` на ветке `main`).
Все 5 витрин загружаются **параллельно** и демонстрируют разные стратегии загрузки —
это ключевая учебная ценность данного DAG.
@@ -8,13 +9,13 @@
Загружает 5 витрин параллельно, для каждой — пара `load → dq`:
| Витрина | Зерно (grain) | Паттерн загрузки |
|---------|---------------|------------------|
| `dm.sales_report` | (flight_date, departure_airport_sk, arrival_airport_sk, tariff_sk) | Инкрементальный UPSERT (HWM по датам) |
| `dm.route_performance` | route_bk | Full Rebuild (TRUNCATE + INSERT) |
| `dm.passenger_loyalty` | passenger_sk | Инкрементальный UPSERT (HWM по затронутым ключам) |
| `dm.airport_traffic` | (traffic_date, airport_sk) | Инкрементальный UPSERT (HWM по датам) |
| `dm.monthly_overview` | (year_actual, month_actual, airplane_sk) | Инкрементальный UPSERT (HWM по месяцам) |
| Витрина | Зерно (grain) | Паттерн загрузки | Статус на main |
|---------|---------------|------------------|----------------|
| `dm.sales_report` | (flight_date, departure_airport_sk, arrival_airport_sk, tariff_sk) | Инкрементальный UPSERT (HWM по датам) | Эталон |
| `dm.route_performance` | route_bk | Full Rebuild (TRUNCATE + INSERT) | Задание (заглушка) |
| `dm.passenger_loyalty` | passenger_sk | Инкрементальный UPSERT (HWM по затронутым ключам) | Задание (заглушка) |
| `dm.airport_traffic` | (traffic_date, airport_sk) | Инкрементальный UPSERT (HWM по датам) | Задание (заглушка) |
| `dm.monthly_overview` | (year_actual, month_actual, airplane_sk) | Инкрементальный UPSERT (HWM по месяцам) | Задание (заглушка) |
## Что должно быть готово перед запуском
@@ -68,6 +69,9 @@ start_dm
- **NULLIF** для защиты от деления на ноль (`boarding_rate = boarded / NULLIF(sold, 0)`);
- **Денормализация**: города и коды аэропортов тянутся в витрину из измерений.
> **Задание.** На ветке `main` этот скрипт — заглушка (`SELECT 1;`).
> Реализуйте по описанию ниже и ТЗ в `analyst_spec.md`.
### 2) `load_dm_route_performance``dq_dm_route_performance`
- **SQL:** `sql/dm/route_performance_load.sql`, `sql/dm/route_performance_dq.sql`
@@ -89,6 +93,9 @@ start_dm
Метрики: `avg_load_factor = total_boarded / (total_flights * total_seats)`,
`avg_ticket_price = total_revenue / total_tickets`.
> **Задание.** На ветке `main` этот скрипт — заглушка (`SELECT 1;`).
> Реализуйте по описанию ниже и ТЗ в `analyst_spec.md`.
### 3) `load_dm_passenger_loyalty``dq_dm_passenger_loyalty`
- **SQL:** `sql/dm/passenger_loyalty_load.sql`, `sql/dm/passenger_loyalty_dq.sql`
@@ -105,6 +112,9 @@ start_dm
- Фильтрация `passenger_sk IS NOT NULL` — защита от неконсистентных фактов (NULL SK
при data quality аномалиях в измерениях).
> **Задание.** На ветке `main` этот скрипт — заглушка (`SELECT 1;`).
> Реализуйте по описанию ниже и ТЗ в `analyst_spec.md`.
### 4) `load_dm_airport_traffic``dq_dm_airport_traffic`
- **SQL:** `sql/dm/airport_traffic_load.sql`, `sql/dm/airport_traffic_dq.sql`
@@ -116,6 +126,9 @@ start_dm
в два «события» (вылет из одного аэропорта и прилёт в другой).
Это позволяет собрать единую статистику аэропорта (departures + arrivals) в одном проходе.
> **Задание.** На ветке `main` этот скрипт — заглушка (`SELECT 1;`).
> Реализуйте по описанию ниже и ТЗ в `analyst_spec.md`.
### 5) `load_dm_monthly_overview``dq_dm_monthly_overview`
- **SQL:** `sql/dm/monthly_overview_load.sql`, `sql/dm/monthly_overview_dq.sql`
@@ -145,19 +158,16 @@ make gp-psql
```sql
SELECT COUNT(*) FROM dm.sales_report;
SELECT COUNT(*) FROM dm.route_performance;
SELECT COUNT(*) FROM dm.passenger_loyalty;
SELECT COUNT(*) FROM dm.airport_traffic;
SELECT COUNT(*) FROM dm.monthly_overview;
SELECT COUNT(*) FROM dm.route_performance; -- будет непусто после реализации задания
SELECT COUNT(*) FROM dm.passenger_loyalty; -- будет непусто после реализации задания
SELECT COUNT(*) FROM dm.airport_traffic; -- будет непусто после реализации задания
SELECT COUNT(*) FROM dm.monthly_overview; -- будет непусто после реализации задания
-- Инвариант sales_report: посаженных не больше, чем продано
SELECT COUNT(*) FROM dm.sales_report WHERE tickets_sold < passengers_boarded;
-- Нет дублей по бизнес-ключу route_performance
SELECT route_bk, COUNT(*) FROM dm.route_performance GROUP BY route_bk HAVING COUNT(*) > 1;
```
Ожидаемо: все витрины непусты, инварианты соблюдены, дублей нет.
Ожидаемо: `dm.sales_report` непуста. Остальные — после реализации заданий.
## Типичные ошибки
+7 -3
View File
@@ -9,7 +9,8 @@
- Определяет `stg_batch_id` — последний согласованный батч, по которому все 4 snapshot-справочника
(`airports`, `airplanes`, `routes`, `seats`) уже приехали в STG.
- Загружает 9 таблиц ODS: `bookings`, `tickets`, `airports`, `airplanes`, `routes`, `seats`,
`flights`, `segments`, `boarding_passes`.
`flights`, `segments`, `boarding_passes` (на ветке `main` загрузка `airplanes` и `seats`
заглушки; реализуйте их по ТЗ в `docs/assignment/analyst_spec.md`).
- Для каждой таблицы выполняет пару задач `load → dq`.
- Snapshot-справочники фильтруются по `stg_batch_id`, транзакционные таблицы — по HWM (`_load_ts`).
- Для snapshot-справочников дополнительно синхронизирует ключи (удаляет из ODS записи,
@@ -97,9 +98,9 @@ resolve_stg_batch_id
| 2 | `load_ods_bookings``dq_ods_bookings` | `sql/ods/bookings_load.sql`, `sql/ods/bookings_dq.sql` | HWM (инкремент) |
| 3 | `load_ods_tickets``dq_ods_tickets` | `sql/ods/tickets_load.sql`, `sql/ods/tickets_dq.sql` | HWM (инкремент) |
| 4 | `load_ods_airports``dq_ods_airports` | `sql/ods/airports_load.sql`, `sql/ods/airports_dq.sql` | snapshot по `stg_batch_id` |
| 5 | `load_ods_airplanes``dq_ods_airplanes` | `sql/ods/airplanes_load.sql`, `sql/ods/airplanes_dq.sql` | snapshot по `stg_batch_id` |
| 5 | `load_ods_airplanes``dq_ods_airplanes` | `sql/ods/airplanes_load.sql`, `sql/ods/airplanes_dq.sql` | snapshot по `stg_batch_id` ⚠️ заглушка на main |
| 6 | `load_ods_routes``dq_ods_routes` | `sql/ods/routes_load.sql`, `sql/ods/routes_dq.sql` | snapshot по `stg_batch_id` |
| 7 | `load_ods_seats``dq_ods_seats` | `sql/ods/seats_load.sql`, `sql/ods/seats_dq.sql` | snapshot по `stg_batch_id` |
| 7 | `load_ods_seats``dq_ods_seats` | `sql/ods/seats_load.sql`, `sql/ods/seats_dq.sql` | snapshot по `stg_batch_id` ⚠️ заглушка на main |
| 8 | `load_ods_flights``dq_ods_flights` | `sql/ods/flights_load.sql`, `sql/ods/flights_dq.sql` | HWM (инкремент) |
| 9 | `load_ods_segments``dq_ods_segments` | `sql/ods/segments_load.sql`, `sql/ods/segments_dq.sql` | HWM (инкремент) |
| 10 | `load_ods_boarding_passes``dq_ods_boarding_passes` | `sql/ods/boarding_passes_load.sql`, `sql/ods/boarding_passes_dq.sql` | HWM (инкремент) |
@@ -118,6 +119,9 @@ resolve_stg_batch_id
> который не поддерживает эффективный row-level UPDATE (вызывает bloat).
> Для маленьких справочников (~100–300 строк) полная перезагрузка быстрее и чище.
> На ветке `main` загрузка `airplanes` и `seats` — заглушки.
> Паттерн TRUNCATE + INSERT описан выше; используйте `airports_load.sql` как образец.
**Транзакционные таблицы** (`bookings`, `tickets`, `flights`, `segments`, `boarding_passes`) —
**SCD1 UPSERT**: