# Как устроено хранилище авиаперевозок Разберем одну поездку: как бронирование связано с билетами и перелетами, какие строки попадут в факт и как сложится выручка по направлениям. Найти эти строки в структуре хранилища поможет [интерактивная карта](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#ods-airports), затем откройте [первое задание ODS](../assignment/README.md#первая-загрузка-ods). Для точечного вопроса о таблице возвращайтесь на карту: там находится каталог объектов, их источников, потребителей, SQL и DQ. Проверяйте задание по [инструкции самопроверки](../assignment/README.md#запуск-и-проверка). Для разбора ошибки загрузки откройте руководство нужного слоя: [STG](../bookings_to_gp_stage.md), [ODS](../bookings_to_gp_ods.md), [DDS](../bookings_to_gp_dds.md), [DM](../bookings_to_gp_dm.md). [Индекс документации](../README.md) ведет к справочникам, проверкам разработки и истории проектирования.