# Как устроено хранилище авиаперевозок Разберем одну поездку: как бронирование связано с билетами и перелетами, какие строки попадут в факт и как сложится выручка по направлениям. Найти эти строки в структуре хранилища поможет [интерактивная карта](architecture-map.html). Карта хранит подробности объектов и связей, а это руководство задает маршрут чтения. ## Как открыть карту Из локальной копии репозитория откройте `docs/design/architecture-map.html` в браузере: через файловый менеджер или командой из корня проекта: ```bash xdg-open docs/design/architecture-map.html ``` На Windows можно открыть этот же файл в Проводнике. Сервер, интернет и работающий Docker для карты не нужны. Храните HTML рядом с остальными файлами репозитория: ссылки из карточек ведут в локальные SQL и Markdown. Браузер может показать Markdown как текст, а SQL предложить скачать. Скачанный файл можно открыть в редакторе; для форматированного чтения руководства используйте Markdown preview. **В 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, а требования к самостоятельной работе - по ТЗ.