Files
airflow-greenplum/docs/design/db_schema.md
T
ddadmin 35144e49f6 docs: согласованы руководства загрузок и учебный маршрут
Руководства STG/ODS/DDS/DM сверены с действующими SQL и DAG,
ожидания учебных заглушек связаны с ТЗ. Самопроверка заданий
отделена от проверок разработки и исторических планов.

Для dim_routes закреплено обновление описательных полей только
у открытых версий. SQL, DAG, DDL и настройки стенда не менялись.

Проверки: make lint; make test (4 passed, 14 skipped); карта;
292 локальные ссылки; маршрут чтения и карта в Chromium.
Живой ETL не запускался.
2026-09-20 15:53:39 -04:00

17 KiB
Raw Blame History

Как устроено хранилище авиаперевозок

Разберем одну поездку: как бронирование связано с билетами и перелетами, какие строки попадут в факт и как сложится выручка по направлениям. Найти эти строки в структуре хранилища поможет интерактивная карта. Карта хранит подробности объектов и связей, а это руководство задает маршрут чтения.

Как открыть карту

Из локальной копии репозитория откройте docs/design/architecture-map.html в браузере: через файловый менеджер или командой из корня проекта:

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).

Откройте билеты в виде "Связи по ключам". Выделены бронирование, сегменты и связанные с ними талоны. Выберите связь билета с сегментами: в карточке виден ticket_no. Для связи сегмента с талоном понадобятся оба поля ключа, иначе талон первого рейса можно ошибочно присоединить ко второму.

Путь данных от источника до витрин

Оставьте билет выбранным и переключитесь на "Поток данных". Таблицы останутся на местах, выбранный билет сохранится; теперь стрелки показывают чтение данных загрузчиками. Синий цвет отмечает источники выбранной таблицы, зеленый - потребителей, оранжевый - сам выбор. Несвязанные объекты остаются видны приглушенными. Повторный выбор, "Общий вид" или Esc возвращает обзор.

Проследите путь сегмента через 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. В нашем примере в нем будет три строки, по одной на каждый сегмент. Важные значения до выполнения студенческих заданий выглядят так:

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 факта.

Версия маршрута на дату рейса

Связь "Версия на дату рейса" ведет к 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. Зерно меняется: день вылета × два аэропорта × класс обслуживания. Три строки примера дадут две строки витрины:

Дата вылета Направление Класс 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. У него два источника: 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 оно равно времени получения снимка. Подробности - в соглашениях об именах.
  • До реализации измерений допустимы NULL в passenger_sk, route_sk и airplane_sk. Ключи фиксируются при вставке факта: повторный запуск меняет цену и сведения о посадке, но не заполняет старые SK. После выполнения заданий следуйте разделу "Пересчет факта после реализации измерений".
  • В карточке заглушки описан результат из актуального ТЗ. Если ТЗ не определяет тип соединения или поведение при пропуске строки, карта отмечает этот предел, не выдает предположение за работающий SQL.

Куда идти дальше

Для первого задания разберите готовую загрузку справочника: от карты к SQL загрузки, затем откройте первое задание ODS. Для точечного вопроса о таблице возвращайтесь на карту: там находится каталог объектов, их источников, потребителей, SQL и DQ.

Проверяйте задание по инструкции самопроверки. Для разбора ошибки загрузки откройте руководство нужного слоя: STG, ODS, DDS, DM. Индекс документации ведет к справочникам, проверкам разработки и истории проектирования.