docs(etl): расширена документация ETL DAG-ов ODS, DDS, DM
- Зачем:
- документация трёх DAG-ов была значительно беднее эталона (bookings_to_gp_stage.md):
отсутствовал пошаговый разбор задач, SQL-пути и ASCII-графы зависимостей.
- Что:
- добавлены секции "Как это работает внутри" с таблицами task_id → SQL-файл → паттерн.
- добавлены ASCII-графы зависимостей в ODS и DDS (в DM уже был).
- исправлено описание паттернов ODS: явно разделены TRUNCATE+INSERT для AO snapshot-справочников и SCD1 UPSERT для транзакционных таблиц.
- исправлено описание fact_flight_sales: убрана неточная отсылка к late-arriving dimensions, добавлено объяснение defensive LEFT JOIN и точных DQ-правил (0% / 1%).
- добавлена таблица grain и описание стратегий загрузки для каждой DM-витрины.
- план улучшений перенесён в docs/archive/.
- Проверка:
- визуально: открыть каждый docs/bookings_to_gp_*.md и убедиться, что секции присутствуют.
This commit is contained in:
+128
-20
@@ -1,17 +1,19 @@
|
||||
# DAG `bookings_to_gp_dds`: `ods` -> `dds` в Greenplum
|
||||
# DAG `bookings_to_gp_dds`: `ods` → `dds` в Greenplum
|
||||
|
||||
Этот DAG — учебный пример загрузки аналитического слоя **DDS** (Star Schema) из текущего состояния **ODS**.
|
||||
Логика: измерения + факт, проверки качества данных после каждой загрузки.
|
||||
Здесь сосредоточены ключевые паттерны аналитического хранилища: SCD1, SCD2 с hashdiff,
|
||||
point-in-time join, защитные LEFT JOIN для устойчивости к data quality аномалиям.
|
||||
|
||||
## Что делает DAG
|
||||
|
||||
- Загружает измерения DDS:
|
||||
- `dds.dim_calendar` (статическое измерение дат);
|
||||
- `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)`.
|
||||
- Для каждой таблицы выполняет пару задач `load -> dq`.
|
||||
- Использует `_load_id = {{ run_id }}` (DDS не требует `stg_batch_id`, потому что читает current state ODS).
|
||||
- Загружает 6 измерений DDS:
|
||||
- `dds.dim_calendar` — статическое измерение дат (Full Rebuild);
|
||||
- `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)`.
|
||||
- Для каждой таблицы выполняет пару задач `load → dq`.
|
||||
- Использует `_load_id = {{ run_id }}`. DDS не требует `stg_batch_id`, потому что читает
|
||||
текущее состояние ODS.
|
||||
|
||||
## Что должно быть готово перед запуском
|
||||
|
||||
@@ -32,17 +34,116 @@ make up
|
||||
2) Если запускаете DDS впервые — выполните `bookings_dds_ddl`.
|
||||
3) Запустите `bookings_to_gp_dds`.
|
||||
|
||||
## Граф зависимостей (упрощённо)
|
||||
## Граф зависимостей
|
||||
|
||||
- `load_dds_dim_calendar -> dq_dds_dim_calendar`
|
||||
- После calendar параллельно:
|
||||
- `load_dds_dim_airports -> dq_dds_dim_airports`
|
||||
- `load_dds_dim_airplanes -> dq_dds_dim_airplanes`
|
||||
- `load_dds_dim_tariffs -> dq_dds_dim_tariffs`
|
||||
- `load_dds_dim_passengers -> dq_dds_dim_passengers`
|
||||
- `load_dds_dim_routes -> dq_dds_dim_routes`
|
||||
- Факт:
|
||||
- `load_dds_fact_flight_sales -> dq_dds_fact_flight_sales -> finish_dds_summary`
|
||||
```
|
||||
load_dds_dim_calendar → dq_dds_dim_calendar
|
||||
├─ load_dds_dim_airports → dq_dds_dim_airports ─┐
|
||||
│ ├─ load_dds_dim_routes
|
||||
├─ load_dds_dim_airplanes → dq_dds_dim_airplanes ─┘ └─ dq_dds_dim_routes
|
||||
│ │
|
||||
├─ load_dds_dim_tariffs → dq_dds_dim_tariffs │
|
||||
│ │
|
||||
└─ load_dds_dim_passengers → dq_dds_dim_passengers │
|
||||
│
|
||||
все 5 dq_dds_dim_* ─────────────────────────────────────────┘
|
||||
└─ load_dds_fact_flight_sales
|
||||
└─ dq_dds_fact_flight_sales
|
||||
└─ finish_dds_summary
|
||||
```
|
||||
|
||||
Ключевой момент: `dim_routes` зависит от `dim_airports` и `dim_airplanes` (денормализация),
|
||||
а факт ждёт завершения **всех** пяти измерений.
|
||||
|
||||
## Как это работает внутри (по шагам)
|
||||
|
||||
### 1) `load_dds_dim_calendar` → `dq_dds_dim_calendar`
|
||||
|
||||
- **SQL:** `sql/dds/dim_calendar_load.sql`, `sql/dds/dim_calendar_dq.sql`
|
||||
- **Паттерн:** Full Rebuild — каждый запуск пересоздаёт календарь целиком.
|
||||
Измерение маленькое и детерминированное, дельту считать нет смысла.
|
||||
|
||||
### 2–5) SCD1-измерения (параллельно после 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` | Самолёты (код, модель, кол-во мест) |
|
||||
| 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, имя, контакты) |
|
||||
|
||||
Паттерн загрузки — SCD1 UPSERT: TEMP TABLE → UPDATE (IS DISTINCT FROM) → INSERT.
|
||||
|
||||
### 6) `load_dds_dim_routes` → `dq_dds_dim_routes` (SCD2)
|
||||
|
||||
- **SQL:** `sql/dds/dim_routes_load.sql`, `sql/dds/dim_routes_dq.sql`
|
||||
- **Паттерн:** SCD2 — самый нетривиальный паттерн в проекте. Работает в 3 фазы:
|
||||
|
||||
**Фаза 1. Hashdiff и закрытие старых версий.**
|
||||
Скрипт считает MD5-хеш от шести бизнес-атрибутов маршрута (`departure_airport`, `arrival_airport`,
|
||||
`airplane_code`, `days_of_week`, `departure_time`, `duration`).
|
||||
Если хеш текущей версии в DDS не совпадает с хешем из ODS — старая версия закрывается
|
||||
(`valid_to = CURRENT_DATE`). Также закрываются маршруты, исчезнувшие из ODS.
|
||||
|
||||
**Фаза 2. Вставка новых версий.**
|
||||
Для изменённых и совершенно новых маршрутов создаётся новая строка.
|
||||
`valid_from` выставляется в `1900-01-01` для первой версии маршрута и `CURRENT_DATE` для версии 2+.
|
||||
Суррогатный ключ (`route_sk`) генерируется через `MAX(route_sk) + ROW_NUMBER()`.
|
||||
|
||||
> **Важно:** такая генерация SK безопасна только при `max_active_runs=1` (Airflow гарантирует
|
||||
> последовательный запуск). В боевых системах используют sequence.
|
||||
|
||||
**Фаза 3. Обновление денормализованных атрибутов.**
|
||||
`dim_routes` хранит денормализованные SCD1-атрибуты из `dim_airports` (города)
|
||||
и `dim_airplanes` (модель, кол-во мест). Если, например, город переименовали —
|
||||
фаза 3 обновляет **все** версии маршрута (и текущие, и исторические),
|
||||
при этом `_load_id` и `_load_ts` не перезаписываются (lineage версий сохраняется).
|
||||
|
||||
### 7) `load_dds_fact_flight_sales` → `dq_dds_fact_flight_sales`
|
||||
|
||||
- **SQL:** `sql/dds/fact_flight_sales_load.sql`, `sql/dds/fact_flight_sales_dq.sql`
|
||||
- **Зерно:** `(ticket_no, flight_id)` — один билет на один рейс.
|
||||
- **Паттерн:** инкрементальный UPSERT.
|
||||
|
||||
Три учебных приёма в этом скрипте:
|
||||
|
||||
**Защитные LEFT JOIN (defensive coding).**
|
||||
Все JOIN-ы с измерениями — `LEFT JOIN`. DAG гарантирует, что все измерения загружены
|
||||
и прошли DQ **до** старта факта (жёсткие зависимости в графе). Поэтому в штатном режиме
|
||||
NULL SK не возникают. LEFT JOIN здесь — защита от data quality аномалий (например, если
|
||||
в `ods.routes` появится маршрут с несуществующим аэропортом).
|
||||
|
||||
DQ-проверки факта отражают эту логику:
|
||||
- `passenger_sk` и `tariff_sk` — **запрещены** NULL целиком (0 строк);
|
||||
- route-related FK (`route_sk`, `airport_sk`, `airplane_sk`) и `calendar_sk` —
|
||||
допускается до **1%** NULL (NOTICE-предупреждение), при превышении — EXCEPTION.
|
||||
|
||||
> Это **не** паттерн late-arriving dimensions (опаздывающих измерений) в классическом
|
||||
> понимании: backfill NULL SK при повторном запуске не реализован.
|
||||
> В боевых системах для этого используют «строку-заглушку» (unknown member, SK = 0)
|
||||
> и отдельный процесс backfill.
|
||||
|
||||
**Point-in-time join для SCD2.**
|
||||
Для `dim_routes` используется привязка по дате рейса:
|
||||
|
||||
```sql
|
||||
LEFT JOIN dds.dim_routes AS rte
|
||||
ON rte.route_bk = flt.route_no
|
||||
AND flt.scheduled_departure::DATE >= rte.valid_from
|
||||
AND (rte.valid_to IS NULL OR flt.scheduled_departure::DATE < rte.valid_to)
|
||||
```
|
||||
|
||||
Это гарантирует, что факт привязывается к той версии маршрута, которая была актуальна
|
||||
на дату рейса.
|
||||
|
||||
**UPDATE мутабельных полей.**
|
||||
UPDATE обновляет только `seat_no`, `price`, `is_boarded` (данные, которые реально
|
||||
могут измениться — посадка пассажира, корректировка цены). SK измерений не перезаписываются —
|
||||
они зафиксированы на момент вставки.
|
||||
|
||||
### 8) `finish_dds_summary`
|
||||
|
||||
Ждёт завершения DQ факта и логирует сводку.
|
||||
|
||||
## Как проверить результат
|
||||
|
||||
@@ -55,12 +156,19 @@ SELECT COUNT(*) FROM dds.dim_calendar;
|
||||
SELECT COUNT(*) FROM dds.dim_routes;
|
||||
SELECT COUNT(*) FROM dds.fact_flight_sales;
|
||||
|
||||
-- Проверка: кол-во строк факта ≈ кол-во строк ODS segments
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM dds.fact_flight_sales) AS fact_rows,
|
||||
(SELECT COUNT(*) FROM ods.segments) AS ods_rows;
|
||||
|
||||
-- Проверка SCD2: текущие версии маршрутов (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`.
|
||||
Ожидаемо: `fact_rows = ods_rows`, `current_versions ≤ total_versions`.
|
||||
|
||||
## Типичные ошибки
|
||||
|
||||
|
||||
Reference in New Issue
Block a user