docs: согласованы руководства загрузок и учебный маршрут
Руководства STG/ODS/DDS/DM сверены с действующими SQL и DAG, ожидания учебных заглушек связаны с ТЗ. Самопроверка заданий отделена от проверок разработки и исторических планов. Для dim_routes закреплено обновление описательных полей только у открытых версий. SQL, DAG, DDL и настройки стенда не менялись. Проверки: make lint; make test (4 passed, 14 skipped); карта; 292 локальные ссылки; маршрут чтения и карта в Chromium. Живой ETL не запускался.
This commit is contained in:
1 parent
a6fdd43e15
commit
35144e49f6
17 files changed
+699
-755
No files matched your search
+86
-154
@@ -1,181 +1,113 @@
|
||||
# DAG `bookings_to_gp_dm`: `dds` → `dm` в Greenplum
|
||||
# Загрузка DDS → DM: `bookings_to_gp_dm`
|
||||
|
||||
Этот DAG — учебный пример загрузки слоя **DM** (Data Mart / витрины) из текущего состояния **DDS**.
|
||||
Эталон: витрина `sales_report`. Остальные 4 витрины — задания (заглушки `SELECT 1;` на ветке `main`).
|
||||
Все 5 витрин загружаются **параллельно** и демонстрируют разные стратегии загрузки —
|
||||
это ключевая учебная ценность данного DAG.
|
||||
DAG собирает витрины из факта и измерений DDS. На `main` полностью работает
|
||||
`dm.sales_report`; остальные четыре загрузки и их DQ содержат `SELECT 1;`.
|
||||
Успешный запуск с заглушками не означает, что все витрины наполнены.
|
||||
|
||||
## Что делает DAG
|
||||
На [карте sales_report](design/architecture-map.html#node=dm.sales_report)
|
||||
можно проследить источники полей. Как сегменты билетов превращаются
|
||||
в строки витрины, объясняет [руководство о структуре](design/db_schema.md#агрегация-фактов-в-витрине-продаж).
|
||||
Карту открывайте [локально](design/db_schema.md#как-открыть-карту).
|
||||
|
||||
Загружает 5 витрин параллельно, для каждой — пара `load → dq`:
|
||||
## Перед запуском
|
||||
|
||||
| Витрина | Зерно (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 по месяцам) | Задание (заглушка) |
|
||||
Дождитесь успешного `bookings_to_gp_dds`. Создайте таблицы через
|
||||
`bookings_dm_ddl`, если еще не запускали его или общий `make ddl-gp`.
|
||||
Затем запустите `bookings_to_gp_dm` в Airflow UI.
|
||||
Общий порядок находится в [инструкции запуска DAG](dag_execution_order.md).
|
||||
|
||||
## Что должно быть готово перед запуском
|
||||
Для готовой `sales_report` достаточно готового факта, календаря, аэропортов
|
||||
и тарифов. Для своих витрин сначала заполните нужные измерения и выполните
|
||||
[пересчет факта после заданий DDS](assignment/analyst_spec.md#пересчёт-факта-после-реализации-измерений).
|
||||
|
||||
1) Стенд поднят:
|
||||
## Что делает sales_report
|
||||
|
||||
```bash
|
||||
make up
|
||||
```
|
||||
Зерно витрины: `(flight_date, departure_airport_sk, arrival_airport_sk, tariff_sk)`.
|
||||
Одна строка содержит продажи сегментов за дату вылета, пару аэропортов и тариф.
|
||||
`tickets_sold = COUNT(*)` считает строки факта в этой группе.
|
||||
|
||||
2) STG, ODS и DDS уже загружены:
|
||||
[sales_report_load.sql](../sql/dm/sales_report_load.sql) работает в три шага:
|
||||
|
||||
- выполнены DAG-и `bookings_to_gp_stage`, `bookings_to_gp_ods`, `bookings_to_gp_dds`;
|
||||
- DDL-объекты созданы (`bookings_dm_ddl` или `make ddl-gp`).
|
||||
1. Находит даты вылета фактов с `_load_ts > MAX(_load_ts)` витрины.
|
||||
Для пустой витрины нижняя граница равна `1900-01-01`.
|
||||
2. Для этих дат пересчитывает агрегаты по всем их фактам во временную таблицу.
|
||||
В расчет входят и строки, загруженные ранее: например, новая посадка
|
||||
меняет итог дня, сохраняя остальные продажи этого дня.
|
||||
3. Обновляет существующие группы при изменении проверяемых полей и вставляет
|
||||
новые. `_load_id` записанных строк равен текущему `run_id` DM.
|
||||
|
||||
## Как запустить
|
||||
Если после пропуска запуска в DDS накопились факты с более новой `_load_ts`,
|
||||
следующий запуск включит их даты в пересчет. Изменение только описания
|
||||
измерения само по себе не выбирает дату: отбор привязан к метке факта.
|
||||
|
||||
1) Откройте Airflow UI: http://localhost:8080.
|
||||
2) Если запускаете DM впервые — выполните `bookings_dm_ddl`.
|
||||
3) Запустите `bookings_to_gp_dm`.
|
||||
[sales_report_dq.sql](../sql/dm/sales_report_dq.sql) берет даты из строк витрины,
|
||||
записанных текущим запуском. Для них проверяет непустоту при наличии фактов,
|
||||
дубли ключа, обязательные поля, неотрицательную выручку и долю посадок от 0 до 1.
|
||||
Если витрина в этом запуске не менялась, DQ выводит `NOTICE` и завершает работу.
|
||||
|
||||
## Граф зависимостей
|
||||
## Учебные витрины
|
||||
|
||||
```
|
||||
start_dm
|
||||
├── load_dm_sales_report → dq_dm_sales_report ─┐
|
||||
├── load_dm_route_performance → dq_dm_route_performance ─┤
|
||||
├── load_dm_passenger_loyalty → dq_dm_passenger_loyalty ─┼─ finish_dm_summary
|
||||
├── load_dm_airport_traffic → dq_dm_airport_traffic ─┤
|
||||
└── load_dm_monthly_overview → dq_dm_monthly_overview ─┘
|
||||
```
|
||||
Ниже указано ожидаемое поведение заданий. Поля и формулы находятся в ТЗ;
|
||||
загрузку и DQ для каждого объекта нужно написать самостоятельно.
|
||||
|
||||
Все 5 веток полностью независимы и работают параллельно.
|
||||
| Задание | Одна строка | Что нужно пересчитывать |
|
||||
|---|---|---|
|
||||
| [airport_traffic](assignment/analyst_spec.md#31-dmairport_traffic) | Дата и аэропорт | Полные итоги затронутых дат, с учетом ролей вылета и прилета |
|
||||
| [route_performance](assignment/analyst_spec.md#32-dmroute_performance) | Маршрут по `route_bk` | Всю историю при каждом запуске, объединяя версии маршрута |
|
||||
| [monthly_overview](assignment/analyst_spec.md#33-dmmonthly_overview) | Год, месяц и модель по `airplane_sk` | Затронутые месяцы; сначала метрики рейса, затем месяца |
|
||||
| [passenger_loyalty](assignment/analyst_spec.md#34-dmpassenger_loyalty) | Пассажир по `passenger_sk` | Всю историю пассажиров, чьи факты изменились |
|
||||
|
||||
## Как это работает внутри (по шагам)
|
||||
Готовый пример отбора затронутых дат находится в `sales_report_load.sql`.
|
||||
В заданиях меняется единица пересчета: дата, месяц, пассажир или вся таблица.
|
||||
Следуйте [порядку заданий](assignment/README.md#следующие-этапы), чтобы каждый
|
||||
следующий расчет опирался на уже освоенный прием.
|
||||
|
||||
### 1) `load_dm_sales_report` → `dq_dm_sales_report`
|
||||
## Порядок задач
|
||||
|
||||
- **SQL:** `sql/dm/sales_report_load.sql`, `sql/dm/sales_report_dq.sql`
|
||||
- **Паттерн:** инкрементальный UPSERT (HWM по датам).
|
||||
В [DAG](../airflow/dags/bookings_to_gp_dm.py) `start_dm` запускает пять
|
||||
независимых веток. Каждая имеет вид
|
||||
`load_dm_<витрина> → dq_dm_<витрина> → finish_dm_summary`.
|
||||
Финальная задача ждет все пять DQ. Витрины читают DDS и не читают друг друга,
|
||||
поэтому между их задачами загрузки нет зависимостей.
|
||||
|
||||
Витрина агрегирует продажи билетов по дате, аэропортам вылета/прилёта и тарифу.
|
||||
Инкрементальность работает через HWM: витрина сравнивает свой `MAX(_load_ts)` с `_load_ts`
|
||||
фактов в DDS и пересчитывает агрегаты только для **затронутых дат**.
|
||||
## Проверка результата
|
||||
|
||||
> Если витрина пуста — `1900-01-01` заберёт всю историю (первичная загрузка).
|
||||
> Если DAG не запускался несколько дней — при следующем запуске витрина автоматически
|
||||
> «догонит» всю накопленную дельту.
|
||||
|
||||
Учебные приёмы:
|
||||
- **TEMP TABLE** для однократной агрегации (канон для MPP);
|
||||
- **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`
|
||||
- **Паттерн:** Full Rebuild (TRUNCATE + INSERT).
|
||||
|
||||
Витрина агрегирует эффективность маршрутов за всю историю. Таблица маленькая (~1000 строк),
|
||||
поэтому пересоздать её с нуля дешевле, чем вычислять дельту. Дополнительная причина:
|
||||
таблица хранится в формате **AO Column Store**, который не поддерживает эффективный UPDATE/DELETE.
|
||||
|
||||
Трёхшаговый паттерн:
|
||||
1. **TRUNCATE** — очистка (единственный эффективный способ для AO).
|
||||
2. **Агрегация** по `route_bk` — факты суммируются через **все исторические версии** маршрута
|
||||
(route_sk из SCD2), чтобы не терять данные при версионировании.
|
||||
3. **JOIN** с текущей (актуальной, `valid_to IS NULL`) версией `dim_routes` для денормализации.
|
||||
|
||||
> Благодаря денормализации `dim_routes` — один JOIN вместо четырёх
|
||||
> (аэропорты вылета/прилёта, самолёт уже хранятся в `dim_routes`).
|
||||
|
||||
Метрики: `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`
|
||||
- **Паттерн:** инкрементальный UPSERT по «затронутым ключам».
|
||||
|
||||
В отличие от `sales_report` (где инкремент по датам), здесь HWM находит **конкретных пассажиров**
|
||||
с новыми фактами, а затем пересчитывает для них всю историю. Это гарантирует точность
|
||||
накопительных агрегатов (`total_spent`, `first/last_flight_date`).
|
||||
|
||||
Учебные приёмы:
|
||||
- **DISTINCT ON** (PostgreSQL-специфика) для нахождения моды (самый частый тариф пассажира);
|
||||
- **Агрегация SCD2 по BK**: при подсчёте уникальных маршрутов используем `route_bk`,
|
||||
а не `route_sk`, т.к. один маршрут может иметь несколько версий;
|
||||
- Фильтрация `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`
|
||||
- **Паттерн:** инкрементальный UPSERT по датам.
|
||||
|
||||
Витрина показывает пассажиропоток аэропортов по дням (вылеты + прилёты).
|
||||
|
||||
Учебный приём — **Dual-role dimension через UNION ALL**: один билет превращается
|
||||
в два «события» (вылет из одного аэропорта и прилёт в другой).
|
||||
Это позволяет собрать единую статистику аэропорта (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`
|
||||
- **Паттерн:** инкрементальный UPSERT по месяцам.
|
||||
|
||||
Витрина показывает помесячную статистику по типам самолётов.
|
||||
|
||||
Учебный приём — **двухуровневая агрегация**: чтобы честно посчитать `avg_load_factor`,
|
||||
сначала считаем load factor для каждого рейса (`boarded / total_seats`),
|
||||
затем берём среднее по месяцу. Прямая агрегация `SUM(boarded) / SUM(seats)` дала бы
|
||||
искажённый результат (взвешенный по числу билетов, а не рейсов).
|
||||
|
||||
> **Ограничение SCD1:** `total_seats` берётся из текущего состояния `dim_airplanes`.
|
||||
> Если самолёт переоборудовали в прошлом, для точного исторического расчёта
|
||||
> потребовалось бы SCD2-измерение.
|
||||
|
||||
### 6) `start_dm` / `finish_dm_summary`
|
||||
|
||||
- `start_dm` — стартовый sentinel, от которого расходятся все 5 параллельных веток.
|
||||
- `finish_dm_summary` — ждёт завершения всех DQ-задач и логирует сводку.
|
||||
|
||||
## Как проверить результат
|
||||
|
||||
```bash
|
||||
make gp-psql
|
||||
```
|
||||
В `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; -- будет непусто после реализации задания
|
||||
|
||||
-- Инвариант sales_report: посаженных не больше, чем продано
|
||||
SELECT COUNT(*) FROM dm.sales_report WHERE tickets_sold < passengers_boarded;
|
||||
SELECT flight_date, departure_airport_bk, arrival_airport_bk,
|
||||
fare_conditions, tickets_sold, passengers_boarded, total_revenue
|
||||
FROM dm.sales_report
|
||||
ORDER BY flight_date DESC, departure_airport_bk, arrival_airport_bk,
|
||||
fare_conditions
|
||||
LIMIT 10;
|
||||
|
||||
SELECT COUNT(*) AS invalid_boarding_rows
|
||||
FROM dm.sales_report
|
||||
WHERE tickets_sold < passengers_boarded;
|
||||
```
|
||||
|
||||
Ожидаемо: `dm.sales_report` непуста. Остальные — после реализации заданий.
|
||||
После первого успешного запуска `sales_report` непуста,
|
||||
`invalid_boarding_rows = 0`. Возьмите одну строку и найдите на карте,
|
||||
откуда пришли ее дата, два аэропорта и тариф.
|
||||
|
||||
## Типичные ошибки
|
||||
После реализации своих витрин запустите `bookings_validate` и посмотрите
|
||||
`validate_dm`. Проверки этой группы требуют непустые таблицы;
|
||||
правильность метрик должна проверять ваша DQ по ТЗ.
|
||||
Соблюдайте [правила запуска валидации](assignment/README.md#запуск-и-проверка):
|
||||
в том же DAG выполняется активный SCD2-тест с восстановлением данных.
|
||||
|
||||
- `relation "dm..." does not exist`:
|
||||
- не применён DM DDL (`bookings_dm_ddl` или `make ddl-gp`).
|
||||
- DQ падает на `sales_report` по `boarding_rate`:
|
||||
- проверьте, что DDS загрузился корректно (`dds.fact_flight_sales` непуста).
|
||||
- DQ падает на `passenger_loyalty` с ошибкой FK:
|
||||
- проверьте, что `dds.dim_passengers` содержит всех пассажиров из факта.
|
||||
- `finish_dm_summary` не выполняется:
|
||||
- одна из DQ-задач упала; найдите в логах Airflow задачу с ошибкой и исправьте.
|
||||
## Если загрузка не прошла
|
||||
|
||||
- Нет `dm.*`: выполните `bookings_dm_ddl`.
|
||||
- `sales_report` пуста: проверьте наличие факта и соответствий его SK
|
||||
календарю, аэропортам и тарифам. Витрина соединяет их через `INNER JOIN`.
|
||||
- Учебная витрина пуста: проверьте замену загрузочной заглушки и наличие
|
||||
необходимых SK факта после заданий DDS.
|
||||
- Ошибка метрики: разберите одну группу и сравните результат со строками факта
|
||||
и формулой в ТЗ. Дополнительные примеры проверок есть в
|
||||
[справочнике DQ](reference/dq_taxonomy.md).
|
||||
- `finish_dm_summary` не выполняется: найдите первую упавшую задачу,
|
||||
откройте ее Log и исправьте причину до следующего запуска слоя.
|
||||
Reference in new issue
Block a user