docs(map): добавлены карта и руководство по структуре DWH

Объяснены зерно таблиц, связи и путь бронирования до витрины.
Добавлена автономная карта с двумя видами связей и адресами объектов.
Состояние заглушек сверено с DDL и ТЗ; SQL и DAG не изменялись.

Проверки: сборка и ссылки, make lint, pytest (4 passed, 14 skipped),
Chromium file:// offline: выбор, связи, сброс, история и узкий экран.

Refs #2
This commit is contained in:
ddadmin committed 2026-09-20 13:01:57 -04:00
1 parent 77d72b08c6
commit b9bcfe7230
9 files changed
+10806 -373

No files matched your search

+174 -366
View File
@@ -1,399 +1,207 @@
# Схема БД DWH (Bookings → Greenplum)
# Как устроено хранилище авиаперевозок
> **Статус:** Все слои реализованы в ветке `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;`). Данные появятся после реализации заданий.
Разберем одну поездку: как бронирование связано с билетами и перелетами,
какие строки попадут в факт и как сложится выручка по направлениям.
Найти эти строки в структуре хранилища поможет [интерактивная карта](architecture-map.html).
Карта хранит подробности объектов и связей, а это руководство задает маршрут чтения.
Архитектура хранилища данных (DWH) для учебного проекта Airflow + Greenplum.
Источник — демо-БД `bookings` (Postgres). Одна диаграмма отвечает на один вопрос:
карта, которая показывает всё сразу, не даёт ясного ответа ни на один из них.
## Как открыть карту
Понять, как устроен проект → лесенка ниже.
Из локальной копии репозитория откройте `docs/design/architecture-map.html`
в браузере: через файловый менеджер или командой из корня проекта:
Пишете свой load-скрипт и хотите свериться → [`reading_the_pipeline.md`](reading_the_pipeline.md).
```bash
xdg-open docs/design/architecture-map.html
```
Какие DQ-проверки писать → [`dq_taxonomy.md`](../reference/dq_taxonomy.md).
На Windows можно открыть этот же файл в Проводнике. Сервер, интернет и работающий
Docker для карты не нужны. Храните HTML рядом с остальными файлами репозитория:
ссылки из карточек ведут в локальные SQL и Markdown.
Браузер может показать Markdown как текст, а SQL предложить скачать.
Скачанный файл можно открыть в редакторе; для форматированного чтения
руководства используйте Markdown preview.
Точечный вопрос «что кормит X?» → полная карта в конце.
**В Gitea ссылка на HTML обычно показывает исходный текст, а не карту.**
Сначала откройте локальный HTML. Адресная ссылка вида
`architecture-map.html#node=dds.fact_flight_sales` сразу выбирает таблицу;
ее можно открыть из локального просмотра этого руководства или добавить
часть после `#` к адресу уже открытой карты.
---
## Бронирование, билет и перелет
Ниже **учебный пример**, придуманный для объяснения. Это не выгрузка из стенда.
Короткие обозначения `T1`, `F101`, `R1` заменяют настоящие номера.
Аня и Борис купили билеты в одном бронировании `B1` от 30 августа.
Аня летит 1 сентября из Москвы через Петербург в Казань. Борис летит
тем же первым рейсом до Петербурга. Все сегменты - Economy.
| Таблица источника | Пример строк | Что различают строки |
|---|---|---|
| `bookings.bookings` | `B1`, сумма `12000` | Одно бронирование на всю покупку |
| `bookings.tickets` | `T1 → B1, Аня`; `T2 → B1, Борис` | Два билета, по одному на пассажира |
| `bookings.segments` | `(T1, F101), 3000`; `(T1, F102), 5000`; `(T2, F101), 4000` | Три оплаченных перелета |
| `bookings.flights` | `F101 → R1`, 1 сентября; `F102 → R2`, 1 сентября | Два конкретных рейса |
| `bookings.routes` | `R1: SVO → LED`; `R2: LED → KZN` | Маршруты и варианты их расписания |
Бронирование объединяет покупку. Билет принадлежит пассажиру и может включать
несколько сегментов. Сегмент соединяет билет с конкретным рейсом и хранит цену
этого перелета. Поэтому `ticket_no` недостаточно для поиска сегмента:
нужна пара `(ticket_no, flight_id)`.
Рейс и маршрут тоже различаются. `flight_id` обозначает один вылет,
а `route_no` повторяется у рейсов в разные дни. В `routes` у одного номера
могут быть разные варианты расписания; строку различает пара
`(route_no, validity)`.
Откройте [билеты](architecture-map.html#node=bookings.tickets&view=keys)
в виде **"Связи по ключам"**. Выделены бронирование, сегменты и связанные с ними
талоны. Выберите связь билета с сегментами: в карточке виден `ticket_no`.
Для связи сегмента с талоном понадобятся оба поля ключа, иначе талон
первого рейса можно ошибочно присоединить ко второму.
## Как устроен поток целиком?
Здесь показано движение данных через пять слоёв DWH.
Оставьте билет выбранным и переключитесь на **"Поток данных"**.
Таблицы останутся на местах, выбранный билет сохранится; теперь стрелки
показывают чтение данных загрузчиками. Синий цвет отмечает источники выбранной
таблицы, зеленый - потребителей, оранжевый - сам выбор. Несвязанные объекты
остаются видны приглушенными. Повторный выбор, **"Общий вид"** или `Esc`
возвращает обзор.
```mermaid
graph LR
SRC["Source<br/>Postgres bookings"] --> STG["STG<br/>сырые данные"]
STG --> ODS["ODS<br/>очищенные данные и типы"]
ODS --> DDS["DDS<br/>факты и измерения"]
DDS --> DM["DM<br/>витрины для бизнес-вопросов"]
```
Проследите путь
[сегмента](architecture-map.html#node=bookings.segments) через STG и ODS к факту:
## Как выглядит звезда?
- Источник → STG: PXF читает Postgres через внешние `stg.*_ext`.
Эти входы описаны в карточках STG и сами не хранят копию. Загрузчики
сохраняют строки в `stg.*` с бизнес-полями в TEXT и метками загрузки.
- STG → ODS: У строки появляются нужные типы. Например,
`stg.segments.price` становится `ods.segments.amount` типа NUMERIC.
ODS хранит текущее состояние строки по бизнес-ключу.
- ODS → DDS → DM: Сегмент обогащается ключами измерений в факте,
затем участвует в агрегатах витрины.
Здесь показан факт и шесть его измерений.
```mermaid
graph TD
CAL["dds.dim_calendar"] --> FACT["dds.fact_flight_sales"]
AIRPORT["dds.dim_airports"] --> FACT
AIRPLANE["dds.dim_airplanes"] --> FACT
TARIFF["dds.dim_tariffs"] --> FACT
PASSENGER["dds.dim_passengers"] --> FACT
ROUTE["dds.dim_routes<br/>SCD2"] --> FACT
```
Стрелка данных не означает очередность задач Airflow. Например, календарь
генерируется без чтения ODS, хотя в DDS DAG задачи других измерений ждут
проверки календаря. Порядок выполнения смотрите по ссылке **"DAG · порядок задач"**
в карточке. Поля, SQL и проверки раскрываются там же, после выбора таблицы.
## Как собирается факт?
Здесь показаны таблицы, которые `fact_src` читает при сборке факта.
Сплошные стрелки обозначают `INNER JOIN`. Пунктирные стрелки обозначают `LEFT JOIN`.
Откройте [dds.fact_flight_sales](architecture-map.html#node=dds.fact_flight_sales).
В нашем примере в нем будет **три строки**, по одной на каждый сегмент.
Важные значения до выполнения студенческих заданий выглядят так:
```mermaid
graph LR
SEG["ods.segments"] -->|"Main stream: grain, price"| FACT["dds.fact_flight_sales"]
TKT["ods.tickets"] -->|"book_ref, passenger_id"| FACT
BKG["ods.bookings"] -->|"book_date"| FACT
FLT["ods.flights"] -->|"schedule, route_no"| FACT
ODS_RTE["ods.routes"] -.->|"dep/arr airports"| FACT
CAL["dds.dim_calendar"] -.->|"calendar_sk"| FACT
AIRPORT["dds.dim_airports"] -.->|"dep/arr _sk"| FACT
AIRPLANE["dds.dim_airplanes"] -.->|"airplane_sk"| FACT
TARIFF["dds.dim_tariffs"] -.->|"tariff_sk"| FACT
PASSENGER["dds.dim_passengers"] -.->|"passenger_sk"| FACT
ROUTE["dds.dim_routes"] -.->|"route_sk"| FACT
BOARDING["ods.boarding_passes"] -.->|"LEFT JOIN seat_no"| FACT
```
| ticket_no | flight_id | price | departure_airport_sk | arrival_airport_sk | route_sk | airplane_sk | passenger_sk |
|---|---|---:|---|---|---|---|---|
| T1 | F101 | 3000 | SK аэропорта SVO | SK аэропорта LED | NULL | NULL | NULL |
| T1 | F102 | 5000 | SK аэропорта LED | SK аэропорта KZN | NULL | NULL | NULL |
| T2 | F101 | 4000 | SK аэропорта SVO | SK аэропорта LED | NULL | NULL | NULL |
`ods.routes` оставлен отдельным узлом: через него факт находит аэропорты
вылета и прилёта, не завися от студенческого `dim_routes`. Схема показывает
замысел загрузки. На ветке `main` поля `route_sk`, `airplane_sk` и
`passenger_sk` могут быть `NULL`, пока студент не реализовал измерения.
Почему это не всегда ошибка, объяснено в [справочнике DQ](../reference/dq_taxonomy.md#null--не-всегда-ошибка).
SK здесь обозначены словами: конкретные числа назначает загрузка измерений.
`calendar_sk` указывает на 1 сентября, `tariff_sk` - на Economy.
`book_ref = B1` повторяется во всех трех строках; `book_date` - 30 августа.
Цена хранится на уровне сегмента, а сумма `12000` из бронирования в факт
не переносится. Иначе ее можно было бы ошибочно посчитать трижды.
Начальный поток - `ods.segments`. Соединения с билетами, бронированиями
и рейсами - `INNER JOIN`: отсутствие подходящей строки исключит сегмент
из вставки факта. Это потеря покрытия, которую должна обнаружить DQ.
Посадочный талон добавляется через `LEFT JOIN` по паре `(ticket_no, flight_id)`.
Нет талона - сегмент сохраняется, `seat_no` остается `NULL`, `is_boarded = false`.
В этом стенде признак посадки означает именно наличие талона.
### Откуда берутся два аэропорта
В карточке факта откройте связи **"Коды двух аэропортов"**,
**"Аэропорт вылета"** и **"Аэропорт прилета"**:
1. Рейс дает `route_no`. Из `ods.routes` берется одна строка с наибольшим
`validity` на этот номер. В ODS поле `validity` хранится как TEXT;
этот выбор не использует дату рейса.
2. `departure_airport` и `arrival_airport` дают два кода аэропорта.
3. Один `dds.dim_airports` присоединяется дважды по `airport_bk`, в ролях
вылета и прилета. Получаются два разных поля SK в факте.
Этот путь работает без студенческого `dim_routes`. Отсутствующий аэропорт
в факте - аномалия данных, а не ожидаемый результат невыполненного задания.
SQL использует `LEFT JOIN`, поэтому строка с таким пропуском сохранится;
допустимость пропуска отдельно оценивает [DQ факта](../../sql/dds/fact_flight_sales_dq.sql).
### Какая версия маршрута нужна рейсу
Связь **"Версия на дату рейса"** ведет к `dds.dim_routes` по `route_bk = route_no`
и дате `scheduled_departure::DATE`. Дата должна попасть в полуоткрытый
интервал `[valid_from, valid_to)`; `NULL` в конце означает открытую версию.
Рейс на границе двух версий относится к новой.
Из выбранной версии берется `route_sk`, а ее `airplane_code` нужен для
поиска `airplane_sk` в `dim_airplanes`. Поэтому одного заполненного справочника
моделей недостаточно: без подходящей версии маршрута `airplane_sk` не появится.
## Что кормит витрины?
Здесь показано, во что расходятся строки факта.
Перейдите к [dm.sales_report](architecture-map.html#node=dm.sales_report).
Зерно меняется: день вылета × два аэропорта × класс обслуживания.
Три строки примера дадут две строки витрины:
```mermaid
graph LR
FACT["dds.fact_flight_sales"] -->|"день × аэропорты × тариф"| SALES["dm.sales_report<br/>выручка и посадка"]
FACT -->|"день × аэропорт"| TRAFFIC["dm.airport_traffic<br/>пассажиропоток"]
FACT -->|"route_bk"| ROUTE["dm.route_performance<br/>эффективность маршрута"]
FACT -->|"месяц × airplane_sk"| MONTHLY["dm.monthly_overview<br/>динамика по самолётам"]
FACT -->|"passenger_sk"| LOYALTY["dm.passenger_loyalty<br/>профиль лояльности"]
```
| Дата вылета | Направление | Класс | tickets_sold | total_revenue |
|---|---|---|---:|---:|
| 1 сентября | SVO → LED | Economy | 2 | 7000 |
| 1 сентября | LED → KZN | Economy | 1 | 5000 |
Измерения уточняют даты, аэропорты, тарифы, маршруты, самолёты и пассажиров,
но основной поток во все пять витрин начинается с `fact_flight_sales`.
`tickets_sold` здесь считает **сегменты**. Один билет может участвовать несколько раз.
Если сложить его по направлениям, получится 3 при двух билетах.
`total_revenue` складывается из цен сегментов, поэтому итог равен 12000.
Доля посадок рассчитывается по `is_boarded`.
---
Готовая витрина использует календарь, две роли аэропорта и тариф через
`INNER JOIN`: факт без совпадения в одном из этих измерений не попадет в агрегат.
Поля `route_sk`, `airplane_sk`, `passenger_sk` ей не требуются, поэтому
эталон работает до выполнения заданий. Остальные витрины видны рядом
пунктирными рамками: в их карточках описан требуемый результат по ТЗ.
## Сводка объектов по слоям
## Модели самолетов и места: начало задания
### STG (Staging)
Откройте [dds.dim_airplanes](architecture-map.html#node=dds.dim_airplanes).
У него два источника: `ods.airplanes` дает характеристики модели,
а `ods.seats` - места, по которым считается вместимость.
Строка измерения описывает **модель**, например тип самолета, а не отдельный борт.
`seat_no` уникален только вместе с `airplane_code`.
Сырые данные из источника, все бизнес-поля как TEXT. Хранение: AO Row (zstd).
Служебные поля: `event_ts`, `_load_ts`, `_load_id`.
| Таблица | Источник (PXF) | Ключ | Стратегия загрузки | Distribution |
|---------|---------------|------|-------------------|--------------|
| `stg.bookings` | `bookings.bookings` | `book_ref` | Инкремент (book_date) | `book_ref` |
| `stg.tickets` | `bookings.tickets` | `ticket_no` | Инкремент (через bookings) | `book_ref` |
| `stg.flights` | `bookings.flights` | `flight_id` | Инкремент (scheduled_departure) | `flight_id` |
| `stg.segments` | `bookings.segments` | `(ticket_no, flight_id)` | Инкремент (через tickets) | `ticket_no` |
| `stg.boarding_passes` | `bookings.boarding_passes` | `(ticket_no, flight_id)` | Инкремент (через tickets/bookings) | `ticket_no` |
| `stg.airports` | `bookings.airports_data` | `airport_code` | Full snapshot | `airport_code` |
| `stg.airplanes` | `bookings.airplanes_data` | `airplane_code` | Full snapshot | `airplane_code` |
| `stg.routes` | `bookings.routes` | `(route_no, validity)` | Full snapshot | `route_no` |
| `stg.seats` | `bookings.seats` | `(airplane_code, seat_no)` | Full snapshot | `airplane_code` |
Детали: [`bookings_stg_design.md`](bookings_stg_design.md)
### ODS (Operational Data Store)
Очищенные данные с корректными типами. Справочники: AO Row (TRUNCATE+INSERT).
Транзакции: Heap (SCD1 UPSERT). Служебные поля: `_load_id`, `_load_ts`,
`event_ts` (для транзакционных таблиц).
| Таблица | Стратегия | Storage | Ключевые преобразования |
|---------|-----------|---------|------------------------|
| `ods.bookings` | SCD1 UPSERT (HWM) | Heap | `total_amount` TEXT → NUMERIC |
| `ods.tickets` | SCD1 UPSERT (HWM) | Heap | `outbound` TEXT → BOOLEAN |
| `ods.flights` | SCD1 UPSERT (HWM) | Heap | TEXT → INTEGER, TIMESTAMP WITH TIME ZONE |
| `ods.segments` | SCD1 UPSERT (HWM) | Heap | `price` TEXT → `amount` NUMERIC |
| `ods.boarding_passes` | SCD1 UPSERT (HWM) | Heap | `boarding_no` TEXT → INTEGER |
| `ods.airports` | TRUNCATE+INSERT | AO Row | JSON → отдельные поля (`airport_name`, `city`) |
| `ods.airplanes` | TRUNCATE+INSERT | AO Row | `range`/`speed` TEXT → INTEGER |
| `ods.routes` | TRUNCATE+INSERT | AO Row | `days_of_week` → INTEGER[], `scheduled_time` → TIME |
| `ods.seats` | TRUNCATE+INSERT | AO Row | Без преобразований |
Детали: [`bookings_ods_design.md`](bookings_ods_design.md)
### DDS (Detailed Data Store — Star Schema)
Измерения с суррогатными ключами. Факт в центре звезды.
SK генерация: `MAX(sk) + ROW_NUMBER()` (безопасно при `concurrency=1`).
#### Измерения
| Измерение | BK | SK | SCD | Storage | Источник |
|-----------|----|----|-----|---------|----------|
| `dim_calendar` | `date_actual` | `calendar_sk` | Static | AO Row | Генерация (2016–2030) |
| `dim_airports` | `airport_bk` | `airport_sk` | SCD1 | Heap | `ods.airports` |
| `dim_airplanes` | `airplane_bk` | `airplane_sk` | SCD1 | Heap | `ods.airplanes` + `ods.seats` (total_seats) |
| `dim_tariffs` | `fare_conditions` | `tariff_sk` | SCD1 | AO Row | `ods.segments` (DISTINCT) |
| `dim_passengers` | `passenger_id` | `passenger_sk` | SCD1 | Heap | `ods.tickets` (дедупликация) |
| `dim_routes` | `route_bk` | `route_sk` | **SCD2** | Heap | `ods.routes` + `dim_airports` + `dim_airplanes` |
#### Факт
`dds.fact_flight_sales` — зерно: 1 строка = 1 сегмент билета (`ticket_no` + `flight_id`).
| FK | Lookup-путь | Примечание |
|----|-------------|------------|
| `calendar_sk` | `dim_calendar` | Дата вылета |
| `departure_airport_sk` | `ods.routes` → `dim_airports` | Аэропорт вылета (эталонный справочник) |
| `arrival_airport_sk` | `ods.routes` → `dim_airports` | Аэропорт прилёта (эталонный справочник) |
| `airplane_sk` | `dim_routes` → `dim_airplanes` | Самолёт (point-in-time SCD2) |
| `tariff_sk` | `dim_tariffs` | Тариф |
| `passenger_sk` | `dim_passengers` | Пассажир |
| `route_sk` | `dim_routes` | Версия маршрута (SCD2, point-in-time) |
Метрики: `price` (NUMERIC), `is_boarded` (BOOLEAN).
Degenerate keys: `book_ref`, `ticket_no`, `flight_id`, `book_date`, `seat_no`.
Детали: [`bookings_dds_design.md`](bookings_dds_design.md)
### DM (Data Marts — Витрины)
Аналитические витрины поверх DDS. Каждая отвечает на конкретный бизнес-вопрос.
| Витрина | Бизнес-вопрос | Зерно | Стратегия | Storage |
|---------|--------------|-------|-----------|---------|
| `dm.sales_report` | Выручка и boarding rate по направлениям/тарифам/дням | (flight_date, dep_sk, arr_sk, tariff_sk) | UPSERT (HWM) | Heap |
| `dm.airport_traffic` | Пассажиропоток аэропортов по дням | (traffic_date, airport_sk) | UPSERT (HWM) | Heap |
| `dm.route_performance` | Эффективность маршрутов за всё время | route_bk | Full Rebuild | AO Column (zstd) |
| `dm.monthly_overview` | Помесячная динамика по типам самолётов | (year, month, airplane_sk) | UPSERT (HWM) | Heap |
| `dm.passenger_loyalty` | Профиль лояльности пассажиров | passenger_sk | UPSERT (затронутые ключи) | Heap |
Детали: [`bookings_dm_design.md`](bookings_dm_design.md)
---
На `main` STG уже заполняется, а загрузки `ods.airplanes`, `ods.seats`
и `dds.dim_airplanes` пока содержат `SELECT 1;`. Карточки помечены
**"Задание"** и ведут к соответствующим пунктам ТЗ. Ожидаемые поля
известны из DDL, но готовой загрузки этих объектов еще нет.
## Ключевые договорённости
- **Нейминг полей**: [`naming_conventions.md`](naming_conventions.md)
- **DQ-проверки**: SQL-скрипты с `RAISE EXCEPTION` (не отдельный DQ-слой).
На ветке `main` студенческие DQ-скрипты (`airplanes_dq.sql`, `seats_dq.sql`,
student dims/DM) — заглушки (`SELECT 1;`) без проверок.
- **Инкремент STG**: для `tickets` опорная дата — из `bookings.book_date`
- **Point-in-time JOIN**: факт ↔ `dim_routes` по `[valid_from, valid_to)`
- **Суррогатные ключи**: `MAX(sk) + ROW_NUMBER()` (не SERIAL — GP-специфика)
- Ключи на карте логические. В DDL таблиц DWH нет ограничений
`PRIMARY KEY`, `UNIQUE` и `FOREIGN KEY`; уникальность и связи проверяют SQL
и DQ. `DISTRIBUTED BY` отвечает за размещение строк.
- `_load_id` и `_load_ts` описывают загрузку. `event_ts` - ее опорное время;
у снимков справочников в STG оно равно времени получения снимка.
Подробности - в [соглашениях об именах](naming_conventions.md).
- До реализации измерений допустимы `NULL` в `passenger_sk`, `route_sk`
и `airplane_sk`. Ключи фиксируются **при вставке** факта: повторный запуск
меняет цену и сведения о посадке, но не заполняет старые SK.
После выполнения заданий следуйте разделу
["Пересчет факта после реализации измерений"](../assignment/analyst_spec.md#пересчёт-факта-после-реализации-измерений).
- В карточке заглушки описан результат из актуального ТЗ. Если ТЗ не определяет
тип соединения или поведение при пропуске строки, карта отмечает этот предел,
не выдает предположение за работающий SQL.
---
## Куда идти дальше
## Обучающие материалы
Для первого задания разберите готовую загрузку справочника:
[как читать SQL загрузки](reading_the_pipeline.md), затем откройте
[учебные задания](../assignment/README.md). Для точечного вопроса
о таблице возвращайтесь на карту: там находится каталог объектов,
их источников, потребителей, SQL и DQ.
### Глоссарий
| Термин | Объяснение |
|--------|-----------|
| **Зерно факта (Fact Grain)** | Минимальная единица в факте. Здесь — один сегмент билета. |
| **Суррогатный ключ (SK)** | Технический INT-ключ, генерируемый в DWH. |
| **Бизнес-ключ (BK)** | Ключ из источника (`airport_code`, `passenger_id`). |
| **Star Schema** | Факт в центре, измерения вокруг (без snowflake-подтаблиц). |
| **SCD Type 1** | Перезапись атрибутов без истории. |
| **SCD Type 2** | Версионирование: `valid_from`/`valid_to`, `hashdiff`. |
| **AO Row/Column** | Append-Only хранение (Row или Column). Не поддерживает UPDATE. |
| **Heap** | Стандартное хранение с поддержкой UPDATE/DELETE. |
| **HWM (High Water Mark)** | Отсечка по `MAX(_load_ts)` для инкрементальной загрузки. |
### На что обратить внимание
**Обогащение измерений:**
`seats` + `airplanes` → `dim_airplanes` — пример обогащения (total_seats).
**«Майнинг» измерений из транзакций:**
`tickets` → `dim_passengers` — в источнике нет таблицы «Пассажиры». Извлекаем
уникальных пассажиров из билетов с дедупликацией.
**Нормализация:**
`segments.fare_conditions` → `dim_tariffs` — выносим строковый атрибут в отдельный
справочник для компактного INT-ключа в факте.
**Почему нет `dim_bookings`:**
`bookings` — транзакция, не справочник. `book_ref` и `book_date` хранятся
как degenerate keys в факте.
**Point-in-time JOIN (SCD2):**
Факт присоединяется к той версии маршрута, которая действовала на дату вылета:
`scheduled_departure::DATE >= valid_from AND (valid_to IS NULL OR ... < valid_to)`.
---
## Связанные документы
- [`bookings_stg_design.md`](bookings_stg_design.md) — дизайн STG
- [`bookings_ods_design.md`](bookings_ods_design.md) — дизайн ODS (SCD1, batch contract, DQ)
- [`bookings_dds_design.md`](bookings_dds_design.md) — дизайн DDS (Star Schema, SCD2)
- [`bookings_dm_design.md`](bookings_dm_design.md) — дизайн DM (5 витрин)
- [`naming_conventions.md`](naming_conventions.md) — нейминг полей
- [`reading_the_pipeline.md`](reading_the_pipeline.md) — как читать связи из скрипта загрузки
- [`../reference/dq_taxonomy.md`](../reference/dq_taxonomy.md) — классы DQ-проверок и допустимые NULL
- Часовые пояса и настройка PXF — в ветке `solution` (`docs/reference/`)
- [`../assignment/analyst_spec.md`](../assignment/analyst_spec.md) — ТЗ от аналитика (курсовое задание)
---
## Полная карта lineage (справочная)
Здесь собран цельный взгляд «сверху». Эта карта нужна для точечного поиска:
например, «что кормит X?» или «куда идут данные из X?». Читать весь пайплайн
по ней подряд не нужно.
```mermaid
graph LR
%% Стили
classDef source fill:#e1f5fe,stroke:#01579b,stroke-width:2px;
classDef stg fill:#fff9c4,stroke:#fbc02d,stroke-width:2px;
classDef ods fill:#e0f2f1,stroke:#00695c,stroke-width:2px;
classDef dim fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px;
classDef fact fill:#ffccbc,stroke:#bf360c,stroke-width:4px;
classDef dm fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
%% 1. Source
subgraph Source_Postgres [Source: Postgres Bookings]
direction TB
SRC_Airports[airports_data]:::source
SRC_Airplanes[airplanes_data]:::source
SRC_Routes[routes]:::source
SRC_Seats[seats]:::source
SRC_Bookings[bookings]:::source
SRC_Tickets[tickets]:::source
SRC_Flights[flights]:::source
SRC_Segments[segments]:::source
SRC_Boarding[boarding_passes]:::source
end
%% 2. STAGING (Load 1-to-1, AO-Row)
subgraph STG_Layer [Layer: STG Staging]
direction TB
STG_Airports[stg.airports]:::stg
STG_Airplanes[stg.airplanes]:::stg
STG_Routes[stg.routes]:::stg
STG_Seats[stg.seats]:::stg
STG_Bookings[stg.bookings]:::stg
STG_Tickets[stg.tickets]:::stg
STG_Flights[stg.flights]:::stg
STG_Segments[stg.segments]:::stg
STG_Boarding[stg.boarding_passes]:::stg
end
%% Links Source to STG
SRC_Airports --> STG_Airports
SRC_Airplanes --> STG_Airplanes
SRC_Routes --> STG_Routes
SRC_Seats --> STG_Seats
SRC_Bookings --> STG_Bookings
SRC_Tickets --> STG_Tickets
SRC_Flights --> STG_Flights
SRC_Segments --> STG_Segments
SRC_Boarding --> STG_Boarding
%% 3. ODS (3NF, Clean, Type)
subgraph ODS_Layer [Layer: ODS Operational Core]
direction TB
ODS_Airports[ods.airports]:::ods
ODS_Airplanes[ods.airplanes]:::ods
ODS_Routes[ods.routes]:::ods
ODS_Seats[ods.seats]:::ods
ODS_Bookings[ods.bookings]:::ods
ODS_Tickets[ods.tickets]:::ods
ODS_Flights[ods.flights]:::ods
ODS_Segments[ods.segments]:::ods
ODS_Boarding[ods.boarding_passes]:::ods
end
%% Links STG to ODS
STG_Airports --> ODS_Airports
STG_Airplanes --> ODS_Airplanes
STG_Routes --> ODS_Routes
STG_Seats --> ODS_Seats
STG_Bookings --> ODS_Bookings
STG_Tickets --> ODS_Tickets
STG_Flights --> ODS_Flights
STG_Segments --> ODS_Segments
STG_Boarding --> ODS_Boarding
%% 4. DDS (Star Schema)
subgraph DDS_Layer [Layer: DDS Star Schema]
direction TB
DIM_Calendar[dds.dim_calendar]:::dim
DIM_Airports[dds.dim_airports]:::dim
DIM_Airplanes[dds.dim_airplanes]:::dim
DIM_Tariffs[dds.dim_tariffs]:::dim
DIM_Passengers[dds.dim_passengers]:::dim
DIM_Routes[dds.dim_routes SCD2]:::dim
FACT_Sales[dds.fact_flight_sales]:::fact
end
%% ODS to DDS Dimensions
ODS_Airports --> DIM_Airports
ODS_Airplanes --> DIM_Airplanes
ODS_Seats -.->|total_seats| DIM_Airplanes
ODS_Segments -.->|DISTINCT| DIM_Tariffs
ODS_Tickets -->|Unique passengers| DIM_Passengers
ODS_Routes -->|SCD2 hashdiff| DIM_Routes
DIM_Airports -.->|cities| DIM_Routes
DIM_Airplanes -.->|model, seats| DIM_Routes
%% ODS to Fact
ODS_Segments -->|Main stream| FACT_Sales
ODS_Tickets -->|book_ref, passenger_id| FACT_Sales
ODS_Bookings -->|book_date| FACT_Sales
ODS_Flights -->|schedule, route_no| FACT_Sales
ODS_Routes -->|dep/arr airports| FACT_Sales
ODS_Boarding -->|LEFT JOIN seat_no| FACT_Sales
%% Dimensions to Fact
DIM_Calendar -->|calendar_sk| FACT_Sales
DIM_Airports -->|dep/arr _sk| FACT_Sales
DIM_Airplanes -->|airplane_sk| FACT_Sales
DIM_Tariffs -->|tariff_sk| FACT_Sales
DIM_Passengers -->|passenger_sk| FACT_Sales
DIM_Routes -->|route_sk| FACT_Sales
%% 5. DM (Vitrines)
subgraph DM_Layer [Layer: DM Data Marts]
direction TB
DM_Sales[dm.sales_report]:::dm
DM_Traffic[dm.airport_traffic]:::dm
DM_Route[dm.route_performance]:::dm
DM_Monthly[dm.monthly_overview]:::dm
DM_Loyalty[dm.passenger_loyalty]:::dm
end
%% DDS to DM
FACT_Sales --> DM_Sales
FACT_Sales --> DM_Traffic
FACT_Sales --> DM_Route
FACT_Sales --> DM_Monthly
FACT_Sales --> DM_Loyalty
DIM_Airports -.-> DM_Sales
DIM_Airports -.-> DM_Traffic
DIM_Tariffs -.-> DM_Sales
DIM_Tariffs -.-> DM_Loyalty
DIM_Calendar -.-> DM_Sales
DIM_Calendar -.-> DM_Traffic
DIM_Calendar -.-> DM_Route
DIM_Calendar -.-> DM_Monthly
DIM_Calendar -.-> DM_Loyalty
DIM_Routes -.-> DM_Route
DIM_Routes -.-> DM_Monthly
DIM_Routes -.-> DM_Loyalty
DIM_Airplanes -.-> DM_Monthly
DIM_Passengers -.-> DM_Loyalty
```
Для проверки результата пригодится [справочник DQ](../reference/dq_taxonomy.md).
Технические материалы по слоям остаются доступны:
[STG](bookings_stg_design.md), [ODS](bookings_ods_design.md),
[DDS](bookings_dds_design.md), [DM](bookings_dm_design.md).
В них есть планы разработки; текущее поведение проверяйте по SQL,
а требования к самостоятельной работе - по ТЗ.