feat(dds): перенесён lookup аэропортов на ods.routes, обновлена документация задания

- Зачем:
  - fact_flight_sales брал airport_sk через dds.dim_routes (студенческое задание SCD2);
    на ветке main без реализованного dim_routes все airport_sk были NULL,
    и эталонные витрины sales_report/airport_traffic становились бесполезны.
- Что:
  - fact_flight_sales_load.sql: добавлен subquery ods_rte (ROW_NUMBER по ods.routes),
    dep/arr airport_sk теперь через ods.routes (эталон); airplane_sk — через dim_routes (point-in-time).
  - analyst_spec.md: удалены STG-задания (1.1–1.3) и ods.routes (2.3), добавлены секции
    «Почему STG уже реализован» и «Пересчёт факта», исправлен ODS intro, перенумерованы части.
  - assignment_design.md: обновлены таблицы эталон/задание, порядок выполнения; убраны
    validate_stg и check_ods_routes_rowcount из структуры валидационного DAG.
  - db_schema.md: статус уточнён («реализовано в ветке solution»), добавлена оговорка
    о заглушках на main, DQ-контракт дополнен примечанием.
- Проверка:
  - make test → 4 passed, 9 skipped.
  - ручная проверка: TRUNCATE dds.fact_flight_sales → перезапуск DDS DAG →
    COUNT(departure_airport_sk) должен равняться COUNT(*).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 10:39:19 +03:00
co-authored by Claude Sonnet 4.6
parent f018f1e7da
commit 4e9d8f5a1c
4 changed files with 109 additions and 162 deletions
+73 -140
View File
@@ -5,8 +5,9 @@
> опираясь на эталонный срез (витрина `dm.sales_report` и вся её цепочка). > опираясь на эталонный срез (витрина `dm.sales_report` и вся её цепочка).
> >
> **Эталон для изучения:** > **Эталон для изучения:**
> - STG: `bookings`, `tickets`, `flights`, `segments`, `boarding_passes`, `airports` > - STG: весь слой — `bookings`, `tickets`, `flights`, `segments`, `boarding_passes`,
> - ODS: те же таблицы > `airports`, `airplanes`, `seats`, `routes` (реализован как образец)
> - ODS: `bookings`, `tickets`, `segments`, `flights`, `boarding_passes`, `airports`, `routes`
> - DDS: `dim_airports`, `dim_tariffs`, `dim_calendar`, `fact_flight_sales` > - DDS: `dim_airports`, `dim_tariffs`, `dim_calendar`, `fact_flight_sales`
> - DM: `sales_report` > - DM: `sales_report`
> >
@@ -16,18 +17,20 @@
## Рекомендуемый порядок выполнения ## Рекомендуемый порядок выполнения
1. **STG**`airplanes`, `seats`, `routes` (разминка по аналогии) > **Примечание:** STG-слой полностью реализован в эталоне (все 9 таблиц), `ods.routes`
2. **ODS**`airplanes`, `seats`, `routes` (закрепление UPSERT / TRUNCATE+INSERT) > тоже эталонный. Ваше задание начинается с ODS: `airplanes` и `seats`.
3. **DDS**`dim_airplanes`, `dim_passengers` (SCD1 — новые измерения)
4. **DDS**`dim_routes` (SCD2 — ключевой вызов курсовой) 1. **ODS**`airplanes`, `seats` (практика TRUNCATE+INSERT по аналогии с `ods.airports`)
5. **DM**`airport_traffic` (простая витрина, похожа на `sales_report`) 2. **DDS**`dim_airplanes`, `dim_passengers` (SCD1 — новые измерения)
6. **DM**`route_performance` (Full Rebuild, работа с SCD2-измерением) 3. **DDS**`dim_routes` (SCD2 — ключевой вызов курсовой)
7. **DM**`monthly_overview` (двухуровневая агрегация) 4. **DM**`airport_traffic` (простая витрина, похожа на `sales_report`)
8. **DM**`passenger_loyalty` (самая сложная, пересчёт истории) 5. **DM**`route_performance` (Full Rebuild, работа с SCD2-измерением)
6. **DM**`monthly_overview` (двухуровневая агрегация)
7. **DM**`passenger_loyalty` (самая сложная, пересчёт истории)
Порядок выстроен от простого к сложному. Каждый шаг опирается на опыт Порядок выстроен от простого к сложному. Каждый шаг опирается на опыт
предыдущего. Вы можете двигаться в ином порядке, но убедитесь, что зависимости предыдущего. Вы можете двигаться в ином порядке, но убедитесь, что зависимости
слоёв соблюдены (STG → ODS → DDS → DM). слоёв соблюдены (ODS → DDS → DM).
--- ---
@@ -46,106 +49,40 @@
--- ---
## Часть 1. STG-слой (Staging) ## Почему STG-слой уже реализован
> **Цель:** Скопировать данные из источника (bookings-db) в Greenplum «как есть», STG-слой (все 9 таблиц) и `ods.routes` полностью реализованы в эталоне. Причина:
> сохраняя все поля в текстовом виде. Преобразование типов — задача ODS.
>
> **Аналог для изучения:** `sql/stg/airports_ddl.sql`, `sql/stg/airports_load.sql`
### 1.1. stg.airplanes Эталонный пайплайн использует **batch resolver** — механизм, который находит
согласованный набор данных во всех четырёх snapshot-таблицах (`stg.airports`,
`stg.airplanes`, `stg.routes`, `stg.seats`) по одному `_load_id`. Если любая
из этих таблиц пуста — batch resolver не найдёт общего батча, и весь ODS pipeline
не запустится.
**Описание:** Справочник моделей воздушных судов. Содержит технические Это означает: без заполненного STG невозможна загрузка ODS и всей последующей
характеристики каждой модели: дальность полёта и крейсерскую скорость. цепочки (DDS, DM). Поэтому весь STG реализован как эталон — чтобы пайплайн
работал с первого запуска.
**Источник:** `bookings.airplanes_data` (через PXF) `ods.routes` тоже эталонный: факт `fact_flight_sales` использует его для резолвинга
аэропортов вылета/прилёта, независимо от студенческого `dds.dim_routes`.
| Поле | Тип | Описание | Маппинг из источника | **Что вам делать:** Изучите эталонные скрипты как образец — именно так написан
|------|-----|----------|----------------------| «боевой» код загрузки:
| `airplane_code` | TEXT | Код модели самолёта (бизнес-ключ) | `airplanes_data.airplane_code::TEXT` | - `sql/stg/airports_load.sql` — инкрементальная загрузка (HWM)
| `model` | TEXT | Полное наименование модели (JSON в источнике) | `airplanes_data.model::TEXT` | - `sql/stg/airplanes_load.sql` — full snapshot с проверкой `_load_id`
| `range` | TEXT | Дальность полёта, км | `airplanes_data.range::TEXT` | - `sql/ods/airports_load.sql` — TRUNCATE+INSERT из STG
| `speed` | TEXT | Крейсерская скорость, км/ч | `airplanes_data.speed::TEXT` |
| `event_ts` | TIMESTAMP | Момент загрузки (`now()`). У snapshot-справочников нет бизнес-события с точным временем, поэтому `event_ts` заполняется при загрузке | `now()` |
| `_load_ts` | TIMESTAMP NOT NULL | Момент загрузки в STG | `now()` |
| `_load_id` | TEXT NOT NULL | Идентификатор батча загрузки | `'{{ run_id }}'` |
**Тип историзации:** Нет (накопительный snapshot). Каждый запуск добавляет
новый батч по `_load_id`; прошлые батчи остаются в таблице. Не используйте TRUNCATE.
**Стратегия загрузки:** Полный снимок (Full Snapshot). Справочник маленький —
загружаем целиком каждый раз. Идемпотентность — через проверку `_load_id`.
**Distribution Key:** `airplane_code`
--- ---
### 1.2. stg.seats ## Часть 1. ODS-слой (Operational Data Store)
**Описание:** Карта посадочных мест для каждой модели самолёта.
Каждая строка — одно конкретное место в конкретной модели.
**Источник:** `bookings.seats` (через PXF)
| Поле | Тип | Описание | Маппинг из источника |
|------|-----|----------|----------------------|
| `airplane_code` | TEXT | Код модели самолёта (FK → airplanes) | `seats.airplane_code::TEXT` |
| `seat_no` | TEXT | Номер места (напр. «1A», «12C») | `seats.seat_no::TEXT` |
| `fare_conditions` | TEXT | Класс обслуживания (`Economy`, `Business`, `Comfort`) | `seats.fare_conditions::TEXT` |
| `event_ts` | TIMESTAMP | Момент загрузки (`now()`). Snapshot-справочник — бизнес-событие отсутствует | `now()` |
| `_load_ts` | TIMESTAMP NOT NULL | Момент загрузки в STG | `now()` |
| `_load_id` | TEXT NOT NULL | Идентификатор батча загрузки | `'{{ run_id }}'` |
**Тип историзации:** Нет (накопительный snapshot). Каждый запуск добавляет
новый батч по `_load_id`; прошлые батчи остаются в таблице. Не используйте TRUNCATE.
**Стратегия загрузки:** Полный снимок. Идемпотентность — через
`airplane_code` + `seat_no` + `_load_id`.
**Distribution Key:** `airplane_code`
---
### 1.3. stg.routes
**Описание:** Справочник авиамаршрутов. Маршрут — регулярный рейс между двумя
аэропортами на определённом типе самолёта с фиксированным расписанием.
**Источник:** `bookings.routes` (через PXF)
| Поле | Тип | Описание | Маппинг из источника |
|------|-----|----------|----------------------|
| `route_no` | TEXT | Номер маршрута (бизнес-ключ, часть 1) | `routes.route_no::TEXT` |
| `validity` | TEXT | Период действия маршрута (бизнес-ключ, часть 2) | `routes.validity::TEXT` |
| `departure_airport` | TEXT | Код аэропорта вылета (FK → airports) | `routes.departure_airport::TEXT` |
| `arrival_airport` | TEXT | Код аэропорта прилёта (FK → airports) | `routes.arrival_airport::TEXT` |
| `airplane_code` | TEXT | Код модели самолёта (FK → airplanes) | `routes.airplane_code::TEXT` |
| `days_of_week` | TEXT | Дни недели выполнения рейса | `routes.days_of_week::TEXT` |
| `scheduled_time` | TEXT | Время вылета по расписанию | `routes.scheduled_time::TEXT` |
| `duration` | TEXT | Плановая продолжительность полёта | `routes.duration::TEXT` |
| `event_ts` | TIMESTAMP | Момент загрузки (`now()`). Snapshot-справочник — бизнес-событие отсутствует | `now()` |
| `_load_ts` | TIMESTAMP NOT NULL | Момент загрузки в STG | `now()` |
| `_load_id` | TEXT NOT NULL | Идентификатор батча загрузки | `'{{ run_id }}'` |
**Тип историзации:** Нет (накопительный snapshot). Каждый запуск добавляет
новый батч по `_load_id`; прошлые батчи остаются в таблице. Не используйте TRUNCATE.
**Стратегия загрузки:** Полный снимок. Идемпотентность — по составному ключу
`route_no` + `validity` + `_load_id`.
**Distribution Key:** `route_no`
---
## Часть 2. ODS-слой (Operational Data Store)
> **Цель:** Привести данные из STG к целевым типам, очистить, дедуплицировать. > **Цель:** Привести данные из STG к целевым типам, очистить, дедуплицировать.
> Справочники (airplanes, seats, routes) загружаются стратегией TRUNCATE + INSERT > Справочники (`airplanes`, `seats`) загружаются стратегией TRUNCATE + INSERT
> из последнего согласованного батча STG. > из последнего согласованного батча STG. (`ods.routes` реализован в эталоне.)
> >
> **Аналог для изучения:** `sql/ods/airports_ddl.sql`, `sql/ods/airports_load.sql` > **Аналог для изучения:** `sql/ods/airports_ddl.sql`, `sql/ods/airports_load.sql`
### 2.1. ods.airplanes ### 1.1. ods.airplanes
**Описание:** Очищенный справочник моделей воздушных судов с правильными типами. **Описание:** Очищенный справочник моделей воздушных судов с правильными типами.
@@ -175,7 +112,7 @@
--- ---
### 2.2. ods.seats ### 1.2. ods.seats
**Описание:** Карта посадочных мест с корректными типами. **Описание:** Карта посадочных мест с корректными типами.
@@ -203,41 +140,9 @@
--- ---
### 2.3. ods.routes
**Описание:** Справочник маршрутов с правильными типами данных.
**Источник:** `stg.routes`
| Поле | Тип | Описание | Маппинг из STG |
|------|-----|----------|----------------|
| `route_no` | TEXT NOT NULL | Номер маршрута (PK, часть 1) | `route_no` |
| `validity` | TEXT NOT NULL | Период действия (PK, часть 2) | `validity` |
| `departure_airport` | TEXT NOT NULL | Код аэропорта вылета | `departure_airport` |
| `arrival_airport` | TEXT NOT NULL | Код аэропорта прилёта | `arrival_airport` |
| `airplane_code` | TEXT NOT NULL | Код модели самолёта | `airplane_code` |
| `days_of_week` | INTEGER[] | Дни недели (массив) | `days_of_week` — преобразовать TEXT в `INTEGER[]` |
| `departure_time` | TIME NOT NULL | Время вылета | `scheduled_time::TIME` |
| `duration` | INTERVAL NOT NULL | Длительность полёта | `duration::INTERVAL` |
| `_load_id` | TEXT NOT NULL | Идентификатор батча | `'{{ run_id }}'` |
| `_load_ts` | TIMESTAMP NOT NULL | Момент загрузки | `now()` |
**Тип историзации:** Нет (текущее состояние справочника, TRUNCATE + INSERT).
**Стратегия загрузки:** TRUNCATE + INSERT.
**Бизнес-правила:**
- Составной PK: `(route_no, validity)`.
- Преобразование `days_of_week` из текста в массив целых чисел (`INTEGER[]`).
- Преобразование `scheduled_time``TIME`, `duration``INTERVAL`.
**Тип хранения Greenplum:** Append-Only Row.
**Distribution Key:** `(route_no, validity)`
--- ---
## Часть 3. DDS-слой (Detailed Data Store) — Измерения ## Часть 2. DDS-слой (Detailed Data Store) — Измерения
> **Цель:** Построить измерения звёздной схемы (Star Schema) с суррогатными > **Цель:** Построить измерения звёздной схемы (Star Schema) с суррогатными
> ключами. SCD1-измерения обновляют атрибуты «на месте». SCD2-измерение > ключами. SCD1-измерения обновляют атрибуты «на месте». SCD2-измерение
@@ -245,7 +150,7 @@
> >
> **Аналог для SCD1:** `sql/dds/dim_airports_ddl.sql`, `sql/dds/dim_airports_load.sql` > **Аналог для SCD1:** `sql/dds/dim_airports_ddl.sql`, `sql/dds/dim_airports_load.sql`
### 3.1. dds.dim_airplanes (SCD1) ### 2.1. dds.dim_airplanes (SCD1)
**Описание:** Измерение моделей самолётов. Содержит технические характеристики **Описание:** Измерение моделей самолётов. Содержит технические характеристики
и рассчитанное общее количество мест (обогащение из `ods.seats`). и рассчитанное общее количество мест (обогащение из `ods.seats`).
@@ -284,7 +189,7 @@
--- ---
### 3.2. dds.dim_passengers (SCD1) ### 2.2. dds.dim_passengers (SCD1)
**Описание:** Измерение пассажиров. Извлекается из таблицы билетов — каждый **Описание:** Измерение пассажиров. Извлекается из таблицы билетов — каждый
уникальный `passenger_id` становится строкой измерения. уникальный `passenger_id` становится строкой измерения.
@@ -322,7 +227,7 @@
--- ---
### 3.3. dds.dim_routes (SCD2) ### 2.3. dds.dim_routes (SCD2)
> **Это ключевой вызов курсовой.** Реализация SCD Type 2 — обязательный навык > **Это ключевой вызов курсовой.** Реализация SCD Type 2 — обязательный навык
> для Data Engineer. Ниже — алгоритм текстом; SQL вы пишете самостоятельно. > для Data Engineer. Ниже — алгоритм текстом; SQL вы пишете самостоятельно.
@@ -406,14 +311,14 @@ md5(concat_ws('|',
--- ---
## Часть 4. DM-слой (Data Marts) — Витрины ## Часть 3. DM-слой (Data Marts) — Витрины
> **Цель:** Построить аналитические витрины поверх DDS. > **Цель:** Построить аналитические витрины поверх DDS.
> Каждая витрина отвечает на конкретный бизнес-вопрос. > Каждая витрина отвечает на конкретный бизнес-вопрос.
> >
> **Аналог для изучения:** `sql/dm/sales_report_ddl.sql`, `sql/dm/sales_report_load.sql` > **Аналог для изучения:** `sql/dm/sales_report_ddl.sql`, `sql/dm/sales_report_load.sql`
### 4.1. dm.airport_traffic ### 3.1. dm.airport_traffic
**Бизнес-вопрос:** «Какой пассажиропоток и выручка у каждого аэропорта по дням?» **Бизнес-вопрос:** «Какой пассажиропоток и выручка у каждого аэропорта по дням?»
@@ -469,7 +374,7 @@ md5(concat_ws('|',
--- ---
### 4.2. dm.route_performance ### 3.2. dm.route_performance
**Бизнес-вопрос:** «Какие маршруты самые эффективные? Где высокий load factor, **Бизнес-вопрос:** «Какие маршруты самые эффективные? Где высокий load factor,
а где теряем пассажиров?» а где теряем пассажиров?»
@@ -532,7 +437,7 @@ md5(concat_ws('|',
--- ---
### 4.3. dm.monthly_overview ### 3.3. dm.monthly_overview
**Бизнес-вопрос:** «Какова помесячная динамика: рейсы, выручка, load factor **Бизнес-вопрос:** «Какова помесячная динамика: рейсы, выручка, load factor
в разрезе типов самолётов?» в разрезе типов самолётов?»
@@ -594,7 +499,7 @@ md5(concat_ws('|',
--- ---
### 4.4. dm.passenger_loyalty ### 3.4. dm.passenger_loyalty
**Бизнес-вопрос:** «Кто наши самые лояльные пассажиры? Сколько они летают, **Бизнес-вопрос:** «Кто наши самые лояльные пассажиры? Сколько они летают,
тратят, какой класс предпочитают?» тратят, какой класс предпочитают?»
@@ -660,6 +565,34 @@ md5(concat_ws('|',
--- ---
## Пересчёт факта после реализации измерений
После того, как вы реализуете все DDS-измерения (`dim_airplanes`, `dim_passengers`,
`dim_routes`), нужно пересчитать факт — он загружался ещё без ваших SK:
1. Запустите загрузку своих измерений (dim_airplanes, dim_passengers, dim_routes)
2. Выполните `TRUNCATE dds.fact_flight_sales;`
3. Перезапустите загрузку факта (DAG или только таск `load_dds_fact_flight_sales`)
4. Перезапустите DM-витрины
```sql
-- Проверка: все SK заполнены
SELECT
COUNT(*) AS total,
COUNT(departure_airport_sk) AS has_dep_sk,
COUNT(arrival_airport_sk) AS has_arr_sk,
COUNT(route_sk) AS has_route_sk,
COUNT(passenger_sk) AS has_passenger_sk,
COUNT(airplane_sk) AS has_airplane_sk
FROM dds.fact_flight_sales;
```
Это стандартная практика при **late-arriving dimensions** (опаздывающих измерениях):
факт загрузился раньше, чем были готовы справочники, поэтому SK заполнены не были.
После пересчёта все SK проставятся корректно.
---
## Валидация ## Валидация
После реализации каждого слоя запустите валидационный DAG `bookings_validate` После реализации каждого слоя запустите валидационный DAG `bookings_validate`
+10 -17
View File
@@ -20,15 +20,14 @@
|------|-----------------------------------------------------------------| |------|-----------------------------------------------------------------|
| DM | `sales_report` | | DM | `sales_report` |
| DDS | `fact_flight_sales`, `dim_airports` (SCD1), `dim_tariffs` (SCD1), `dim_calendar` | | DDS | `fact_flight_sales`, `dim_airports` (SCD1), `dim_tariffs` (SCD1), `dim_calendar` |
| ODS | `bookings`, `tickets`, `segments`, `flights`, `boarding_passes`, `airports` | | ODS | `bookings`, `tickets`, `segments`, `flights`, `boarding_passes`, `airports`, `routes` |
| STG | `bookings`, `tickets`, `segments`, `flights`, `boarding_passes`, `airports` | | STG | весь слой: `bookings`, `tickets`, `segments`, `flights`, `boarding_passes`, `airports`, `airplanes`, `seats`, `routes` |
### Задание студенту ### Задание студенту
| Слой | Таблицы | Что нового для студента | | Слой | Таблицы | Что нового для студента |
|------|-------------------------------------------------------------------|--------------------------------------------------| |------|-------------------------------------------------------------------|--------------------------------------------------|
| STG | `airplanes`, `seats`, `routes` | Практика по аналогии с эталоном | | ODS | `airplanes`, `seats` | Практика TRUNCATE+INSERT по аналогии с эталоном |
| ODS | `airplanes`, `seats`, `routes` | Практика SCD1 UPSERT по аналогии |
| DDS | `dim_airplanes` (SCD1), `dim_passengers` (SCD1), `dim_routes` (SCD2) | **SCD2 — ключевой вызов курсовой** | | DDS | `dim_airplanes` (SCD1), `dim_passengers` (SCD1), `dim_routes` (SCD2) | **SCD2 — ключевой вызов курсовой** |
| DM | `airport_traffic`, `monthly_overview`, `route_performance`, `passenger_loyalty` | Разная сложность (от простой к сложной) | | DM | `airport_traffic`, `monthly_overview`, `route_performance`, `passenger_loyalty` | Разная сложность (от простой к сложной) |
@@ -38,14 +37,13 @@
Студенту рекомендуется (но не обязательно) двигаться в таком порядке: Студенту рекомендуется (но не обязательно) двигаться в таком порядке:
1. **STG** (airplanes, seats, routes) — разминка, по аналогии 1. **ODS** (airplanes, seats) — практика TRUNCATE+INSERT
2. **ODS** (airplanes, seats, routes) — закрепление UPSERT 2. **DDS** dim_airplanes, dim_passengers (SCD1) — новые измерения
3. **DDS** dim_airplanes, dim_passengers (SCD1) — новые измерения 3. **DDS** dim_routes (**SCD2**) — ключевой вызов
4. **DDS** dim_routes (**SCD2**) — ключевой вызов 4. **DM** airport_traffic — простая витрина, похожа на sales_report
5. **DM** airport_traffic — простая витрина, похожа на sales_report 5. **DM** route_performance — TRUNCATE+INSERT, SCD2-агрегация по BK
6. **DM** route_performance — TRUNCATE+INSERT, SCD2-агрегация по BK 6. **DM** monthly_overview — двухуровневая агрегация
7. **DM** monthly_overview — двухуровневая агрегация 7. **DM** passenger_loyalty — самая сложная, пересчёт истории
8. **DM** passenger_loyalty — самая сложная, пересчёт истории
Порядок выстроен от простого к сложному. Каждый шаг опирается на опыт Порядок выстроен от простого к сложному. Каждый шаг опирается на опыт
предыдущего. предыдущего.
@@ -83,14 +81,9 @@
``` ```
bookings_validate bookings_validate
├── validate_stg
│ ├── check_stg_airplanes_exists (таблица создана, >0 строк)
│ ├── check_stg_seats_exists
│ └── check_stg_routes_exists
├── validate_ods ├── validate_ods
│ ├── check_ods_airplanes_rowcount (ODS >= STG по кол-ву уникальных BK) │ ├── check_ods_airplanes_rowcount (ODS >= STG по кол-ву уникальных BK)
│ ├── check_ods_seats_rowcount │ ├── check_ods_seats_rowcount
│ ├── check_ods_routes_rowcount
│ └── check_ods_no_null_pks (PK not null) │ └── check_ods_no_null_pks (PK not null)
├── validate_dds ├── validate_dds
│ ├── check_dim_airplanes_exists │ ├── check_dim_airplanes_exists
+8 -2
View File
@@ -1,6 +1,10 @@
# Схема БД DWH (Bookings → Greenplum) # Схема БД DWH (Bookings → Greenplum)
> **Статус:** Все слои реализованы (STG, ODS, DDS, DM). > **Статус:** Все слои реализованы в ветке `solution` (STG, ODS, DDS, DM).
> На ветке `main` студенческие таблицы ODS (`airplanes`, `seats`), DDS-измерения
> (`dim_airplanes`, `dim_passengers`, `dim_routes`) и студенческие DM-витрины
> (`airport_traffic`, `route_performance`, `monthly_overview`, `passenger_loyalty`)
> — заглушки (`SELECT 1;`). Данные появятся после реализации заданий.
Архитектура хранилища данных (DWH) для учебного проекта Airflow + Greenplum. Архитектура хранилища данных (DWH) для учебного проекта Airflow + Greenplum.
Источник — демо-БД `bookings` (Postgres). Документ даёт цельный взгляд «сверху»; Источник — демо-БД `bookings` (Postgres). Документ даёт цельный взгляд «сверху»;
@@ -250,7 +254,9 @@ Degenerate keys: `book_ref`, `ticket_no`, `flight_id`, `book_date`, `seat_no`.
## Ключевые договорённости ## Ключевые договорённости
- **Нейминг полей**: [`naming_conventions.md`](naming_conventions.md) - **Нейминг полей**: [`naming_conventions.md`](naming_conventions.md)
- **DQ-проверки**: SQL-скрипты с `RAISE EXCEPTION` (не отдельный DQ-слой) - **DQ-проверки**: SQL-скрипты с `RAISE EXCEPTION` (не отдельный DQ-слой).
На ветке `main` студенческие DQ-скрипты (`airplanes_dq.sql`, `seats_dq.sql`,
student dims/DM) — заглушки (`SELECT 1;`) без проверок.
- **Инкремент STG**: для `tickets` опорная дата — из `bookings.book_date` - **Инкремент STG**: для `tickets` опорная дата — из `bookings.book_date`
- **Point-in-time JOIN**: факт ↔ `dim_routes` по `[valid_from, valid_to)` - **Point-in-time JOIN**: факт ↔ `dim_routes` по `[valid_from, valid_to)`
- **Суррогатные ключи**: `MAX(sk) + ROW_NUMBER()` (не SERIAL — GP-специфика) - **Суррогатные ключи**: `MAX(sk) + ROW_NUMBER()` (не SERIAL — GP-специфика)
+18 -3
View File
@@ -48,6 +48,21 @@ WITH fact_src AS (
-- Учебный комментарий: Late-arriving dimensions (Опаздывающие измерения) -- Учебный комментарий: Late-arriving dimensions (Опаздывающие измерения)
-- Мы используем LEFT JOIN, так как факт (рейс/билет) может прийти раньше, -- Мы используем LEFT JOIN, так как факт (рейс/билет) может прийти раньше,
-- чем справочник (пассажир/маршрут) обновится в DDS. -- чем справочник (пассажир/маршрут) обновится в DDS.
--
-- Airport lookup через ods.routes (эталонный справочник), а не через dds.dim_routes
-- (студенческое задание SCD2). Это архитектурное решение: эталонный пайплайн работает
-- независимо от студенческого кода. Аэропорты вылета/прилёта одинаковы во всех
-- версиях одного route_no — безопасно брать из ODS без point-in-time логики.
-- ROW_NUMBER по validity DESC: выбираем актуальную версию расписания маршрута.
LEFT JOIN (
SELECT route_no, departure_airport, arrival_airport
FROM (
SELECT route_no, departure_airport, arrival_airport,
ROW_NUMBER() OVER (PARTITION BY route_no ORDER BY validity DESC) AS rn
FROM ods.routes
) ranked
WHERE rn = 1
) AS ods_rte ON ods_rte.route_no = flt.route_no
LEFT JOIN dds.dim_routes AS rte LEFT JOIN dds.dim_routes AS rte
ON rte.route_bk = flt.route_no ON rte.route_bk = flt.route_no
AND flt.scheduled_departure::DATE >= rte.valid_from AND flt.scheduled_departure::DATE >= rte.valid_from
@@ -55,11 +70,11 @@ WITH fact_src AS (
LEFT JOIN dds.dim_calendar AS cal LEFT JOIN dds.dim_calendar AS cal
ON cal.date_actual = flt.scheduled_departure::DATE ON cal.date_actual = flt.scheduled_departure::DATE
LEFT JOIN dds.dim_airports AS dep LEFT JOIN dds.dim_airports AS dep
ON dep.airport_bk = rte.departure_airport ON dep.airport_bk = ods_rte.departure_airport -- через ods.routes (эталон)
LEFT JOIN dds.dim_airports AS arr LEFT JOIN dds.dim_airports AS arr
ON arr.airport_bk = rte.arrival_airport ON arr.airport_bk = ods_rte.arrival_airport -- через ods.routes (эталон)
LEFT JOIN dds.dim_airplanes AS ap LEFT JOIN dds.dim_airplanes AS ap
ON ap.airplane_bk = rte.airplane_code ON ap.airplane_bk = rte.airplane_code -- через dim_routes (point-in-time, как прежде)
LEFT JOIN dds.dim_tariffs AS tar LEFT JOIN dds.dim_tariffs AS tar
ON tar.fare_conditions = seg.fare_conditions ON tar.fare_conditions = seg.fare_conditions
LEFT JOIN dds.dim_passengers AS pax LEFT JOIN dds.dim_passengers AS pax