Руководства STG/ODS/DDS/DM сверены с действующими SQL и DAG, ожидания учебных заглушек связаны с ТЗ. Самопроверка заданий отделена от проверок разработки и исторических планов. Для dim_routes закреплено обновление описательных полей только у открытых версий. SQL, DAG, DDL и настройки стенда не менялись. Проверки: make lint; make test (4 passed, 14 skipped); карта; 292 локальные ссылки; маршрут чтения и карта в Chromium. Живой ETL не запускался.
218 lines
17 KiB
Markdown
218 lines
17 KiB
Markdown
# Как устроено хранилище авиаперевозок
|
||
|
||
Разберем одну поездку: как бронирование связано с билетами и перелетами,
|
||
какие строки попадут в факт и как сложится выручка по направлениям.
|
||
Найти эти строки в структуре хранилища поможет [интерактивная карта](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`.
|
||
Для связи сегмента с талоном понадобятся оба поля ключа, иначе талон
|
||
первого рейса можно ошибочно присоединить ко второму.
|
||
|
||
<a id="как-устроен-поток-целиком"></a>
|
||
|
||
## Путь данных от источника до витрин
|
||
|
||
Оставьте билет выбранным и переключитесь на **"Поток данных"**.
|
||
Таблицы останутся на местах, выбранный билет сохранится; теперь стрелки
|
||
показывают чтение данных загрузчиками. Синий цвет отмечает источники выбранной
|
||
таблицы, зеленый - потребителей, оранжевый - сам выбор. Несвязанные объекты
|
||
остаются видны приглушенными. Повторный выбор, **"Общий вид"** или `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 и проверки раскрываются там же, после выбора таблицы.
|
||
|
||
<a id="как-собирается-факт"></a>
|
||
|
||
## Сборка факта продаж
|
||
|
||
Откройте [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`.
|
||
В этом стенде признак посадки означает именно наличие талона.
|
||
|
||
<a id="откуда-берутся-два-аэропорта"></a>
|
||
|
||
### Аэропорты вылета и прилета
|
||
|
||
В карточке факта откройте связи **"Коды двух аэропортов"**,
|
||
**"Аэропорт вылета"** и **"Аэропорт прилета"**:
|
||
|
||
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).
|
||
|
||
<a id="какая-версия-маршрута-нужна-рейсу"></a>
|
||
|
||
### Версия маршрута на дату рейса
|
||
|
||
Связь **"Версия на дату рейса"** ведет к `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` не появится.
|
||
|
||
<a id="что-кормит-витрины"></a>
|
||
|
||
## Агрегация фактов в витрине продаж
|
||
|
||
Перейдите к [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) ведет к справочникам, проверкам разработки
|
||
и истории проектирования.
|