# Как устроено хранилище авиаперевозок
Разберем одну поездку: как бронирование связано с билетами и перелетами,
какие строки попадут в факт и как сложится выручка по направлениям.
Найти эти строки в структуре хранилища поможет [интерактивная карта](architecture-map.html).
Карта хранит подробности объектов и связей, а это руководство задает маршрут чтения.
## Как открыть карту
Из локальной копии репозитория откройте `docs/design/architecture-map.html`
в браузере: через файловый менеджер или командой из корня проекта:
```bash
xdg-open docs/design/architecture-map.html
```
На Windows можно открыть этот же файл в Проводнике. Сервер, интернет и работающий
Docker для карты не нужны: все ее данные включены в HTML.
Ссылки на SQL, DAG, задание и это руководство открывают файлы в Gitea:
документы — с форматированием, код — с подсветкой синтаксиса.
Для этих переходов нужен интернет. Файлы берутся из текущей ветки репозитория
по умолчанию.
**В 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`.
Для связи сегмента с талоном понадобятся оба поля ключа, иначе талон
первого рейса можно ошибочно присоединить ко второму.
## Путь данных от источника до витрин
Оставьте билет выбранным и переключитесь на **"Поток данных"**.
Таблицы останутся на местах, выбранный билет сохранится; теперь стрелки
показывают чтение данных загрузчиками. Синий цвет отмечает источники выбранной
таблицы, зеленый - потребителей, оранжевый - сам выбор. Несвязанные объекты
остаются видны приглушенными. Повторный выбор, **"Общий вид"** или `Esc`
возвращает обзор.
Проследите путь
[сегмента](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: Сегмент обогащается ключами измерений в факте,
затем участвует в агрегатах витрины.
Стрелка данных не означает очередность задач Airflow. Например, календарь
генерируется без чтения ODS, хотя в DDS DAG задачи других измерений ждут
проверки календаря. Порядок выполнения смотрите по ссылке **"DAG · порядок задач"**
в карточке. Поля, SQL и проверки раскрываются там же, после выбора таблицы.
## Сборка факта продаж
Откройте [dds.fact_flight_sales](architecture-map.html#node=dds.fact_flight_sales).
В нашем примере в нем будет **три строки**, по одной на каждый сегмент.
Важные значения до выполнения студенческих заданий выглядят так:
| 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 |
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).
Зерно меняется: день вылета × два аэропорта × класс обслуживания.
Три строки примера дадут две строки витрины:
| Дата вылета | Направление | Класс | tickets_sold | total_revenue |
|---|---|---|---:|---:|
| 1 сентября | SVO → LED | Economy | 2 | 7000 |
| 1 сентября | LED → KZN | Economy | 1 | 5000 |
`tickets_sold` здесь считает **сегменты**. Один билет может участвовать несколько раз.
Если сложить его по направлениям, получится 3 при двух билетах.
`total_revenue` складывается из цен сегментов, поэтому итог равен 12000.
Доля посадок рассчитывается по `is_boarded`.
Готовая витрина использует календарь, две роли аэропорта и тариф через
`INNER JOIN`: факт без совпадения в одном из этих измерений не попадет в агрегат.
Поля `route_sk`, `airplane_sk`, `passenger_sk` ей не требуются, поэтому
эталон работает до выполнения заданий. Остальные витрины видны рядом
пунктирными рамками: в их карточках описан требуемый результат по ТЗ.
## Модели самолетов и места: начало задания
Откройте [dds.dim_airplanes](architecture-map.html#node=dds.dim_airplanes).
У него два источника: `ods.airplanes` дает характеристики модели,
а `ods.seats` - места, по которым считается вместимость.
Строка измерения описывает **модель**, например тип самолета, а не отдельный борт.
`seat_no` уникален только вместе с `airplane_code`.
На `main` STG уже заполняется, а загрузки `ods.airplanes`, `ods.seats`
и `dds.dim_airplanes` пока содержат `SELECT 1;`. Карточки помечены
**"Задание"** и ведут к соответствующим пунктам ТЗ. Ожидаемые поля
известны из DDL, но готовой загрузки этих объектов еще нет.
## Ключевые договорённости
- Ключи на карте логические. В 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.
Для проверки результата пригодится [справочник 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,
а требования к самостоятельной работе - по ТЗ.