diff --git a/README.md b/README.md
index bed9180..10fe0a7 100644
--- a/README.md
+++ b/README.md
@@ -96,11 +96,15 @@ SELECT COUNT(*) FROM dm.sales_report;
Подробнее про логику DAG и проверки — `docs/bookings_to_gp_stage.md`.
-7) Приступайте к заданию:
+7) Разберитесь в данных и приступайте к заданию:
-Стенд работает, данные загружены. Переходите к
-[учебным заданиям](docs/assignment/README.md) — там описан путь
-от изучения эталона до полной реализации DWH.
+Стенд работает, данные загружены. Откройте
+[руководство о структуре DWH](docs/design/db_schema.md): на примере поездки
+оно проведет от билетов и сегментов к факту и витрине. Рядом держите
+[интерактивную карту](docs/design/architecture-map.html) — локальный файл
+`docs/design/architecture-map.html` нужно открыть в браузере; в Gitea HTML
+показывается как исходный текст. Затем переходите к
+[учебным заданиям](docs/assignment/README.md).
## DAG-и в стенде
@@ -151,7 +155,8 @@ make clean # полный reset: удалить контейнер
## Документация
- [Учебные задания](docs/assignment/README.md)
-- [Схема DWH: четыре уровня приближения](docs/design/db_schema.md)
+- [Структура DWH: от бронирования до витрины](docs/design/db_schema.md)
+- [Интерактивная карта таблиц и связей](docs/design/architecture-map.html)
- [Как читать пайплайн по скрипту загрузки](docs/design/reading_the_pipeline.md)
- [План тестирования/проверок и негативные кейсы](TESTING.md)
- [Дополнительные заметки и технические детали](docs/README.md)
diff --git a/docs/README.md b/docs/README.md
index 6808ab1..04ecee3 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -5,6 +5,8 @@
## Быстрый путь (для менти)
- [Быстрый старт и команды](../README.md)
+- [Структура DWH: от бронирования до витрины](design/db_schema.md)
+- [Интерактивная карта таблиц и связей](design/architecture-map.html)
- [Учебные задания](assignment/README.md)
- [Как читать пайплайн по скрипту загрузки](design/reading_the_pipeline.md)
- [План тестирования и проверки](../TESTING.md)
@@ -13,10 +15,14 @@
- [Учебный DAG: ods -> dds](bookings_to_gp_dds.md)
- [Учебный DAG: dds -> dm](bookings_to_gp_dm.md)
+Карту открывайте из локальной копии: `docs/design/architecture-map.html` в браузере.
+Сервер не нужен. Просмотр исходного HTML в Gitea не показывает карту.
+[Руководство](design/db_schema.md#как-открыть-карту) объясняет открытие и адресные ссылки.
+
## Дизайн (`design/`)
- [Единые конвенции нейминга DWH (служебные поля и SCD)](design/naming_conventions.md)
-- [Схема БД всех слоёв DWH](design/db_schema.md)
+- [Структура DWH и маршрут по карте](design/db_schema.md)
- [Как читать пайплайн по скрипту загрузки](design/reading_the_pipeline.md)
- [Дизайн-документ STG](design/bookings_stg_design.md)
- [Дизайн-документ ODS](design/bookings_ods_design.md)
diff --git a/docs/assignment/README.md b/docs/assignment/README.md
index d608434..d779a8f 100644
--- a/docs/assignment/README.md
+++ b/docs/assignment/README.md
@@ -11,7 +11,7 @@
## С чего начать чтение
-Сначала посмотрите [схему DWH по уровням приближения](../design/db_schema.md).
+Сначала посмотрите [руководство о структуре DWH и карту](../design/db_schema.md).
Пока пишете свой слой, держите рядом [метод чтения скрипта загрузки](../design/reading_the_pipeline.md).
Для `*_dq.sql` сверяйтесь с [классами DQ-проверок](../reference/dq_taxonomy.md).
diff --git a/docs/design/architecture-map.html b/docs/design/architecture-map.html
new file mode 100644
index 0000000..92f577b
--- /dev/null
+++ b/docs/design/architecture-map.html
@@ -0,0 +1,5197 @@
+
+
+
+
+
+
+ Карта DWH авиаперевозок
+
+
+
+
+
+
+
+
+ Сплошная рамка: готово Пунктирная рамка: задание
+ Синий: источники Зеленый: потребители Оранжевый: выбор
+
+
+
+
+
+
+
+
+ Для интерактивной карты включите JavaScript. Объяснение модели доступно в руководстве .
+
+
+
diff --git a/docs/design/architecture-map/README.md b/docs/design/architecture-map/README.md
new file mode 100644
index 0000000..0107994
--- /dev/null
+++ b/docs/design/architecture-map/README.md
@@ -0,0 +1,60 @@
+# Исходники карты DWH
+
+Готовый файл - [../architecture-map.html](../architecture-map.html).
+Откройте его локально в браузере. Сервер и интернет не требуются;
+SQL и Markdown открываются из соседних каталогов репозитория.
+Учебный маршрут находится в [руководстве о структуре](../db_schema.md).
+
+## Редактирование и сборка
+
+- `map.json` - единственный каталог объектов, полей, связей и текстов карточек.
+- `template.html` - интерфейс. SVG, карточки, постоянная раскладка и выделение
+ адаптированы из `clickstream-data-platform/docs/handbook/architecture-map/template.html`.
+- `render.py` - сборка автономного HTML и статические проверки. Нужны uv и Python
+ 3.11 или новее; пакеты Python и JavaScript не требуются. JSON заменяет YAML
+ образца, чтобы сборка обходилась стандартной библиотекой.
+
+Из корня репозитория:
+
+```bash
+uv run docs/design/architecture-map/render.py
+uv run docs/design/architecture-map/render.py --check
+```
+
+Правьте исходники, затем пересобирайте HTML. `--check` сверяет:
+имена объектов с DDL/PXF, важные поля и поля связей, статусы загрузок и DQ,
+входы готовых SQL со стрелками потока, файлы и разделы по ссылкам,
+уникальность идентификаторов и позиций, свежесть HTML.
+Проверка входов учитывает именованные таблицы в FROM/JOIN;
+смысл соединений нужно сверять при изменении SQL вручную.
+
+Внешние `stg.*_ext` представлены в карточках STG, поскольку это входы чтения,
+а не еще одна копия данных. Поля источника сверяются с контрактом PXF.
+Для заглушек используются DDL и `docs/assignment/analyst_spec.md`.
+Новые предположения о поведении загрузок в карточки не добавляйте.
+
+## Адреса объектов
+
+`../architecture-map.html#node=ods.airplanes` выбирает таблицу.
+Дополнение `&view=keys` включает связи по ключам; без него показан поток данных.
+Идентификатор равен полному имени таблицы и не зависит от позиции в карте.
+Неизвестный адрес возвращает обзор с сообщением. Назад/вперед в браузере
+восстанавливают предыдущий выбор и вид связей.
+
+## Проверка в браузере
+
+Статические проверки не заменяют этот проход:
+
+1. Откройте готовый HTML с диска. Убедитесь, что видны пять слоев,
+ имена таблиц и пометки заданий; ошибок JavaScript нет.
+2. Выберите `bookings.tickets`, затем `ods.segments` и `dds.fact_flight_sales`.
+ Проверьте источники, потребителей и карточки связей. Снимите выделение
+ повторным выбором, кнопкой "Общий вид" и клавишей Esc.
+3. Для выбранных билета, факта и `dds.dim_airplanes` переключите оба вида:
+ выбор, позиции и прокрутка карты должны сохраниться.
+4. У факта откройте обе роли `dim_airports`, путь через `ods.routes`
+ и выбор версии `dim_routes`. Проверьте отдельные карточки связей.
+5. Откройте адрес объекта, обновите страницу, проверьте назад/вперед.
+ Откройте DDL, загрузку, DQ и пункт задания из карточки.
+6. Проверьте выбор клавишами Enter/пробел и вид на узком экране.
+ На узком экране карта прокручивается, карточка находится под ней.
diff --git a/docs/design/architecture-map/map.json b/docs/design/architecture-map/map.json
new file mode 100644
index 0000000..ca16d32
--- /dev/null
+++ b/docs/design/architecture-map/map.json
@@ -0,0 +1,4749 @@
+{
+ "title": "Хранилище авиаперевозок",
+ "lead": "От бронирования до витрины: выберите таблицу или связь.",
+ "layers": [
+ {
+ "id": "source",
+ "title": "Источник",
+ "caption": "Postgres · demo"
+ },
+ {
+ "id": "stg",
+ "title": "STG",
+ "caption": "Сырые строки · Greenplum"
+ },
+ {
+ "id": "ods",
+ "title": "ODS",
+ "caption": "Типы и текущее состояние"
+ },
+ {
+ "id": "dds",
+ "title": "DDS",
+ "caption": "Измерения и факт"
+ },
+ {
+ "id": "dm",
+ "title": "DM",
+ "caption": "Ответы на бизнес-вопросы"
+ }
+ ],
+ "nodes": [
+ {
+ "id": "bookings.bookings",
+ "name": "bookings.bookings",
+ "layer": "source",
+ "order": 0,
+ "what": "Бронирование объединяет билеты одной покупки.",
+ "grain": "Одно бронирование.",
+ "keys": "`book_ref` — логический ключ строки источника.",
+ "fields": [
+ {
+ "name": "book_ref",
+ "meaning": "номер бронирования"
+ },
+ {
+ "name": "book_date",
+ "meaning": "момент бронирования"
+ },
+ {
+ "name": "total_amount",
+ "meaning": "сумма бронирования целиком"
+ }
+ ],
+ "fill": "Демо-БД восстанавливается из seed-дампа; генератор добавляет учебные дни. PXF читает ее через `stg.bookings_ext`.",
+ "status": "source",
+ "links": [
+ {
+ "label": "Источник и генератор",
+ "url": "../../bookings/README.md"
+ },
+ {
+ "label": "DDL входа PXF",
+ "url": "../../sql/stg/bookings_ddl.sql"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "stg.bookings",
+ "name": "stg.bookings",
+ "layer": "stg",
+ "order": 0,
+ "what": "Бронирование объединяет билеты одной покупки. Здесь бизнес-поля хранятся как TEXT.",
+ "grain": "Одно бронирование. Строка относится к загрузке STG.",
+ "keys": "Бизнес-ключ: `book_ref`. Для хранимой строки учитывайте также `_load_id`: снимки разных запусков сосуществуют.",
+ "fields": [
+ {
+ "name": "book_ref",
+ "meaning": "номер бронирования"
+ },
+ {
+ "name": "book_date",
+ "meaning": "момент бронирования"
+ },
+ {
+ "name": "total_amount",
+ "meaning": "сумма бронирования целиком"
+ },
+ {
+ "name": "event_ts",
+ "meaning": "опорное время инкремента"
+ },
+ {
+ "name": "_load_id",
+ "meaning": "идентификатор загрузки"
+ },
+ {
+ "name": "_load_ts",
+ "meaning": "время записи в STG"
+ }
+ ],
+ "fill": "Добавление строк по `event_ts` предыдущих загрузок. Опорное время — `book_date` бронирования. Повтор защищен ключом строки внутри `_load_id`.",
+ "status": "ready",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/stg/bookings_ddl.sql"
+ },
+ {
+ "label": "Загрузка",
+ "url": "../../sql/stg/bookings_load.sql"
+ },
+ {
+ "label": "DQ",
+ "url": "../../sql/stg/bookings_dq.sql"
+ }
+ ],
+ "parts": [
+ "stg.bookings_ext"
+ ]
+ },
+ {
+ "id": "ods.bookings",
+ "name": "ods.bookings",
+ "layer": "ods",
+ "order": 0,
+ "what": "Бронирование объединяет билеты одной покупки.",
+ "grain": "Одно бронирование.",
+ "keys": "`book_ref` — логический ключ текущей строки.",
+ "fields": [
+ {
+ "name": "book_ref",
+ "meaning": "номер бронирования"
+ },
+ {
+ "name": "book_date",
+ "meaning": "момент бронирования"
+ },
+ {
+ "name": "total_amount",
+ "meaning": "сумма бронирования целиком"
+ }
+ ],
+ "fill": "Дельта из STG по `_load_ts`, приведение типов, выбор последней строки по бизнес-ключу; UPDATE изменившихся и INSERT новых. Время и идентификатор загрузки сохраняются из STG.",
+ "status": "ready",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/ods/bookings_ddl.sql"
+ },
+ {
+ "label": "Загрузка",
+ "url": "../../sql/ods/bookings_load.sql"
+ },
+ {
+ "label": "DQ",
+ "url": "../../sql/ods/bookings_dq.sql"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "bookings.tickets",
+ "name": "bookings.tickets",
+ "layer": "source",
+ "order": 1,
+ "what": "Билет связывает пассажира с бронированием. В билете может быть несколько сегментов.",
+ "grain": "Один билет пассажира.",
+ "keys": "`ticket_no` — логический ключ строки источника.",
+ "fields": [
+ {
+ "name": "ticket_no",
+ "meaning": "номер билета"
+ },
+ {
+ "name": "book_ref",
+ "meaning": "бронирование"
+ },
+ {
+ "name": "passenger_id",
+ "meaning": "идентификатор пассажира"
+ },
+ {
+ "name": "passenger_name",
+ "meaning": "имя пассажира"
+ },
+ {
+ "name": "outbound",
+ "meaning": "признак направления поездки"
+ }
+ ],
+ "fill": "Демо-БД восстанавливается из seed-дампа; генератор добавляет учебные дни. PXF читает ее через `stg.tickets_ext`.",
+ "status": "source",
+ "links": [
+ {
+ "label": "Источник и генератор",
+ "url": "../../bookings/README.md"
+ },
+ {
+ "label": "DDL входа PXF",
+ "url": "../../sql/stg/tickets_ddl.sql"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "stg.tickets",
+ "name": "stg.tickets",
+ "layer": "stg",
+ "order": 1,
+ "what": "Билет связывает пассажира с бронированием. В билете может быть несколько сегментов. Здесь бизнес-поля хранятся как TEXT.",
+ "grain": "Один билет пассажира. Строка относится к загрузке STG.",
+ "keys": "Бизнес-ключ: `ticket_no`. Загрузчик исключает повторный `ticket_no` во всей таблице.",
+ "fields": [
+ {
+ "name": "ticket_no",
+ "meaning": "номер билета"
+ },
+ {
+ "name": "book_ref",
+ "meaning": "бронирование"
+ },
+ {
+ "name": "passenger_id",
+ "meaning": "идентификатор пассажира"
+ },
+ {
+ "name": "passenger_name",
+ "meaning": "имя пассажира"
+ },
+ {
+ "name": "outbound",
+ "meaning": "признак направления поездки"
+ },
+ {
+ "name": "event_ts",
+ "meaning": "опорное время инкремента"
+ },
+ {
+ "name": "_load_id",
+ "meaning": "идентификатор загрузки"
+ },
+ {
+ "name": "_load_ts",
+ "meaning": "время записи в STG"
+ }
+ ],
+ "fill": "Добавление строк по `event_ts` предыдущих загрузок. Опорное время — `book_date` бронирования. Уже сохраненный `ticket_no` повторно не вставляется.",
+ "status": "ready",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/stg/tickets_ddl.sql"
+ },
+ {
+ "label": "Загрузка",
+ "url": "../../sql/stg/tickets_load.sql"
+ },
+ {
+ "label": "DQ",
+ "url": "../../sql/stg/tickets_dq.sql"
+ }
+ ],
+ "parts": [
+ "stg.tickets_ext"
+ ]
+ },
+ {
+ "id": "ods.tickets",
+ "name": "ods.tickets",
+ "layer": "ods",
+ "order": 1,
+ "what": "Билет связывает пассажира с бронированием. В билете может быть несколько сегментов.",
+ "grain": "Один билет пассажира.",
+ "keys": "`ticket_no` — логический ключ текущей строки.",
+ "fields": [
+ {
+ "name": "ticket_no",
+ "meaning": "номер билета"
+ },
+ {
+ "name": "book_ref",
+ "meaning": "бронирование"
+ },
+ {
+ "name": "passenger_id",
+ "meaning": "идентификатор пассажира"
+ },
+ {
+ "name": "passenger_name",
+ "meaning": "имя пассажира"
+ },
+ {
+ "name": "is_outbound",
+ "meaning": "признак направления поездки"
+ }
+ ],
+ "fill": "Дельта из STG по `_load_ts`, приведение типов, выбор последней строки по бизнес-ключу; UPDATE изменившихся и INSERT новых. Время и идентификатор загрузки сохраняются из STG. `outbound` становится `is_outbound` (BOOLEAN).",
+ "status": "ready",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/ods/tickets_ddl.sql"
+ },
+ {
+ "label": "Загрузка",
+ "url": "../../sql/ods/tickets_load.sql"
+ },
+ {
+ "label": "DQ",
+ "url": "../../sql/ods/tickets_dq.sql"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "bookings.segments",
+ "name": "bookings.segments",
+ "layer": "source",
+ "order": 2,
+ "what": "Сегмент связывает билет с конкретным рейсом и хранит цену этого перелета.",
+ "grain": "Один перелет по одному билету.",
+ "keys": "`ticket_no` + `flight_id` — логический ключ строки источника.",
+ "fields": [
+ {
+ "name": "ticket_no",
+ "meaning": "билет"
+ },
+ {
+ "name": "flight_id",
+ "meaning": "рейс"
+ },
+ {
+ "name": "fare_conditions",
+ "meaning": "класс обслуживания"
+ },
+ {
+ "name": "price",
+ "meaning": "стоимость сегмента"
+ }
+ ],
+ "fill": "Демо-БД восстанавливается из seed-дампа; генератор добавляет учебные дни. PXF читает ее через `stg.segments_ext`.",
+ "status": "source",
+ "links": [
+ {
+ "label": "Источник и генератор",
+ "url": "../../bookings/README.md"
+ },
+ {
+ "label": "DDL входа PXF",
+ "url": "../../sql/stg/segments_ddl.sql"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "stg.segments",
+ "name": "stg.segments",
+ "layer": "stg",
+ "order": 2,
+ "what": "Сегмент связывает билет с конкретным рейсом и хранит цену этого перелета. Здесь бизнес-поля хранятся как TEXT.",
+ "grain": "Один перелет по одному билету. Строка относится к загрузке STG.",
+ "keys": "Бизнес-ключ: `ticket_no` + `flight_id`. Для хранимой строки учитывайте также `_load_id`: снимки разных запусков сосуществуют.",
+ "fields": [
+ {
+ "name": "ticket_no",
+ "meaning": "билет"
+ },
+ {
+ "name": "flight_id",
+ "meaning": "рейс"
+ },
+ {
+ "name": "fare_conditions",
+ "meaning": "класс обслуживания"
+ },
+ {
+ "name": "price",
+ "meaning": "стоимость сегмента"
+ },
+ {
+ "name": "event_ts",
+ "meaning": "опорное время инкремента"
+ },
+ {
+ "name": "_load_id",
+ "meaning": "идентификатор загрузки"
+ },
+ {
+ "name": "_load_ts",
+ "meaning": "время записи в STG"
+ }
+ ],
+ "fill": "Добавление строк по `event_ts` предыдущих загрузок. Опорное время — `book_date` бронирования. Повтор защищен ключом строки внутри `_load_id`.",
+ "status": "ready",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/stg/segments_ddl.sql"
+ },
+ {
+ "label": "Загрузка",
+ "url": "../../sql/stg/segments_load.sql"
+ },
+ {
+ "label": "DQ",
+ "url": "../../sql/stg/segments_dq.sql"
+ }
+ ],
+ "parts": [
+ "stg.segments_ext"
+ ]
+ },
+ {
+ "id": "ods.segments",
+ "name": "ods.segments",
+ "layer": "ods",
+ "order": 2,
+ "what": "Сегмент связывает билет с конкретным рейсом и хранит цену этого перелета.",
+ "grain": "Один перелет по одному билету.",
+ "keys": "`ticket_no` + `flight_id` — логический ключ текущей строки.",
+ "fields": [
+ {
+ "name": "ticket_no",
+ "meaning": "билет"
+ },
+ {
+ "name": "flight_id",
+ "meaning": "рейс"
+ },
+ {
+ "name": "fare_conditions",
+ "meaning": "класс обслуживания"
+ },
+ {
+ "name": "amount",
+ "meaning": "стоимость сегмента"
+ }
+ ],
+ "fill": "Дельта из STG по `_load_ts`, приведение типов, выбор последней строки по бизнес-ключу; UPDATE изменившихся и INSERT новых. Время и идентификатор загрузки сохраняются из STG. `stg.segments.price` становится `amount` (NUMERIC).",
+ "status": "ready",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/ods/segments_ddl.sql"
+ },
+ {
+ "label": "Загрузка",
+ "url": "../../sql/ods/segments_load.sql"
+ },
+ {
+ "label": "DQ",
+ "url": "../../sql/ods/segments_dq.sql"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "bookings.flights",
+ "name": "bookings.flights",
+ "layer": "source",
+ "order": 3,
+ "what": "Рейс представляет конкретный вылет по маршруту. Один маршрут выполняется многократно.",
+ "grain": "Один рейс с собственным расписанием и статусом.",
+ "keys": "`flight_id` — логический ключ строки источника.",
+ "fields": [
+ {
+ "name": "flight_id",
+ "meaning": "идентификатор рейса"
+ },
+ {
+ "name": "route_no",
+ "meaning": "маршрут"
+ },
+ {
+ "name": "scheduled_departure",
+ "meaning": "плановый момент вылета"
+ },
+ {
+ "name": "scheduled_arrival",
+ "meaning": "плановый момент прилета"
+ },
+ {
+ "name": "status",
+ "meaning": "состояние рейса"
+ }
+ ],
+ "fill": "Демо-БД восстанавливается из seed-дампа; генератор добавляет учебные дни. PXF читает ее через `stg.flights_ext`.",
+ "status": "source",
+ "links": [
+ {
+ "label": "Источник и генератор",
+ "url": "../../bookings/README.md"
+ },
+ {
+ "label": "DDL входа PXF",
+ "url": "../../sql/stg/flights_ddl.sql"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "stg.flights",
+ "name": "stg.flights",
+ "layer": "stg",
+ "order": 3,
+ "what": "Рейс представляет конкретный вылет по маршруту. Один маршрут выполняется многократно. Здесь бизнес-поля хранятся как TEXT.",
+ "grain": "Один рейс с собственным расписанием и статусом. Строка относится к загрузке STG.",
+ "keys": "Бизнес-ключ: `flight_id`. Для хранимой строки учитывайте также `_load_id`: снимки разных запусков сосуществуют.",
+ "fields": [
+ {
+ "name": "flight_id",
+ "meaning": "идентификатор рейса"
+ },
+ {
+ "name": "route_no",
+ "meaning": "маршрут"
+ },
+ {
+ "name": "scheduled_departure",
+ "meaning": "плановый момент вылета"
+ },
+ {
+ "name": "scheduled_arrival",
+ "meaning": "плановый момент прилета"
+ },
+ {
+ "name": "status",
+ "meaning": "состояние рейса"
+ },
+ {
+ "name": "event_ts",
+ "meaning": "опорное время инкремента"
+ },
+ {
+ "name": "_load_id",
+ "meaning": "идентификатор загрузки"
+ },
+ {
+ "name": "_load_ts",
+ "meaning": "время записи в STG"
+ }
+ ],
+ "fill": "Добавление строк по `event_ts` предыдущих загрузок. Опорное время — `scheduled_departure`. Повтор защищен ключом строки внутри `_load_id`.",
+ "status": "ready",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/stg/flights_ddl.sql"
+ },
+ {
+ "label": "Загрузка",
+ "url": "../../sql/stg/flights_load.sql"
+ },
+ {
+ "label": "DQ",
+ "url": "../../sql/stg/flights_dq.sql"
+ }
+ ],
+ "parts": [
+ "stg.flights_ext"
+ ]
+ },
+ {
+ "id": "ods.flights",
+ "name": "ods.flights",
+ "layer": "ods",
+ "order": 3,
+ "what": "Рейс представляет конкретный вылет по маршруту. Один маршрут выполняется многократно.",
+ "grain": "Один рейс с собственным расписанием и статусом.",
+ "keys": "`flight_id` — логический ключ текущей строки.",
+ "fields": [
+ {
+ "name": "flight_id",
+ "meaning": "идентификатор рейса"
+ },
+ {
+ "name": "route_no",
+ "meaning": "маршрут"
+ },
+ {
+ "name": "scheduled_departure",
+ "meaning": "плановый момент вылета"
+ },
+ {
+ "name": "scheduled_arrival",
+ "meaning": "плановый момент прилета"
+ },
+ {
+ "name": "status",
+ "meaning": "состояние рейса"
+ }
+ ],
+ "fill": "Дельта из STG по `_load_ts`, приведение типов, выбор последней строки по бизнес-ключу; UPDATE изменившихся и INSERT новых. Время и идентификатор загрузки сохраняются из STG. Дополнительно берутся рейсы из истории STG, на которые ссылаются новые `stg.segments`.",
+ "status": "ready",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/ods/flights_ddl.sql"
+ },
+ {
+ "label": "Загрузка",
+ "url": "../../sql/ods/flights_load.sql"
+ },
+ {
+ "label": "DQ",
+ "url": "../../sql/ods/flights_dq.sql"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "bookings.boarding_passes",
+ "name": "bookings.boarding_passes",
+ "layer": "source",
+ "order": 4,
+ "what": "Посадочный талон относится к сегменту билета. Его наличие используется как признак посадки в факте.",
+ "grain": "Один посадочный талон на билет и рейс.",
+ "keys": "`ticket_no` + `flight_id` — логический ключ строки источника.",
+ "fields": [
+ {
+ "name": "ticket_no",
+ "meaning": "билет"
+ },
+ {
+ "name": "flight_id",
+ "meaning": "рейс"
+ },
+ {
+ "name": "seat_no",
+ "meaning": "место в салоне"
+ },
+ {
+ "name": "boarding_no",
+ "meaning": "номер посадочного талона"
+ },
+ {
+ "name": "boarding_time",
+ "meaning": "время посадки"
+ }
+ ],
+ "fill": "Демо-БД восстанавливается из seed-дампа; генератор добавляет учебные дни. PXF читает ее через `stg.boarding_passes_ext`.",
+ "status": "source",
+ "links": [
+ {
+ "label": "Источник и генератор",
+ "url": "../../bookings/README.md"
+ },
+ {
+ "label": "DDL входа PXF",
+ "url": "../../sql/stg/boarding_passes_ddl.sql"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "stg.boarding_passes",
+ "name": "stg.boarding_passes",
+ "layer": "stg",
+ "order": 4,
+ "what": "Посадочный талон относится к сегменту билета. Его наличие используется как признак посадки в факте. Здесь бизнес-поля хранятся как TEXT.",
+ "grain": "Один посадочный талон на билет и рейс. Строка относится к загрузке STG.",
+ "keys": "Бизнес-ключ: `ticket_no` + `flight_id`. Для хранимой строки учитывайте также `_load_id`: снимки разных запусков сосуществуют.",
+ "fields": [
+ {
+ "name": "ticket_no",
+ "meaning": "билет"
+ },
+ {
+ "name": "flight_id",
+ "meaning": "рейс"
+ },
+ {
+ "name": "seat_no",
+ "meaning": "место в салоне"
+ },
+ {
+ "name": "boarding_no",
+ "meaning": "номер посадочного талона"
+ },
+ {
+ "name": "boarding_time",
+ "meaning": "время посадки"
+ },
+ {
+ "name": "event_ts",
+ "meaning": "опорное время инкремента"
+ },
+ {
+ "name": "_load_id",
+ "meaning": "идентификатор загрузки"
+ },
+ {
+ "name": "_load_ts",
+ "meaning": "время записи в STG"
+ }
+ ],
+ "fill": "Добавление строк по `event_ts` предыдущих загрузок. Опорное время — `book_date` бронирования. Повтор защищен ключом строки внутри `_load_id`.",
+ "status": "ready",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/stg/boarding_passes_ddl.sql"
+ },
+ {
+ "label": "Загрузка",
+ "url": "../../sql/stg/boarding_passes_load.sql"
+ },
+ {
+ "label": "DQ",
+ "url": "../../sql/stg/boarding_passes_dq.sql"
+ }
+ ],
+ "parts": [
+ "stg.boarding_passes_ext"
+ ]
+ },
+ {
+ "id": "ods.boarding_passes",
+ "name": "ods.boarding_passes",
+ "layer": "ods",
+ "order": 4,
+ "what": "Посадочный талон относится к сегменту билета. Его наличие используется как признак посадки в факте.",
+ "grain": "Один посадочный талон на билет и рейс.",
+ "keys": "`ticket_no` + `flight_id` — логический ключ текущей строки.",
+ "fields": [
+ {
+ "name": "ticket_no",
+ "meaning": "билет"
+ },
+ {
+ "name": "flight_id",
+ "meaning": "рейс"
+ },
+ {
+ "name": "seat_no",
+ "meaning": "место в салоне"
+ },
+ {
+ "name": "boarding_no",
+ "meaning": "номер посадочного талона"
+ },
+ {
+ "name": "boarding_time",
+ "meaning": "время посадки"
+ }
+ ],
+ "fill": "Дельта из STG по `_load_ts`, приведение типов, выбор последней строки по бизнес-ключу; UPDATE изменившихся и INSERT новых. Время и идентификатор загрузки сохраняются из STG.",
+ "status": "ready",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/ods/boarding_passes_ddl.sql"
+ },
+ {
+ "label": "Загрузка",
+ "url": "../../sql/ods/boarding_passes_load.sql"
+ },
+ {
+ "label": "DQ",
+ "url": "../../sql/ods/boarding_passes_dq.sql"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "bookings.routes",
+ "name": "bookings.routes",
+ "layer": "source",
+ "order": 5,
+ "what": "Маршрут задает аэропорты, модель самолета и расписание на период действия.",
+ "grain": "Один вариант расписания маршрута.",
+ "keys": "`route_no` + `validity` — логический ключ строки источника.",
+ "fields": [
+ {
+ "name": "route_no",
+ "meaning": "номер маршрута"
+ },
+ {
+ "name": "validity",
+ "meaning": "период действия исходного расписания"
+ },
+ {
+ "name": "departure_airport",
+ "meaning": "код аэропорта вылета"
+ },
+ {
+ "name": "arrival_airport",
+ "meaning": "код аэропорта прилета"
+ },
+ {
+ "name": "airplane_code",
+ "meaning": "код модели самолета"
+ },
+ {
+ "name": "scheduled_time",
+ "meaning": "время вылета по расписанию"
+ }
+ ],
+ "fill": "Демо-БД восстанавливается из seed-дампа; генератор добавляет учебные дни. PXF читает ее через `stg.routes_ext`.",
+ "status": "source",
+ "links": [
+ {
+ "label": "Источник и генератор",
+ "url": "../../bookings/README.md"
+ },
+ {
+ "label": "DDL входа PXF",
+ "url": "../../sql/stg/routes_ddl.sql"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "stg.routes",
+ "name": "stg.routes",
+ "layer": "stg",
+ "order": 5,
+ "what": "Маршрут задает аэропорты, модель самолета и расписание на период действия. Здесь бизнес-поля хранятся как TEXT.",
+ "grain": "Один вариант расписания маршрута. Строка относится к загрузке STG.",
+ "keys": "Бизнес-ключ: `route_no` + `validity`. Для хранимой строки учитывайте также `_load_id`: снимки разных запусков сосуществуют.",
+ "fields": [
+ {
+ "name": "route_no",
+ "meaning": "номер маршрута"
+ },
+ {
+ "name": "validity",
+ "meaning": "период действия исходного расписания"
+ },
+ {
+ "name": "departure_airport",
+ "meaning": "код аэропорта вылета"
+ },
+ {
+ "name": "arrival_airport",
+ "meaning": "код аэропорта прилета"
+ },
+ {
+ "name": "airplane_code",
+ "meaning": "код модели самолета"
+ },
+ {
+ "name": "scheduled_time",
+ "meaning": "время вылета по расписанию"
+ },
+ {
+ "name": "event_ts",
+ "meaning": "время снимка (`now()`)"
+ },
+ {
+ "name": "_load_id",
+ "meaning": "идентификатор загрузки"
+ },
+ {
+ "name": "_load_ts",
+ "meaning": "время записи в STG"
+ }
+ ],
+ "fill": "Полный снимок при каждом новом запуске STG; повтор в том же `_load_id` не добавляет строку повторно.",
+ "status": "ready",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/stg/routes_ddl.sql"
+ },
+ {
+ "label": "Загрузка",
+ "url": "../../sql/stg/routes_load.sql"
+ },
+ {
+ "label": "DQ",
+ "url": "../../sql/stg/routes_dq.sql"
+ }
+ ],
+ "parts": [
+ "stg.routes_ext"
+ ]
+ },
+ {
+ "id": "ods.routes",
+ "name": "ods.routes",
+ "layer": "ods",
+ "order": 5,
+ "what": "Маршрут задает аэропорты, модель самолета и расписание на период действия.",
+ "grain": "Один вариант расписания маршрута.",
+ "keys": "`route_no` + `validity` — логический ключ текущей строки.",
+ "fields": [
+ {
+ "name": "route_no",
+ "meaning": "номер маршрута"
+ },
+ {
+ "name": "validity",
+ "meaning": "период действия исходного расписания"
+ },
+ {
+ "name": "departure_airport",
+ "meaning": "код аэропорта вылета"
+ },
+ {
+ "name": "arrival_airport",
+ "meaning": "код аэропорта прилета"
+ },
+ {
+ "name": "airplane_code",
+ "meaning": "код модели самолета"
+ },
+ {
+ "name": "departure_time",
+ "meaning": "время вылета по расписанию"
+ }
+ ],
+ "fill": "Полный снимок из согласованного батча STG (`stg_batch_id`): TRUNCATE + INSERT, одна строка на бизнес-ключ. `scheduled_time` становится `departure_time` (TIME), `days_of_week` — INTEGER[], `duration` — INTERVAL.",
+ "status": "ready",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/ods/routes_ddl.sql"
+ },
+ {
+ "label": "Загрузка",
+ "url": "../../sql/ods/routes_load.sql"
+ },
+ {
+ "label": "DQ",
+ "url": "../../sql/ods/routes_dq.sql"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "bookings.airports_data",
+ "name": "bookings.airports_data",
+ "layer": "source",
+ "order": 6,
+ "what": "Справочник аэропортов. Один аэропорт может участвовать в разных рейсах как место вылета или прилета.",
+ "grain": "Один аэропорт.",
+ "keys": "`airport_code` — логический ключ строки источника.",
+ "fields": [
+ {
+ "name": "airport_code",
+ "meaning": "код аэропорта"
+ },
+ {
+ "name": "airport_name",
+ "meaning": "название"
+ },
+ {
+ "name": "city",
+ "meaning": "город"
+ },
+ {
+ "name": "timezone",
+ "meaning": "часовой пояс"
+ }
+ ],
+ "fill": "Демо-БД восстанавливается из seed-дампа; генератор добавляет учебные дни. PXF читает ее через `stg.airports_ext`.",
+ "status": "source",
+ "links": [
+ {
+ "label": "Источник и генератор",
+ "url": "../../bookings/README.md"
+ },
+ {
+ "label": "DDL входа PXF",
+ "url": "../../sql/stg/airports_ddl.sql"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "stg.airports",
+ "name": "stg.airports",
+ "layer": "stg",
+ "order": 6,
+ "what": "Справочник аэропортов. Один аэропорт может участвовать в разных рейсах как место вылета или прилета. Здесь бизнес-поля хранятся как TEXT.",
+ "grain": "Один аэропорт. Строка относится к загрузке STG.",
+ "keys": "Бизнес-ключ: `airport_code`. Для хранимой строки учитывайте также `_load_id`: снимки разных запусков сосуществуют.",
+ "fields": [
+ {
+ "name": "airport_code",
+ "meaning": "код аэропорта"
+ },
+ {
+ "name": "airport_name",
+ "meaning": "название"
+ },
+ {
+ "name": "city",
+ "meaning": "город"
+ },
+ {
+ "name": "timezone",
+ "meaning": "часовой пояс"
+ },
+ {
+ "name": "event_ts",
+ "meaning": "время снимка (`now()`)"
+ },
+ {
+ "name": "_load_id",
+ "meaning": "идентификатор загрузки"
+ },
+ {
+ "name": "_load_ts",
+ "meaning": "время записи в STG"
+ }
+ ],
+ "fill": "Полный снимок при каждом новом запуске STG; повтор в том же `_load_id` не добавляет строку повторно.",
+ "status": "ready",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/stg/airports_ddl.sql"
+ },
+ {
+ "label": "Загрузка",
+ "url": "../../sql/stg/airports_load.sql"
+ },
+ {
+ "label": "DQ",
+ "url": "../../sql/stg/airports_dq.sql"
+ }
+ ],
+ "parts": [
+ "stg.airports_ext"
+ ]
+ },
+ {
+ "id": "ods.airports",
+ "name": "ods.airports",
+ "layer": "ods",
+ "order": 6,
+ "what": "Справочник аэропортов. Один аэропорт может участвовать в разных рейсах как место вылета или прилета.",
+ "grain": "Один аэропорт.",
+ "keys": "`airport_code` — логический ключ текущей строки.",
+ "fields": [
+ {
+ "name": "airport_code",
+ "meaning": "код аэропорта"
+ },
+ {
+ "name": "airport_name",
+ "meaning": "название"
+ },
+ {
+ "name": "city",
+ "meaning": "город"
+ },
+ {
+ "name": "timezone",
+ "meaning": "часовой пояс"
+ }
+ ],
+ "fill": "Полный снимок из согласованного батча STG (`stg_batch_id`): TRUNCATE + INSERT, одна строка на бизнес-ключ. Из JSON извлекаются русские названия аэропорта, города и страны.",
+ "status": "ready",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/ods/airports_ddl.sql"
+ },
+ {
+ "label": "Загрузка",
+ "url": "../../sql/ods/airports_load.sql"
+ },
+ {
+ "label": "DQ",
+ "url": "../../sql/ods/airports_dq.sql"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "bookings.airplanes_data",
+ "name": "bookings.airplanes_data",
+ "layer": "source",
+ "order": 7,
+ "what": "Справочник моделей самолетов; бортовых номеров отдельных самолетов здесь нет.",
+ "grain": "Одна модель самолета.",
+ "keys": "`airplane_code` — логический ключ строки источника.",
+ "fields": [
+ {
+ "name": "airplane_code",
+ "meaning": "код модели"
+ },
+ {
+ "name": "model",
+ "meaning": "название модели"
+ },
+ {
+ "name": "range",
+ "meaning": "дальность полета"
+ },
+ {
+ "name": "speed",
+ "meaning": "скорость"
+ }
+ ],
+ "fill": "Демо-БД восстанавливается из seed-дампа; генератор добавляет учебные дни. PXF читает ее через `stg.airplanes_ext`.",
+ "status": "source",
+ "links": [
+ {
+ "label": "Источник и генератор",
+ "url": "../../bookings/README.md"
+ },
+ {
+ "label": "DDL входа PXF",
+ "url": "../../sql/stg/airplanes_ddl.sql"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "stg.airplanes",
+ "name": "stg.airplanes",
+ "layer": "stg",
+ "order": 7,
+ "what": "Справочник моделей самолетов; бортовых номеров отдельных самолетов здесь нет. Здесь бизнес-поля хранятся как TEXT.",
+ "grain": "Одна модель самолета. Строка относится к загрузке STG.",
+ "keys": "Бизнес-ключ: `airplane_code`. Для хранимой строки учитывайте также `_load_id`: снимки разных запусков сосуществуют.",
+ "fields": [
+ {
+ "name": "airplane_code",
+ "meaning": "код модели"
+ },
+ {
+ "name": "model",
+ "meaning": "название модели"
+ },
+ {
+ "name": "range",
+ "meaning": "дальность полета"
+ },
+ {
+ "name": "speed",
+ "meaning": "скорость"
+ },
+ {
+ "name": "event_ts",
+ "meaning": "время снимка (`now()`)"
+ },
+ {
+ "name": "_load_id",
+ "meaning": "идентификатор загрузки"
+ },
+ {
+ "name": "_load_ts",
+ "meaning": "время записи в STG"
+ }
+ ],
+ "fill": "Полный снимок при каждом новом запуске STG; повтор в том же `_load_id` не добавляет строку повторно.",
+ "status": "ready",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/stg/airplanes_ddl.sql"
+ },
+ {
+ "label": "Загрузка",
+ "url": "../../sql/stg/airplanes_load.sql"
+ },
+ {
+ "label": "DQ",
+ "url": "../../sql/stg/airplanes_dq.sql"
+ }
+ ],
+ "parts": [
+ "stg.airplanes_ext"
+ ]
+ },
+ {
+ "id": "ods.airplanes",
+ "name": "ods.airplanes",
+ "layer": "ods",
+ "order": 7,
+ "what": "Справочник моделей самолетов; бортовых номеров отдельных самолетов здесь нет.",
+ "grain": "Одна модель самолета.",
+ "keys": "`airplane_code` — логический ключ текущей строки.",
+ "fields": [
+ {
+ "name": "airplane_code",
+ "meaning": "код модели"
+ },
+ {
+ "name": "model",
+ "meaning": "название модели"
+ },
+ {
+ "name": "range_km",
+ "meaning": "дальность полета"
+ },
+ {
+ "name": "speed_kmh",
+ "meaning": "скорость"
+ }
+ ],
+ "fill": "Ожидается по ТЗ: Полный снимок из согласованного батча STG (`stg_batch_id`): TRUNCATE + INSERT, одна строка на бизнес-ключ.",
+ "status": "student",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/ods/airplanes_ddl.sql"
+ },
+ {
+ "label": "Загрузка · заглушка",
+ "url": "../../sql/ods/airplanes_load.sql"
+ },
+ {
+ "label": "DQ · заглушка",
+ "url": "../../sql/ods/airplanes_dq.sql"
+ },
+ {
+ "label": "Задание 1.1",
+ "url": "../assignment/analyst_spec.md#11-odsairplanes"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "bookings.seats",
+ "name": "bookings.seats",
+ "layer": "source",
+ "order": 8,
+ "what": "Схема салона модели самолета. Номер места повторяется у разных моделей.",
+ "grain": "Одно место в салоне модели.",
+ "keys": "`airplane_code` + `seat_no` — логический ключ строки источника.",
+ "fields": [
+ {
+ "name": "airplane_code",
+ "meaning": "модель самолета"
+ },
+ {
+ "name": "seat_no",
+ "meaning": "номер места"
+ },
+ {
+ "name": "fare_conditions",
+ "meaning": "класс обслуживания места"
+ }
+ ],
+ "fill": "Демо-БД восстанавливается из seed-дампа; генератор добавляет учебные дни. PXF читает ее через `stg.seats_ext`.",
+ "status": "source",
+ "links": [
+ {
+ "label": "Источник и генератор",
+ "url": "../../bookings/README.md"
+ },
+ {
+ "label": "DDL входа PXF",
+ "url": "../../sql/stg/seats_ddl.sql"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "stg.seats",
+ "name": "stg.seats",
+ "layer": "stg",
+ "order": 8,
+ "what": "Схема салона модели самолета. Номер места повторяется у разных моделей. Здесь бизнес-поля хранятся как TEXT.",
+ "grain": "Одно место в салоне модели. Строка относится к загрузке STG.",
+ "keys": "Бизнес-ключ: `airplane_code` + `seat_no`. Для хранимой строки учитывайте также `_load_id`: снимки разных запусков сосуществуют.",
+ "fields": [
+ {
+ "name": "airplane_code",
+ "meaning": "модель самолета"
+ },
+ {
+ "name": "seat_no",
+ "meaning": "номер места"
+ },
+ {
+ "name": "fare_conditions",
+ "meaning": "класс обслуживания места"
+ },
+ {
+ "name": "event_ts",
+ "meaning": "время снимка (`now()`)"
+ },
+ {
+ "name": "_load_id",
+ "meaning": "идентификатор загрузки"
+ },
+ {
+ "name": "_load_ts",
+ "meaning": "время записи в STG"
+ }
+ ],
+ "fill": "Полный снимок при каждом новом запуске STG; повтор в том же `_load_id` не добавляет строку повторно.",
+ "status": "ready",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/stg/seats_ddl.sql"
+ },
+ {
+ "label": "Загрузка",
+ "url": "../../sql/stg/seats_load.sql"
+ },
+ {
+ "label": "DQ",
+ "url": "../../sql/stg/seats_dq.sql"
+ }
+ ],
+ "parts": [
+ "stg.seats_ext"
+ ]
+ },
+ {
+ "id": "ods.seats",
+ "name": "ods.seats",
+ "layer": "ods",
+ "order": 8,
+ "what": "Схема салона модели самолета. Номер места повторяется у разных моделей.",
+ "grain": "Одно место в салоне модели.",
+ "keys": "`airplane_code` + `seat_no` — логический ключ текущей строки.",
+ "fields": [
+ {
+ "name": "airplane_code",
+ "meaning": "модель самолета"
+ },
+ {
+ "name": "seat_no",
+ "meaning": "номер места"
+ },
+ {
+ "name": "fare_conditions",
+ "meaning": "класс обслуживания места"
+ }
+ ],
+ "fill": "Ожидается по ТЗ: Полный снимок из согласованного батча STG (`stg_batch_id`): TRUNCATE + INSERT, одна строка на бизнес-ключ.",
+ "status": "student",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/ods/seats_ddl.sql"
+ },
+ {
+ "label": "Загрузка · заглушка",
+ "url": "../../sql/ods/seats_load.sql"
+ },
+ {
+ "label": "DQ · заглушка",
+ "url": "../../sql/ods/seats_dq.sql"
+ },
+ {
+ "label": "Задание 1.2",
+ "url": "../assignment/analyst_spec.md#12-odsseats"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "dds.dim_calendar",
+ "name": "dds.dim_calendar",
+ "layer": "dds",
+ "order": 0,
+ "what": "Дает дату и календарные признаки рейса.",
+ "grain": "Одна календарная дата.",
+ "keys": "`date_actual` — дата; `calendar_sk` — ключ измерения.",
+ "fields": [
+ {
+ "name": "calendar_sk",
+ "meaning": "ключ даты"
+ },
+ {
+ "name": "date_actual",
+ "meaning": "дата"
+ },
+ {
+ "name": "year_actual",
+ "meaning": "год"
+ },
+ {
+ "name": "month_actual",
+ "meaning": "месяц"
+ },
+ {
+ "name": "day_of_week",
+ "meaning": "день недели ISO"
+ },
+ {
+ "name": "is_weekend",
+ "meaning": "суббота или воскресенье"
+ }
+ ],
+ "fill": "Генерация дат 2016–2030 только при пустой таблице. Календарь не читается из ODS.",
+ "status": "ready",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/dds/dim_calendar_ddl.sql"
+ },
+ {
+ "label": "Загрузка",
+ "url": "../../sql/dds/dim_calendar_load.sql"
+ },
+ {
+ "label": "DQ",
+ "url": "../../sql/dds/dim_calendar_dq.sql"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "dds.dim_tariffs",
+ "name": "dds.dim_tariffs",
+ "layer": "dds",
+ "order": 1,
+ "what": "Выносит классы обслуживания из сегментов в измерение.",
+ "grain": "Один класс обслуживания.",
+ "keys": "`fare_conditions` — бизнес-ключ; `tariff_sk` — ключ измерения.",
+ "fields": [
+ {
+ "name": "tariff_sk",
+ "meaning": "ключ класса"
+ },
+ {
+ "name": "fare_conditions",
+ "meaning": "Economy, Business или Comfort"
+ }
+ ],
+ "fill": "DISTINCT непустых `ods.segments.fare_conditions`; вставляются только новые значения с новым SK.",
+ "status": "ready",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/dds/dim_tariffs_ddl.sql"
+ },
+ {
+ "label": "Загрузка",
+ "url": "../../sql/dds/dim_tariffs_load.sql"
+ },
+ {
+ "label": "DQ",
+ "url": "../../sql/dds/dim_tariffs_dq.sql"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "dds.dim_passengers",
+ "name": "dds.dim_passengers",
+ "layer": "dds",
+ "order": 2,
+ "what": "Собирает пассажиров из билетов: отдельного справочника пассажиров в источнике нет.",
+ "grain": "Один пассажир, даже если у него много билетов.",
+ "keys": "`passenger_id` — бизнес-ключ; `passenger_sk` — ключ измерения.",
+ "fields": [
+ {
+ "name": "passenger_sk",
+ "meaning": "ключ пассажира"
+ },
+ {
+ "name": "passenger_id",
+ "meaning": "идентификатор из билетов"
+ },
+ {
+ "name": "passenger_name",
+ "meaning": "актуальное имя"
+ }
+ ],
+ "fill": "По ТЗ: SCD1; выбрать актуальное имя из билетов, добавлять новых пассажиров и обновлять изменившиеся имена.",
+ "status": "student",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/dds/dim_passengers_ddl.sql"
+ },
+ {
+ "label": "Загрузка · заглушка",
+ "url": "../../sql/dds/dim_passengers_load.sql"
+ },
+ {
+ "label": "DQ · заглушка",
+ "url": "../../sql/dds/dim_passengers_dq.sql"
+ },
+ {
+ "label": "Задание",
+ "url": "../assignment/analyst_spec.md#22-ddsdim_passengers-scd1"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "dds.dim_routes",
+ "name": "dds.dim_routes",
+ "layer": "dds",
+ "order": 3,
+ "what": "Хранит версии маршрута для выбора состояния на дату рейса.",
+ "grain": "Одна версия маршрута на интервале `[valid_from, valid_to)`.",
+ "keys": "`route_bk` повторяется в истории; `route_sk` обозначает одну версию. `valid_to IS NULL` — открытая версия.",
+ "fields": [
+ {
+ "name": "route_sk",
+ "meaning": "ключ версии"
+ },
+ {
+ "name": "route_bk",
+ "meaning": "номер маршрута"
+ },
+ {
+ "name": "airplane_code",
+ "meaning": "модель самолета этой версии"
+ },
+ {
+ "name": "departure_airport",
+ "meaning": "код вылета"
+ },
+ {
+ "name": "arrival_airport",
+ "meaning": "код прилета"
+ },
+ {
+ "name": "total_seats",
+ "meaning": "число мест"
+ },
+ {
+ "name": "valid_from",
+ "meaning": "начало версии"
+ },
+ {
+ "name": "valid_to",
+ "meaning": "конец версии, не включается"
+ },
+ {
+ "name": "hashdiff",
+ "meaning": "признак изменения атрибутов"
+ }
+ ],
+ "fill": "По ТЗ: SCD2 для атрибутов маршрута; города, модель и число мест обогащаются из измерений. Их обновление у открытых версий само по себе новую версию не создает.",
+ "status": "student",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/dds/dim_routes_ddl.sql"
+ },
+ {
+ "label": "Загрузка · заглушка",
+ "url": "../../sql/dds/dim_routes_load.sql"
+ },
+ {
+ "label": "DQ · заглушка",
+ "url": "../../sql/dds/dim_routes_dq.sql"
+ },
+ {
+ "label": "Задание",
+ "url": "../assignment/analyst_spec.md#23-ddsdim_routes-scd2"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "dds.fact_flight_sales",
+ "name": "dds.fact_flight_sales",
+ "layer": "dds",
+ "order": 4,
+ "what": "Связывает проданный сегмент с измерениями; служит основой витрин.",
+ "grain": "Один сегмент билета: `(ticket_no, flight_id)`. Один билет с пересадкой дает несколько строк.",
+ "keys": "`ticket_no` + `flight_id` — логическое зерно. `book_ref` повторяется у разных билетов и сегментов. Собственного `fact_sk` нет.",
+ "fields": [
+ {
+ "name": "ticket_no",
+ "meaning": "билет"
+ },
+ {
+ "name": "flight_id",
+ "meaning": "рейс"
+ },
+ {
+ "name": "book_ref",
+ "meaning": "бронирование"
+ },
+ {
+ "name": "book_date",
+ "meaning": "дата бронирования"
+ },
+ {
+ "name": "price",
+ "meaning": "цена сегмента из `ods.segments.amount`"
+ },
+ {
+ "name": "is_boarded",
+ "meaning": "есть ли посадочный талон"
+ },
+ {
+ "name": "seat_no",
+ "meaning": "место из талона"
+ },
+ {
+ "name": "calendar_sk",
+ "meaning": "дата вылета"
+ },
+ {
+ "name": "departure_airport_sk",
+ "meaning": "аэропорт вылета"
+ },
+ {
+ "name": "arrival_airport_sk",
+ "meaning": "аэропорт прилета"
+ },
+ {
+ "name": "route_sk",
+ "meaning": "версия маршрута на дату вылета"
+ },
+ {
+ "name": "airplane_sk",
+ "meaning": "модель через выбранную версию маршрута"
+ },
+ {
+ "name": "passenger_sk",
+ "meaning": "пассажир"
+ },
+ {
+ "name": "tariff_sk",
+ "meaning": "класс обслуживания"
+ }
+ ],
+ "fill": "INSERT новых пар `(ticket_no, flight_id)`; UPDATE меняет только `seat_no`, `price`, `is_boarded` и метаданные загрузки. Ключи измерений фиксируются при вставке. До выполнения заданий `passenger_sk`, `route_sk`, `airplane_sk` могут быть NULL. Повторный запуск их не заполнит: после реализации измерений нужен пересчет факта по ТЗ.",
+ "status": "ready",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/dds/fact_flight_sales_ddl.sql"
+ },
+ {
+ "label": "Загрузка",
+ "url": "../../sql/dds/fact_flight_sales_load.sql"
+ },
+ {
+ "label": "DQ",
+ "url": "../../sql/dds/fact_flight_sales_dq.sql"
+ },
+ {
+ "label": "Пересчет после реализации измерений",
+ "url": "../assignment/analyst_spec.md#пересчёт-факта-после-реализации-измерений"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "dds.dim_airports",
+ "name": "dds.dim_airports",
+ "layer": "dds",
+ "order": 6,
+ "what": "Один справочник для двух ролей аэропорта в факте: вылет и прилет.",
+ "grain": "Один аэропорт в текущем состоянии.",
+ "keys": "`airport_bk` — код из `ods.airports.airport_code`; `airport_sk` — ключ измерения.",
+ "fields": [
+ {
+ "name": "airport_sk",
+ "meaning": "ключ аэропорта"
+ },
+ {
+ "name": "airport_bk",
+ "meaning": "код аэропорта"
+ },
+ {
+ "name": "city",
+ "meaning": "город"
+ },
+ {
+ "name": "airport_name",
+ "meaning": "название"
+ },
+ {
+ "name": "timezone",
+ "meaning": "часовой пояс"
+ }
+ ],
+ "fill": "SCD1 из `ods.airports`: UPDATE изменившихся атрибутов, INSERT новых аэропортов с новым SK.",
+ "status": "ready",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/dds/dim_airports_ddl.sql"
+ },
+ {
+ "label": "Загрузка",
+ "url": "../../sql/dds/dim_airports_load.sql"
+ },
+ {
+ "label": "DQ",
+ "url": "../../sql/dds/dim_airports_dq.sql"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "dds.dim_airplanes",
+ "name": "dds.dim_airplanes",
+ "layer": "dds",
+ "order": 7,
+ "what": "Описывает модель самолета и вместимость ее салона.",
+ "grain": "Одна модель самолета.",
+ "keys": "`airplane_bk` — код модели; `airplane_sk` — ключ измерения.",
+ "fields": [
+ {
+ "name": "airplane_sk",
+ "meaning": "ключ модели"
+ },
+ {
+ "name": "airplane_bk",
+ "meaning": "код модели"
+ },
+ {
+ "name": "model",
+ "meaning": "название"
+ },
+ {
+ "name": "total_seats",
+ "meaning": "количество мест из `ods.seats`"
+ },
+ {
+ "name": "range_km",
+ "meaning": "дальность, км"
+ },
+ {
+ "name": "speed_kmh",
+ "meaning": "скорость, км/ч"
+ }
+ ],
+ "fill": "По ТЗ: SCD1 из `ods.airplanes` с подсчетом мест из `ods.seats` по коду модели. Сейчас загрузка и DQ — заглушки.",
+ "status": "student",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/dds/dim_airplanes_ddl.sql"
+ },
+ {
+ "label": "Загрузка · заглушка",
+ "url": "../../sql/dds/dim_airplanes_load.sql"
+ },
+ {
+ "label": "DQ · заглушка",
+ "url": "../../sql/dds/dim_airplanes_dq.sql"
+ },
+ {
+ "label": "Задание",
+ "url": "../assignment/analyst_spec.md#21-ddsdim_airplanes-scd1"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "dm.sales_report",
+ "name": "dm.sales_report",
+ "layer": "dm",
+ "order": 0,
+ "what": "Показывает выручку и долю посадок по направлениям и классам обслуживания.",
+ "grain": "Один день вылета × аэропорт вылета × аэропорт прилета × класс обслуживания.",
+ "keys": "`flight_date` + `departure_airport_sk` + `arrival_airport_sk` + `tariff_sk` — логический ключ агрегата.",
+ "fields": [
+ {
+ "name": "flight_date",
+ "meaning": "дата вылета"
+ },
+ {
+ "name": "departure_airport_sk",
+ "meaning": "аэропорт вылета"
+ },
+ {
+ "name": "arrival_airport_sk",
+ "meaning": "аэропорт прилета"
+ },
+ {
+ "name": "tariff_sk",
+ "meaning": "класс обслуживания"
+ },
+ {
+ "name": "tickets_sold",
+ "meaning": "число сегментов, COUNT(*) факта"
+ },
+ {
+ "name": "total_revenue",
+ "meaning": "сумма цен сегментов"
+ },
+ {
+ "name": "passengers_boarded",
+ "meaning": "сегменты с посадочным талоном"
+ },
+ {
+ "name": "boarding_rate",
+ "meaning": "доля сегментов с талоном"
+ }
+ ],
+ "fill": "По HWM факта выбираются затронутые даты; за эти даты агрегируются все факты, выполняются UPDATE и INSERT.",
+ "status": "ready",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/dm/sales_report_ddl.sql"
+ },
+ {
+ "label": "Загрузка",
+ "url": "../../sql/dm/sales_report_load.sql"
+ },
+ {
+ "label": "DQ",
+ "url": "../../sql/dm/sales_report_dq.sql"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "dm.airport_traffic",
+ "name": "dm.airport_traffic",
+ "layer": "dm",
+ "order": 2,
+ "what": "Сводит вылеты и прилеты каждого аэропорта за день.",
+ "grain": "Один аэропорт за день.",
+ "keys": "`traffic_date` + `airport_sk` — логический ключ агрегата.",
+ "fields": [
+ {
+ "name": "traffic_date",
+ "meaning": "дата из календаря вылета"
+ },
+ {
+ "name": "airport_sk",
+ "meaning": "аэропорт в роли вылета или прилета"
+ },
+ {
+ "name": "total_passengers",
+ "meaning": "посадки по обеим ролям"
+ },
+ {
+ "name": "departures_revenue",
+ "meaning": "выручка вылетов"
+ },
+ {
+ "name": "arrivals_revenue",
+ "meaning": "выручка прилетов"
+ }
+ ],
+ "fill": "По ТЗ: пересчитывать затронутые дни. Каждый факт участвует в двух ролях. Выручку вылетов и прилетов нельзя складывать: получится двойной счет. Дата обеих ролей берется из календаря факта (дата вылета).",
+ "status": "student",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/dm/airport_traffic_ddl.sql"
+ },
+ {
+ "label": "Загрузка · заглушка",
+ "url": "../../sql/dm/airport_traffic_load.sql"
+ },
+ {
+ "label": "DQ · заглушка",
+ "url": "../../sql/dm/airport_traffic_dq.sql"
+ },
+ {
+ "label": "Задание",
+ "url": "../assignment/analyst_spec.md#31-dmairport_traffic"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "dm.route_performance",
+ "name": "dm.route_performance",
+ "layer": "dm",
+ "order": 4,
+ "what": "Сравнивает маршруты по выручке, посадкам и заполняемости.",
+ "grain": "Один маршрут за всю историю, объединяя его версии.",
+ "keys": "`route_bk` — логический ключ агрегата.",
+ "fields": [
+ {
+ "name": "route_bk",
+ "meaning": "маршрут независимо от версии"
+ },
+ {
+ "name": "route_sk",
+ "meaning": "ключ текущей версии"
+ },
+ {
+ "name": "total_flights",
+ "meaning": "уникальные рейсы"
+ },
+ {
+ "name": "total_tickets",
+ "meaning": "проданные сегменты"
+ },
+ {
+ "name": "avg_load_factor",
+ "meaning": "посадки относительно числа рейсов и мест текущей версии"
+ }
+ ],
+ "fill": "По ТЗ: полный пересчет; факты всех версий группируются по бизнес-ключу маршрута, описания берутся из открытой версии.",
+ "status": "student",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/dm/route_performance_ddl.sql"
+ },
+ {
+ "label": "Загрузка · заглушка",
+ "url": "../../sql/dm/route_performance_load.sql"
+ },
+ {
+ "label": "DQ · заглушка",
+ "url": "../../sql/dm/route_performance_dq.sql"
+ },
+ {
+ "label": "Задание",
+ "url": "../assignment/analyst_spec.md#32-dmroute_performance"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "dm.monthly_overview",
+ "name": "dm.monthly_overview",
+ "layer": "dm",
+ "order": 6,
+ "what": "Показывает помесячные показатели по моделям самолетов.",
+ "grain": "Одна модель самолета за месяц.",
+ "keys": "`year_actual` + `month_actual` + `airplane_sk` — логический ключ агрегата.",
+ "fields": [
+ {
+ "name": "year_actual",
+ "meaning": "год"
+ },
+ {
+ "name": "month_actual",
+ "meaning": "месяц"
+ },
+ {
+ "name": "airplane_sk",
+ "meaning": "модель"
+ },
+ {
+ "name": "total_flights",
+ "meaning": "число уникальных рейсов"
+ },
+ {
+ "name": "avg_load_factor",
+ "meaning": "средняя заполняемость по рейсам"
+ }
+ ],
+ "fill": "По ТЗ: пересчитывать затронутые месяцы; сначала считать показатели рейса, затем месяца.",
+ "status": "student",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/dm/monthly_overview_ddl.sql"
+ },
+ {
+ "label": "Загрузка · заглушка",
+ "url": "../../sql/dm/monthly_overview_load.sql"
+ },
+ {
+ "label": "DQ · заглушка",
+ "url": "../../sql/dm/monthly_overview_dq.sql"
+ },
+ {
+ "label": "Задание",
+ "url": "../assignment/analyst_spec.md#33-dmmonthly_overview"
+ }
+ ],
+ "parts": []
+ },
+ {
+ "id": "dm.passenger_loyalty",
+ "name": "dm.passenger_loyalty",
+ "layer": "dm",
+ "order": 8,
+ "what": "Собирает историю перелетов и трат пассажира.",
+ "grain": "Один пассажир за всю историю.",
+ "keys": "`passenger_sk` — логический ключ агрегата.",
+ "fields": [
+ {
+ "name": "passenger_sk",
+ "meaning": "ключ пассажира"
+ },
+ {
+ "name": "passenger_bk",
+ "meaning": "исходный passenger_id"
+ },
+ {
+ "name": "total_bookings",
+ "meaning": "уникальные бронирования"
+ },
+ {
+ "name": "total_flights",
+ "meaning": "число сегментов пассажира"
+ },
+ {
+ "name": "total_spent",
+ "meaning": "сумма цен"
+ },
+ {
+ "name": "favorite_fare_conditions",
+ "meaning": "самый частый класс обслуживания"
+ }
+ ],
+ "fill": "По ТЗ: пересчитывать всю историю пассажиров, чьи факты изменились. Строки без `passenger_sk` исключаются.",
+ "status": "student",
+ "links": [
+ {
+ "label": "DDL",
+ "url": "../../sql/dm/passenger_loyalty_ddl.sql"
+ },
+ {
+ "label": "Загрузка · заглушка",
+ "url": "../../sql/dm/passenger_loyalty_load.sql"
+ },
+ {
+ "label": "DQ · заглушка",
+ "url": "../../sql/dm/passenger_loyalty_dq.sql"
+ },
+ {
+ "label": "Задание",
+ "url": "../assignment/analyst_spec.md#34-dmpassenger_loyalty"
+ }
+ ],
+ "parts": []
+ }
+ ],
+ "edges": [
+ {
+ "id": "flow-bookings.bookings-stg.bookings-0",
+ "from": "bookings.bookings",
+ "to": "stg.bookings",
+ "label": "Чтение через PXF",
+ "join": "`stg.bookings_ext` читает `bookings.bookings` без локальной копии. Загрузчик переносит выбранные строки в `stg.bookings`. Добавление строк по `event_ts` предыдущих загрузок. Опорное время — `book_date` бронирования. Повтор защищен ключом строки внутри `_load_id`.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.bookings",
+ "columns": [
+ "book_ref"
+ ]
+ },
+ {
+ "node": "stg.bookings",
+ "columns": [
+ "book_ref"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/stg/bookings_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-stg.bookings-ods.bookings-1",
+ "from": "stg.bookings",
+ "to": "ods.bookings",
+ "label": "Типизация и загрузка ODS",
+ "join": "Дельта из STG по `_load_ts`, приведение типов, выбор последней строки по бизнес-ключу; UPDATE изменившихся и INSERT новых. Время и идентификатор загрузки сохраняются из STG.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "stg.bookings",
+ "columns": [
+ "book_ref"
+ ]
+ },
+ {
+ "node": "ods.bookings",
+ "columns": [
+ "book_ref"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/ods/bookings_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-bookings.tickets-stg.tickets-2",
+ "from": "bookings.tickets",
+ "to": "stg.tickets",
+ "label": "Чтение через PXF",
+ "join": "`stg.tickets_ext` читает `bookings.tickets` без локальной копии. Загрузчик переносит выбранные строки в `stg.tickets`. Добавление строк по `event_ts` предыдущих загрузок. Опорное время — `book_date` бронирования. Уже сохраненный `ticket_no` повторно не вставляется.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.tickets",
+ "columns": [
+ "ticket_no"
+ ]
+ },
+ {
+ "node": "stg.tickets",
+ "columns": [
+ "ticket_no"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/stg/tickets_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-stg.tickets-ods.tickets-3",
+ "from": "stg.tickets",
+ "to": "ods.tickets",
+ "label": "Типизация и загрузка ODS",
+ "join": "Дельта из STG по `_load_ts`, приведение типов, выбор последней строки по бизнес-ключу; UPDATE изменившихся и INSERT новых. Время и идентификатор загрузки сохраняются из STG. `outbound` становится `is_outbound` (BOOLEAN).",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "stg.tickets",
+ "columns": [
+ "ticket_no"
+ ]
+ },
+ {
+ "node": "ods.tickets",
+ "columns": [
+ "ticket_no"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/ods/tickets_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-bookings.segments-stg.segments-4",
+ "from": "bookings.segments",
+ "to": "stg.segments",
+ "label": "Чтение через PXF",
+ "join": "`stg.segments_ext` читает `bookings.segments` без локальной копии. Загрузчик переносит выбранные строки в `stg.segments`. Добавление строк по `event_ts` предыдущих загрузок. Опорное время — `book_date` бронирования. Повтор защищен ключом строки внутри `_load_id`.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.segments",
+ "columns": [
+ "ticket_no",
+ "flight_id"
+ ]
+ },
+ {
+ "node": "stg.segments",
+ "columns": [
+ "ticket_no",
+ "flight_id"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/stg/segments_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-stg.segments-ods.segments-5",
+ "from": "stg.segments",
+ "to": "ods.segments",
+ "label": "Типизация и загрузка ODS",
+ "join": "Дельта из STG по `_load_ts`, приведение типов, выбор последней строки по бизнес-ключу; UPDATE изменившихся и INSERT новых. Время и идентификатор загрузки сохраняются из STG. `stg.segments.price` становится `amount` (NUMERIC).",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "stg.segments",
+ "columns": [
+ "ticket_no",
+ "flight_id"
+ ]
+ },
+ {
+ "node": "ods.segments",
+ "columns": [
+ "ticket_no",
+ "flight_id"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/ods/segments_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-bookings.flights-stg.flights-6",
+ "from": "bookings.flights",
+ "to": "stg.flights",
+ "label": "Чтение через PXF",
+ "join": "`stg.flights_ext` читает `bookings.flights` без локальной копии. Загрузчик переносит выбранные строки в `stg.flights`. Добавление строк по `event_ts` предыдущих загрузок. Опорное время — `scheduled_departure`. Повтор защищен ключом строки внутри `_load_id`.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.flights",
+ "columns": [
+ "flight_id"
+ ]
+ },
+ {
+ "node": "stg.flights",
+ "columns": [
+ "flight_id"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/stg/flights_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-stg.flights-ods.flights-7",
+ "from": "stg.flights",
+ "to": "ods.flights",
+ "label": "Типизация и загрузка ODS",
+ "join": "Дельта из STG по `_load_ts`, приведение типов, выбор последней строки по бизнес-ключу; UPDATE изменившихся и INSERT новых. Время и идентификатор загрузки сохраняются из STG. Дополнительно берутся рейсы из истории STG, на которые ссылаются новые `stg.segments`.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "stg.flights",
+ "columns": [
+ "flight_id"
+ ]
+ },
+ {
+ "node": "ods.flights",
+ "columns": [
+ "flight_id"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/ods/flights_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-bookings.boarding_passes-stg.boarding_passes-8",
+ "from": "bookings.boarding_passes",
+ "to": "stg.boarding_passes",
+ "label": "Чтение через PXF",
+ "join": "`stg.boarding_passes_ext` читает `bookings.boarding_passes` без локальной копии. Загрузчик переносит выбранные строки в `stg.boarding_passes`. Добавление строк по `event_ts` предыдущих загрузок. Опорное время — `book_date` бронирования. Повтор защищен ключом строки внутри `_load_id`.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.boarding_passes",
+ "columns": [
+ "ticket_no",
+ "flight_id"
+ ]
+ },
+ {
+ "node": "stg.boarding_passes",
+ "columns": [
+ "ticket_no",
+ "flight_id"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/stg/boarding_passes_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-stg.boarding_passes-ods.boarding_passes-9",
+ "from": "stg.boarding_passes",
+ "to": "ods.boarding_passes",
+ "label": "Типизация и загрузка ODS",
+ "join": "Дельта из STG по `_load_ts`, приведение типов, выбор последней строки по бизнес-ключу; UPDATE изменившихся и INSERT новых. Время и идентификатор загрузки сохраняются из STG.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "stg.boarding_passes",
+ "columns": [
+ "ticket_no",
+ "flight_id"
+ ]
+ },
+ {
+ "node": "ods.boarding_passes",
+ "columns": [
+ "ticket_no",
+ "flight_id"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/ods/boarding_passes_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-bookings.routes-stg.routes-10",
+ "from": "bookings.routes",
+ "to": "stg.routes",
+ "label": "Чтение через PXF",
+ "join": "`stg.routes_ext` читает `bookings.routes` без локальной копии. Загрузчик переносит выбранные строки в `stg.routes`. Полный снимок при каждом новом запуске STG; повтор в том же `_load_id` не добавляет строку повторно.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.routes",
+ "columns": [
+ "route_no",
+ "validity"
+ ]
+ },
+ {
+ "node": "stg.routes",
+ "columns": [
+ "route_no",
+ "validity"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/stg/routes_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-stg.routes-ods.routes-11",
+ "from": "stg.routes",
+ "to": "ods.routes",
+ "label": "Типизация и загрузка ODS",
+ "join": "Полный снимок из согласованного батча STG (`stg_batch_id`): TRUNCATE + INSERT, одна строка на бизнес-ключ. `scheduled_time` становится `departure_time` (TIME), `days_of_week` — INTEGER[], `duration` — INTERVAL.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "stg.routes",
+ "columns": [
+ "route_no",
+ "validity"
+ ]
+ },
+ {
+ "node": "ods.routes",
+ "columns": [
+ "route_no",
+ "validity"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/ods/routes_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-bookings.airports_data-stg.airports-12",
+ "from": "bookings.airports_data",
+ "to": "stg.airports",
+ "label": "Чтение через PXF",
+ "join": "`stg.airports_ext` читает `bookings.airports_data` без локальной копии. Загрузчик переносит выбранные строки в `stg.airports`. Полный снимок при каждом новом запуске STG; повтор в том же `_load_id` не добавляет строку повторно.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.airports_data",
+ "columns": [
+ "airport_code"
+ ]
+ },
+ {
+ "node": "stg.airports",
+ "columns": [
+ "airport_code"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/stg/airports_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-stg.airports-ods.airports-13",
+ "from": "stg.airports",
+ "to": "ods.airports",
+ "label": "Типизация и загрузка ODS",
+ "join": "Полный снимок из согласованного батча STG (`stg_batch_id`): TRUNCATE + INSERT, одна строка на бизнес-ключ. Из JSON извлекаются русские названия аэропорта, города и страны.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "stg.airports",
+ "columns": [
+ "airport_code"
+ ]
+ },
+ {
+ "node": "ods.airports",
+ "columns": [
+ "airport_code"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/ods/airports_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-bookings.airplanes_data-stg.airplanes-14",
+ "from": "bookings.airplanes_data",
+ "to": "stg.airplanes",
+ "label": "Чтение через PXF",
+ "join": "`stg.airplanes_ext` читает `bookings.airplanes_data` без локальной копии. Загрузчик переносит выбранные строки в `stg.airplanes`. Полный снимок при каждом новом запуске STG; повтор в том же `_load_id` не добавляет строку повторно.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.airplanes_data",
+ "columns": [
+ "airplane_code"
+ ]
+ },
+ {
+ "node": "stg.airplanes",
+ "columns": [
+ "airplane_code"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/stg/airplanes_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-stg.airplanes-ods.airplanes-15",
+ "from": "stg.airplanes",
+ "to": "ods.airplanes",
+ "label": "Типизация и загрузка ODS",
+ "join": "Ожидается по ТЗ: Полный снимок из согласованного батча STG (`stg_batch_id`): TRUNCATE + INSERT, одна строка на бизнес-ключ.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "stg.airplanes",
+ "columns": [
+ "airplane_code"
+ ]
+ },
+ {
+ "node": "ods.airplanes",
+ "columns": [
+ "airplane_code"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#11-odsairplanes"
+ }
+ ]
+ },
+ {
+ "id": "flow-bookings.seats-stg.seats-16",
+ "from": "bookings.seats",
+ "to": "stg.seats",
+ "label": "Чтение через PXF",
+ "join": "`stg.seats_ext` читает `bookings.seats` без локальной копии. Загрузчик переносит выбранные строки в `stg.seats`. Полный снимок при каждом новом запуске STG; повтор в том же `_load_id` не добавляет строку повторно.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.seats",
+ "columns": [
+ "airplane_code",
+ "seat_no"
+ ]
+ },
+ {
+ "node": "stg.seats",
+ "columns": [
+ "airplane_code",
+ "seat_no"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/stg/seats_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-stg.seats-ods.seats-17",
+ "from": "stg.seats",
+ "to": "ods.seats",
+ "label": "Типизация и загрузка ODS",
+ "join": "Ожидается по ТЗ: Полный снимок из согласованного батча STG (`stg_batch_id`): TRUNCATE + INSERT, одна строка на бизнес-ключ.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "stg.seats",
+ "columns": [
+ "airplane_code",
+ "seat_no"
+ ]
+ },
+ {
+ "node": "ods.seats",
+ "columns": [
+ "airplane_code",
+ "seat_no"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#12-odsseats"
+ }
+ ]
+ },
+ {
+ "id": "flow-bookings.bookings-stg.tickets-18",
+ "from": "bookings.bookings",
+ "to": "stg.tickets",
+ "label": "Дата отбора",
+ "join": "`book_date` из `stg.bookings_ext` определяет окно. INNER JOIN по `book_ref` с билетом. Без бронирования строка не проходит соединение.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.bookings",
+ "columns": [
+ "book_ref",
+ "book_date"
+ ]
+ },
+ {
+ "node": "bookings.tickets",
+ "columns": [
+ "ticket_no",
+ "book_ref"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/stg/tickets_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-bookings.bookings-stg.segments-19",
+ "from": "bookings.bookings",
+ "to": "stg.segments",
+ "label": "Дата отбора",
+ "join": "`book_date` из `stg.bookings_ext` определяет окно. Сегмент/талон соединяется с `stg.tickets_ext` по `ticket_no`, затем с `stg.bookings_ext` по `book_ref`. Без бронирования строка не проходит соединение.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.bookings",
+ "columns": [
+ "book_ref",
+ "book_date"
+ ]
+ },
+ {
+ "node": "bookings.tickets",
+ "columns": [
+ "ticket_no",
+ "book_ref"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/stg/segments_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-bookings.bookings-stg.boarding_passes-20",
+ "from": "bookings.bookings",
+ "to": "stg.boarding_passes",
+ "label": "Дата отбора",
+ "join": "`book_date` из `stg.bookings_ext` определяет окно. Сегмент/талон соединяется с `stg.tickets_ext` по `ticket_no`, затем с `stg.bookings_ext` по `book_ref`. Без бронирования строка не проходит соединение.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.bookings",
+ "columns": [
+ "book_ref",
+ "book_date"
+ ]
+ },
+ {
+ "node": "bookings.tickets",
+ "columns": [
+ "ticket_no",
+ "book_ref"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/stg/boarding_passes_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-bookings.tickets-stg.segments-21",
+ "from": "bookings.tickets",
+ "to": "stg.segments",
+ "label": "Билет для отбора",
+ "join": "INNER JOIN по `ticket_no` дает `book_ref`, через него находится дата бронирования. Без билета строка не попадет в STG.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.tickets",
+ "columns": [
+ "ticket_no",
+ "book_ref"
+ ]
+ },
+ {
+ "node": "stg.segments",
+ "columns": [
+ "ticket_no"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/stg/segments_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-bookings.tickets-stg.boarding_passes-22",
+ "from": "bookings.tickets",
+ "to": "stg.boarding_passes",
+ "label": "Билет для отбора",
+ "join": "INNER JOIN по `ticket_no` дает `book_ref`, через него находится дата бронирования. Без билета строка не попадет в STG.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.tickets",
+ "columns": [
+ "ticket_no",
+ "book_ref"
+ ]
+ },
+ {
+ "node": "stg.boarding_passes",
+ "columns": [
+ "ticket_no"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/stg/boarding_passes_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-stg.segments-ods.flights-23",
+ "from": "stg.segments",
+ "to": "ods.flights",
+ "label": "Рейсы новых сегментов",
+ "join": "`flight_id` новых сегментов определяет, какие рейсы дополнительно взять из истории `stg.flights`. Если рейса там нет, эта ветка его не создаст; связь проверяет DQ сегментов.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "stg.segments",
+ "columns": [
+ "flight_id",
+ "_load_ts"
+ ]
+ },
+ {
+ "node": "stg.flights",
+ "columns": [
+ "flight_id"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/ods/flights_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-ods.airports-dds.dim_airports-24",
+ "from": "ods.airports",
+ "to": "dds.dim_airports",
+ "label": "Код аэропорта → SK",
+ "join": "`airport_code = airport_bk`: атрибуты существующего аэропорта обновляются, для нового кода создается строка с SK.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "ods.airports",
+ "columns": [
+ "airport_code"
+ ]
+ },
+ {
+ "node": "dds.dim_airports",
+ "columns": [
+ "airport_bk",
+ "airport_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dds/dim_airports_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-ods.segments-dds.dim_tariffs-25",
+ "from": "ods.segments",
+ "to": "dds.dim_tariffs",
+ "label": "Уникальные классы",
+ "join": "Из `fare_conditions` выбираются уникальные непустые значения. Новому классу назначается `tariff_sk`; отсутствие сегментов не удаляет старые классы.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "ods.segments",
+ "columns": [
+ "fare_conditions"
+ ]
+ },
+ {
+ "node": "dds.dim_tariffs",
+ "columns": [
+ "fare_conditions",
+ "tariff_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dds/dim_tariffs_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-ods.airplanes-dds.dim_airplanes-26",
+ "from": "ods.airplanes",
+ "to": "dds.dim_airplanes",
+ "label": "Описание модели",
+ "join": "По ТЗ: `airplane_code` становится `airplane_bk`; характеристики модели получают свой SK.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "ods.airplanes",
+ "columns": [
+ "airplane_code"
+ ]
+ },
+ {
+ "node": "dds.dim_airplanes",
+ "columns": [
+ "airplane_bk",
+ "airplane_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#21-ddsdim_airplanes-scd1"
+ }
+ ]
+ },
+ {
+ "id": "flow-ods.seats-dds.dim_airplanes-27",
+ "from": "ods.seats",
+ "to": "dds.dim_airplanes",
+ "label": "Вместимость модели",
+ "join": "По ТЗ: места считаются по `airplane_code` и добавляют `total_seats`. LEFT JOIN сохраняет модель без совпавших мест. Точное значение для отсутствующих мест ТЗ не оговаривает; нулевую вместимость нельзя трактовать как известную.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "ods.seats",
+ "columns": [
+ "airplane_code",
+ "seat_no"
+ ]
+ },
+ {
+ "node": "dds.dim_airplanes",
+ "columns": [
+ "airplane_bk",
+ "total_seats"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#21-ddsdim_airplanes-scd1"
+ }
+ ]
+ },
+ {
+ "id": "flow-ods.tickets-dds.dim_passengers-28",
+ "from": "ods.tickets",
+ "to": "dds.dim_passengers",
+ "label": "Пассажиры из билетов",
+ "join": "По ТЗ: несколько билетов одного `passenger_id` дают одного пассажира с актуальным именем. Отдельного JOIN к справочнику пассажиров источника нет.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "ods.tickets",
+ "columns": [
+ "passenger_id",
+ "passenger_name"
+ ]
+ },
+ {
+ "node": "dds.dim_passengers",
+ "columns": [
+ "passenger_id",
+ "passenger_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#22-ddsdim_passengers-scd1"
+ }
+ ]
+ },
+ {
+ "id": "flow-ods.routes-dds.dim_routes-29",
+ "from": "ods.routes",
+ "to": "dds.dim_routes",
+ "label": "История маршрута",
+ "join": "По ТЗ: `route_no` становится `route_bk`; изменения атрибутов создают новую версию с `route_sk` и интервалом действия. Исчезновение маршрута закрывает открытую версию.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "ods.routes",
+ "columns": [
+ "route_no"
+ ]
+ },
+ {
+ "node": "dds.dim_routes",
+ "columns": [
+ "route_bk",
+ "route_sk",
+ "valid_from",
+ "valid_to"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#23-ddsdim_routes-scd2"
+ }
+ ]
+ },
+ {
+ "id": "flow-dds.dim_airports-dds.dim_routes-30",
+ "from": "dds.dim_airports",
+ "to": "dds.dim_routes",
+ "label": "Города вылета и прилета",
+ "join": "По ТЗ: коды `departure_airport` и `arrival_airport` сопоставляются с `airport_bk`; добавляются два города. Тип JOIN при пропуске ТЗ не задает, но города в DDL обязательны: пропуск нельзя молча записать как NULL.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "dds.dim_airports",
+ "columns": [
+ "airport_bk",
+ "city"
+ ]
+ },
+ {
+ "node": "dds.dim_routes",
+ "columns": [
+ "departure_airport",
+ "arrival_airport",
+ "departure_city",
+ "arrival_city"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#23-ddsdim_routes-scd2"
+ }
+ ]
+ },
+ {
+ "id": "flow-dds.dim_airplanes-dds.dim_routes-31",
+ "from": "dds.dim_airplanes",
+ "to": "dds.dim_routes",
+ "label": "Модель и места",
+ "join": "По ТЗ: `airplane_code = airplane_bk` дает `airplane_model` и `total_seats`. Тип JOIN при пропуске не определен; оба поля в DDL обязательны. Это требуемое обогащение, загрузчик пока пуст.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "dds.dim_airplanes",
+ "columns": [
+ "airplane_bk",
+ "model",
+ "total_seats"
+ ]
+ },
+ {
+ "node": "dds.dim_routes",
+ "columns": [
+ "airplane_code",
+ "airplane_model",
+ "total_seats"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#23-ddsdim_routes-scd2"
+ }
+ ]
+ },
+ {
+ "id": "fact-0",
+ "from": "ods.segments",
+ "to": "dds.fact_flight_sales",
+ "label": "Зерно и цена",
+ "join": "Основной поток: пара `(ticket_no, flight_id)` и `amount → price`. Нет сегмента — нет новой строки факта.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "ods.segments",
+ "columns": [
+ "ticket_no",
+ "flight_id",
+ "amount"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "ticket_no",
+ "flight_id",
+ "price"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dds/fact_flight_sales_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "fact-1",
+ "from": "ods.tickets",
+ "to": "dds.fact_flight_sales",
+ "label": "Билет и пассажир",
+ "join": "INNER JOIN `tkt.ticket_no = seg.ticket_no` дает `book_ref` и `passenger_id` для дальнейших соединений. Без билета сегмент не попадет в новый факт.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "ods.tickets",
+ "columns": [
+ "ticket_no",
+ "book_ref",
+ "passenger_id"
+ ]
+ },
+ {
+ "node": "ods.segments",
+ "columns": [
+ "ticket_no"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dds/fact_flight_sales_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "fact-2",
+ "from": "ods.bookings",
+ "to": "dds.fact_flight_sales",
+ "label": "Дата бронирования",
+ "join": "INNER JOIN `bkg.book_ref = tkt.book_ref` дает `book_date::DATE`. Без бронирования сегмент не попадет в новый факт; `total_amount` здесь не используется.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "ods.bookings",
+ "columns": [
+ "book_ref",
+ "book_date"
+ ]
+ },
+ {
+ "node": "ods.tickets",
+ "columns": [
+ "book_ref"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "book_date"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dds/fact_flight_sales_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "fact-3",
+ "from": "ods.flights",
+ "to": "dds.fact_flight_sales",
+ "label": "Рейс, дата и маршрут",
+ "join": "INNER JOIN `flt.flight_id = seg.flight_id` дает `scheduled_departure` и `route_no` для lookup. Без рейса сегмент не попадет в новый факт.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "ods.flights",
+ "columns": [
+ "flight_id",
+ "scheduled_departure",
+ "route_no"
+ ]
+ },
+ {
+ "node": "ods.segments",
+ "columns": [
+ "flight_id"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dds/fact_flight_sales_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "fact-4",
+ "from": "ods.routes",
+ "to": "dds.fact_flight_sales",
+ "label": "Коды двух аэропортов",
+ "join": "Из ODS выбирается одна строка на `route_no` с наибольшим `validity` (поле TEXT). LEFT JOIN по `flt.route_no` дает коды вылета и прилета. Это отдельный путь от SCD2: дата рейса здесь не участвует. Если маршрута нет, оба airport_sk останутся NULL; DQ считает это аномалией, а не учебной заглушкой.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "ods.routes",
+ "columns": [
+ "route_no",
+ "validity",
+ "departure_airport",
+ "arrival_airport"
+ ]
+ },
+ {
+ "node": "ods.flights",
+ "columns": [
+ "route_no"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "departure_airport_sk",
+ "arrival_airport_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dds/fact_flight_sales_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "fact-5",
+ "from": "dds.dim_routes",
+ "to": "dds.fact_flight_sales",
+ "label": "Версия на дату рейса",
+ "join": "LEFT JOIN `rte.route_bk = flt.route_no` и `scheduled_departure::DATE >= valid_from`, а конец — `valid_to IS NULL` или дата `< valid_to`. Дает `route_sk` и `airplane_code` для поиска модели. Нет подходящей версии — route_sk и airplane_sk NULL, строка факта сохраняется. На main это ожидается до выполнения задания.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "dds.dim_routes",
+ "columns": [
+ "route_bk",
+ "route_sk",
+ "airplane_code",
+ "valid_from",
+ "valid_to"
+ ]
+ },
+ {
+ "node": "ods.flights",
+ "columns": [
+ "route_no",
+ "scheduled_departure"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "route_sk",
+ "airplane_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dds/fact_flight_sales_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "fact-6",
+ "from": "dds.dim_calendar",
+ "to": "dds.fact_flight_sales",
+ "label": "Дата вылета",
+ "join": "LEFT JOIN `date_actual = scheduled_departure::DATE` дает `calendar_sk`. Нет даты в календаре — SK будет NULL, строка факта сохраняется. Это не дата бронирования.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "dds.dim_calendar",
+ "columns": [
+ "date_actual",
+ "calendar_sk"
+ ]
+ },
+ {
+ "node": "ods.flights",
+ "columns": [
+ "scheduled_departure"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "calendar_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dds/fact_flight_sales_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "fact-7",
+ "from": "dds.dim_airports",
+ "to": "dds.fact_flight_sales",
+ "label": "Аэропорт вылета",
+ "join": "LEFT JOIN `dep.airport_bk = ods_rte.departure_airport` дает `departure_airport_sk`. Без аэропорта SK будет NULL, факт сохраняется. Код берется через ods.routes; пустой dim_routes этому не мешает.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "dds.dim_airports",
+ "columns": [
+ "airport_bk",
+ "airport_sk"
+ ]
+ },
+ {
+ "node": "ods.routes",
+ "columns": [
+ "departure_airport"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "departure_airport_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dds/fact_flight_sales_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "fact-8",
+ "from": "dds.dim_airports",
+ "to": "dds.fact_flight_sales",
+ "label": "Аэропорт прилета",
+ "join": "LEFT JOIN `arr.airport_bk = ods_rte.arrival_airport` дает `arrival_airport_sk`. Без аэропорта SK будет NULL, факт сохраняется. Это вторая роль того же измерения.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "dds.dim_airports",
+ "columns": [
+ "airport_bk",
+ "airport_sk"
+ ]
+ },
+ {
+ "node": "ods.routes",
+ "columns": [
+ "arrival_airport"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "arrival_airport_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dds/fact_flight_sales_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "fact-9",
+ "from": "dds.dim_airplanes",
+ "to": "dds.fact_flight_sales",
+ "label": "Модель выбранной версии",
+ "join": "LEFT JOIN `ap.airplane_bk = rte.airplane_code` дает `airplane_sk`. Код берется из выбранной по дате версии dim_routes. Без версии или модели SK будет NULL; факт сохраняется.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "dds.dim_airplanes",
+ "columns": [
+ "airplane_bk",
+ "airplane_sk"
+ ]
+ },
+ {
+ "node": "dds.dim_routes",
+ "columns": [
+ "airplane_code"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "airplane_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dds/fact_flight_sales_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "fact-10",
+ "from": "dds.dim_tariffs",
+ "to": "dds.fact_flight_sales",
+ "label": "Класс обслуживания",
+ "join": "LEFT JOIN `tar.fare_conditions = seg.fare_conditions` дает `tariff_sk`. Нет совпадения — факт сохраняется с NULL; последующая DQ факта считает такой NULL ошибкой.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "dds.dim_tariffs",
+ "columns": [
+ "fare_conditions",
+ "tariff_sk"
+ ]
+ },
+ {
+ "node": "ods.segments",
+ "columns": [
+ "fare_conditions"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "tariff_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dds/fact_flight_sales_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "fact-11",
+ "from": "dds.dim_passengers",
+ "to": "dds.fact_flight_sales",
+ "label": "Пассажир",
+ "join": "LEFT JOIN `pax.passenger_id = tkt.passenger_id` дает `passenger_sk`. Без пассажира факт сохраняется с NULL; на main это ожидается до выполнения задания.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "dds.dim_passengers",
+ "columns": [
+ "passenger_id",
+ "passenger_sk"
+ ]
+ },
+ {
+ "node": "ods.tickets",
+ "columns": [
+ "passenger_id"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "passenger_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dds/fact_flight_sales_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "fact-12",
+ "from": "ods.boarding_passes",
+ "to": "dds.fact_flight_sales",
+ "label": "Посадка и место",
+ "join": "LEFT JOIN по ОБОИМ полям `ticket_no` и `flight_id` дает `seat_no`; наличие талона означает `is_boarded = true`. Без талона `seat_no = NULL`, `is_boarded = false`, сегмент остается в факте.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "ods.boarding_passes",
+ "columns": [
+ "ticket_no",
+ "flight_id",
+ "seat_no"
+ ]
+ },
+ {
+ "node": "ods.segments",
+ "columns": [
+ "ticket_no",
+ "flight_id"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "seat_no",
+ "is_boarded"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dds/fact_flight_sales_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-dds.fact_flight_sales-dm.sales_report-45",
+ "from": "dds.fact_flight_sales",
+ "to": "dm.sales_report",
+ "label": "Агрегация сегментов",
+ "join": "Один день вылета × аэропорт вылета × аэропорт прилета × класс обслуживания. По HWM факта выбираются затронутые даты; за эти даты агрегируются все факты, выполняются UPDATE и INSERT. INNER JOIN к календарю, двум ролям аэропортов и тарифу: факты без этих совпадений не входят в агрегат. `tickets_sold = COUNT(*)`, `total_revenue = SUM(price)`.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "price",
+ "is_boarded"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dm/sales_report_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-dds.dim_calendar-dm.sales_report-46",
+ "from": "dds.dim_calendar",
+ "to": "dm.sales_report",
+ "label": "Измерение для витрины",
+ "join": "Дает дату и календарные признаки по calendar_sk. INNER JOIN: без совпадения факт исключается из агрегата.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "dds.dim_calendar",
+ "columns": [
+ "calendar_sk"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "calendar_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dm/sales_report_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-dds.dim_airports-dm.sales_report-47",
+ "from": "dds.dim_airports",
+ "to": "dm.sales_report",
+ "label": "Измерение для витрины",
+ "join": "Дает коды и города для departure_airport_sk и arrival_airport_sk; один справочник используется дважды. INNER JOIN: без совпадения факт исключается из агрегата.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "dds.dim_airports",
+ "columns": [
+ "airport_sk"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "departure_airport_sk",
+ "arrival_airport_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dm/sales_report_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-dds.dim_tariffs-dm.sales_report-48",
+ "from": "dds.dim_tariffs",
+ "to": "dm.sales_report",
+ "label": "Измерение для витрины",
+ "join": "Дает класс обслуживания по tariff_sk. INNER JOIN: без совпадения факт исключается из агрегата.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "dds.dim_tariffs",
+ "columns": [
+ "tariff_sk"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "tariff_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dm/sales_report_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-dds.fact_flight_sales-dm.airport_traffic-49",
+ "from": "dds.fact_flight_sales",
+ "to": "dm.airport_traffic",
+ "label": "Агрегация сегментов",
+ "join": "Один аэропорт за день. По ТЗ: пересчитывать затронутые дни. Каждый факт участвует в двух ролях. Выручку вылетов и прилетов нельзя складывать: получится двойной счет. Дата обеих ролей берется из календаря факта (дата вылета). Без фактов нет входных строк для агрегата.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "price",
+ "is_boarded"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#31-dmairport_traffic"
+ }
+ ]
+ },
+ {
+ "id": "flow-dds.dim_calendar-dm.airport_traffic-50",
+ "from": "dds.dim_calendar",
+ "to": "dm.airport_traffic",
+ "label": "Измерение для витрины",
+ "join": "Дает дату и календарные признаки по calendar_sk. Ожидаемая связь по ТЗ; загрузка пока заглушка. Тип JOIN и общее поведение при пропуске измерения ТЗ не задает.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "dds.dim_calendar",
+ "columns": [
+ "calendar_sk"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "calendar_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#31-dmairport_traffic"
+ }
+ ]
+ },
+ {
+ "id": "flow-dds.dim_airports-dm.airport_traffic-51",
+ "from": "dds.dim_airports",
+ "to": "dm.airport_traffic",
+ "label": "Измерение для витрины",
+ "join": "Дает коды и города для departure_airport_sk и arrival_airport_sk; один справочник используется дважды. Ожидаемая связь по ТЗ; загрузка пока заглушка. Тип JOIN и общее поведение при пропуске измерения ТЗ не задает.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "dds.dim_airports",
+ "columns": [
+ "airport_sk"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "departure_airport_sk",
+ "arrival_airport_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#31-dmairport_traffic"
+ }
+ ]
+ },
+ {
+ "id": "flow-dds.fact_flight_sales-dm.route_performance-52",
+ "from": "dds.fact_flight_sales",
+ "to": "dm.route_performance",
+ "label": "Агрегация сегментов",
+ "join": "Один маршрут за всю историю, объединяя его версии. По ТЗ: полный пересчет; факты всех версий группируются по бизнес-ключу маршрута, описания берутся из открытой версии. Без фактов нет входных строк для агрегата.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "price",
+ "is_boarded"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#32-dmroute_performance"
+ }
+ ]
+ },
+ {
+ "id": "flow-dds.dim_routes-dm.route_performance-53",
+ "from": "dds.dim_routes",
+ "to": "dm.route_performance",
+ "label": "Измерение для витрины",
+ "join": "Дает route_bk по route_sk для объединения версий маршрута. Ожидаемая связь по ТЗ; загрузка пока заглушка. Тип JOIN и общее поведение при пропуске измерения ТЗ не задает.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "dds.dim_routes",
+ "columns": [
+ "route_sk"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "route_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#32-dmroute_performance"
+ }
+ ]
+ },
+ {
+ "id": "flow-dds.dim_calendar-dm.route_performance-54",
+ "from": "dds.dim_calendar",
+ "to": "dm.route_performance",
+ "label": "Измерение для витрины",
+ "join": "Дает дату и календарные признаки по calendar_sk. Ожидаемая связь по ТЗ; загрузка пока заглушка. Тип JOIN и общее поведение при пропуске измерения ТЗ не задает.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "dds.dim_calendar",
+ "columns": [
+ "calendar_sk"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "calendar_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#32-dmroute_performance"
+ }
+ ]
+ },
+ {
+ "id": "flow-dds.fact_flight_sales-dm.monthly_overview-55",
+ "from": "dds.fact_flight_sales",
+ "to": "dm.monthly_overview",
+ "label": "Агрегация сегментов",
+ "join": "Одна модель самолета за месяц. По ТЗ: пересчитывать затронутые месяцы; сначала считать показатели рейса, затем месяца. Без фактов нет входных строк для агрегата.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "price",
+ "is_boarded"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#33-dmmonthly_overview"
+ }
+ ]
+ },
+ {
+ "id": "flow-dds.dim_calendar-dm.monthly_overview-56",
+ "from": "dds.dim_calendar",
+ "to": "dm.monthly_overview",
+ "label": "Измерение для витрины",
+ "join": "Дает дату и календарные признаки по calendar_sk. Ожидаемая связь по ТЗ; загрузка пока заглушка. Тип JOIN и общее поведение при пропуске измерения ТЗ не задает.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "dds.dim_calendar",
+ "columns": [
+ "calendar_sk"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "calendar_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#33-dmmonthly_overview"
+ }
+ ]
+ },
+ {
+ "id": "flow-dds.dim_airplanes-dm.monthly_overview-57",
+ "from": "dds.dim_airplanes",
+ "to": "dm.monthly_overview",
+ "label": "Измерение для витрины",
+ "join": "Дает название модели и вместимость по airplane_sk. Ожидаемая связь по ТЗ; загрузка пока заглушка. Тип JOIN и общее поведение при пропуске измерения ТЗ не задает.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "dds.dim_airplanes",
+ "columns": [
+ "airplane_sk"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "airplane_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#33-dmmonthly_overview"
+ }
+ ]
+ },
+ {
+ "id": "flow-dds.dim_routes-dm.monthly_overview-58",
+ "from": "dds.dim_routes",
+ "to": "dm.monthly_overview",
+ "label": "Измерение для витрины",
+ "join": "Дает route_bk по route_sk для объединения версий маршрута. Ожидаемая связь по ТЗ; загрузка пока заглушка. Тип JOIN и общее поведение при пропуске измерения ТЗ не задает.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "dds.dim_routes",
+ "columns": [
+ "route_sk"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "route_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#33-dmmonthly_overview"
+ }
+ ]
+ },
+ {
+ "id": "flow-dds.fact_flight_sales-dm.passenger_loyalty-59",
+ "from": "dds.fact_flight_sales",
+ "to": "dm.passenger_loyalty",
+ "label": "Агрегация сегментов",
+ "join": "Один пассажир за всю историю. По ТЗ: пересчитывать всю историю пассажиров, чьи факты изменились. Строки без `passenger_sk` исключаются. Без фактов нет входных строк для агрегата.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "price",
+ "is_boarded"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#34-dmpassenger_loyalty"
+ }
+ ]
+ },
+ {
+ "id": "flow-dds.dim_passengers-dm.passenger_loyalty-60",
+ "from": "dds.dim_passengers",
+ "to": "dm.passenger_loyalty",
+ "label": "Измерение для витрины",
+ "join": "Дает исходный идентификатор и имя по passenger_sk. По ТЗ факты с NULL passenger_sk исключаются; загрузка пока заглушка.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "dds.dim_passengers",
+ "columns": [
+ "passenger_sk"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "passenger_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#34-dmpassenger_loyalty"
+ }
+ ]
+ },
+ {
+ "id": "flow-dds.dim_tariffs-dm.passenger_loyalty-61",
+ "from": "dds.dim_tariffs",
+ "to": "dm.passenger_loyalty",
+ "label": "Измерение для витрины",
+ "join": "Дает класс обслуживания по tariff_sk. Ожидаемая связь по ТЗ; загрузка пока заглушка. Тип JOIN и общее поведение при пропуске измерения ТЗ не задает.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "dds.dim_tariffs",
+ "columns": [
+ "tariff_sk"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "tariff_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#34-dmpassenger_loyalty"
+ }
+ ]
+ },
+ {
+ "id": "flow-dds.dim_routes-dm.passenger_loyalty-62",
+ "from": "dds.dim_routes",
+ "to": "dm.passenger_loyalty",
+ "label": "Измерение для витрины",
+ "join": "Дает route_bk по route_sk для объединения версий маршрута. Ожидаемая связь по ТЗ; загрузка пока заглушка. Тип JOIN и общее поведение при пропуске измерения ТЗ не задает.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "dds.dim_routes",
+ "columns": [
+ "route_sk"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "route_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#34-dmpassenger_loyalty"
+ }
+ ]
+ },
+ {
+ "id": "flow-dds.dim_calendar-dm.passenger_loyalty-63",
+ "from": "dds.dim_calendar",
+ "to": "dm.passenger_loyalty",
+ "label": "Измерение для витрины",
+ "join": "Дает дату и календарные признаки по calendar_sk. Ожидаемая связь по ТЗ; загрузка пока заглушка. Тип JOIN и общее поведение при пропуске измерения ТЗ не задает.",
+ "mode": "flow",
+ "status": "student",
+ "refs": [
+ {
+ "node": "dds.dim_calendar",
+ "columns": [
+ "calendar_sk"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "calendar_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../assignment/analyst_spec.md#34-dmpassenger_loyalty"
+ }
+ ]
+ },
+ {
+ "id": "keys-bookings.bookings-bookings.tickets-64",
+ "from": "bookings.bookings",
+ "to": "bookings.tickets",
+ "label": "Бронирование → билеты",
+ "join": "`bookings.book_ref = tickets.book_ref`: одно бронирование объединяет несколько билетов. Само сопоставление ничего не загружает. Это логическая связь, не объявление FOREIGN KEY и не выбор типа JOIN.",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.bookings",
+ "columns": [
+ "book_ref"
+ ]
+ },
+ {
+ "node": "bookings.tickets",
+ "columns": [
+ "book_ref"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../bookings/README.md"
+ }
+ ]
+ },
+ {
+ "id": "keys-bookings.tickets-bookings.segments-65",
+ "from": "bookings.tickets",
+ "to": "bookings.segments",
+ "label": "Билет → сегменты",
+ "join": "`tickets.ticket_no = segments.ticket_no`: билет с пересадкой дает несколько сегментов. Это логическая связь, не объявление FOREIGN KEY и не выбор типа JOIN.",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.tickets",
+ "columns": [
+ "ticket_no"
+ ]
+ },
+ {
+ "node": "bookings.segments",
+ "columns": [
+ "ticket_no"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../bookings/README.md"
+ }
+ ]
+ },
+ {
+ "id": "keys-bookings.flights-bookings.segments-66",
+ "from": "bookings.flights",
+ "to": "bookings.segments",
+ "label": "Рейс → сегменты",
+ "join": "`flights.flight_id = segments.flight_id`: один рейс связан с сегментами разных билетов. Это логическая связь, не объявление FOREIGN KEY и не выбор типа JOIN.",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.flights",
+ "columns": [
+ "flight_id"
+ ]
+ },
+ {
+ "node": "bookings.segments",
+ "columns": [
+ "flight_id"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../bookings/README.md"
+ }
+ ]
+ },
+ {
+ "id": "keys-bookings.segments-bookings.boarding_passes-67",
+ "from": "bookings.segments",
+ "to": "bookings.boarding_passes",
+ "label": "Сегмент → талон",
+ "join": "Совпадают `ticket_no` И `flight_id`. У сегмента может не быть талона; талон другого рейса того же билета не подходит. Это логическая связь, не объявление FOREIGN KEY и не выбор типа JOIN.",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.segments",
+ "columns": [
+ "ticket_no",
+ "flight_id"
+ ]
+ },
+ {
+ "node": "bookings.boarding_passes",
+ "columns": [
+ "ticket_no",
+ "flight_id"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../bookings/README.md"
+ }
+ ]
+ },
+ {
+ "id": "keys-bookings.routes-bookings.flights-68",
+ "from": "bookings.routes",
+ "to": "bookings.flights",
+ "label": "Маршрут → рейсы",
+ "join": "`routes.route_no = flights.route_no` связывает рейс с маршрутом, но route_no в routes не уникален: есть варианты validity. Простой JOIN только по номеру может размножить рейс. В сборке факта ODS-маршрут сначала сокращается до одной строки. Это логическая связь, не объявление FOREIGN KEY и не выбор типа JOIN.",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.routes",
+ "columns": [
+ "route_no",
+ "validity"
+ ]
+ },
+ {
+ "node": "bookings.flights",
+ "columns": [
+ "route_no"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../bookings/README.md"
+ }
+ ]
+ },
+ {
+ "id": "keys-bookings.airports_data-bookings.routes-69",
+ "from": "bookings.airports_data",
+ "to": "bookings.routes",
+ "label": "Аэропорт вылета → маршруты",
+ "join": "`airports_data.airport_code = routes.departure_airport`: один аэропорт является местом вылета многих маршрутов. Это логическая связь, не объявление FOREIGN KEY и не выбор типа JOIN.",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.airports_data",
+ "columns": [
+ "airport_code"
+ ]
+ },
+ {
+ "node": "bookings.routes",
+ "columns": [
+ "departure_airport"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../bookings/README.md"
+ }
+ ]
+ },
+ {
+ "id": "keys-bookings.airports_data-bookings.routes-70",
+ "from": "bookings.airports_data",
+ "to": "bookings.routes",
+ "label": "Аэропорт прилета → маршруты",
+ "join": "`airports_data.airport_code = routes.arrival_airport`: тот же справочник играет роль места прилета. Это логическая связь, не объявление FOREIGN KEY и не выбор типа JOIN.",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.airports_data",
+ "columns": [
+ "airport_code"
+ ]
+ },
+ {
+ "node": "bookings.routes",
+ "columns": [
+ "arrival_airport"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../bookings/README.md"
+ }
+ ]
+ },
+ {
+ "id": "keys-bookings.airplanes_data-bookings.seats-71",
+ "from": "bookings.airplanes_data",
+ "to": "bookings.seats",
+ "label": "Модель → места",
+ "join": "`airplanes_data.airplane_code = seats.airplane_code`: одной модели соответствует много мест. Номер seat_no сам по себе модель не определяет. Это логическая связь, не объявление FOREIGN KEY и не выбор типа JOIN.",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.airplanes_data",
+ "columns": [
+ "airplane_code"
+ ]
+ },
+ {
+ "node": "bookings.seats",
+ "columns": [
+ "airplane_code",
+ "seat_no"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../bookings/README.md"
+ }
+ ]
+ },
+ {
+ "id": "keys-bookings.airplanes_data-bookings.routes-72",
+ "from": "bookings.airplanes_data",
+ "to": "bookings.routes",
+ "label": "Модель → маршруты",
+ "join": "`airplanes_data.airplane_code = routes.airplane_code`: одна модель используется на разных маршрутах. Это логическая связь, не объявление FOREIGN KEY и не выбор типа JOIN.",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "bookings.airplanes_data",
+ "columns": [
+ "airplane_code"
+ ]
+ },
+ {
+ "node": "bookings.routes",
+ "columns": [
+ "airplane_code"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../bookings/README.md"
+ }
+ ]
+ },
+ {
+ "id": "keys-ods.bookings-ods.tickets-73",
+ "from": "ods.bookings",
+ "to": "ods.tickets",
+ "label": "Бронирование → билеты",
+ "join": "`bookings.book_ref = tickets.book_ref`: одно бронирование объединяет несколько билетов. Само сопоставление ничего не загружает. Это логическая связь, не объявление FOREIGN KEY и не выбор типа JOIN.",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "ods.bookings",
+ "columns": [
+ "book_ref"
+ ]
+ },
+ {
+ "node": "ods.tickets",
+ "columns": [
+ "book_ref"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/ods/tickets_ddl.sql"
+ }
+ ]
+ },
+ {
+ "id": "keys-ods.tickets-ods.segments-74",
+ "from": "ods.tickets",
+ "to": "ods.segments",
+ "label": "Билет → сегменты",
+ "join": "`tickets.ticket_no = segments.ticket_no`: билет с пересадкой дает несколько сегментов. Это логическая связь, не объявление FOREIGN KEY и не выбор типа JOIN.",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "ods.tickets",
+ "columns": [
+ "ticket_no"
+ ]
+ },
+ {
+ "node": "ods.segments",
+ "columns": [
+ "ticket_no"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/ods/segments_ddl.sql"
+ }
+ ]
+ },
+ {
+ "id": "keys-ods.flights-ods.segments-75",
+ "from": "ods.flights",
+ "to": "ods.segments",
+ "label": "Рейс → сегменты",
+ "join": "`flights.flight_id = segments.flight_id`: один рейс связан с сегментами разных билетов. Это логическая связь, не объявление FOREIGN KEY и не выбор типа JOIN.",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "ods.flights",
+ "columns": [
+ "flight_id"
+ ]
+ },
+ {
+ "node": "ods.segments",
+ "columns": [
+ "flight_id"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/ods/segments_ddl.sql"
+ }
+ ]
+ },
+ {
+ "id": "keys-ods.segments-ods.boarding_passes-76",
+ "from": "ods.segments",
+ "to": "ods.boarding_passes",
+ "label": "Сегмент → талон",
+ "join": "Совпадают `ticket_no` И `flight_id`. У сегмента может не быть талона; талон другого рейса того же билета не подходит. Это логическая связь, не объявление FOREIGN KEY и не выбор типа JOIN.",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "ods.segments",
+ "columns": [
+ "ticket_no",
+ "flight_id"
+ ]
+ },
+ {
+ "node": "ods.boarding_passes",
+ "columns": [
+ "ticket_no",
+ "flight_id"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/ods/boarding_passes_ddl.sql"
+ }
+ ]
+ },
+ {
+ "id": "keys-ods.routes-ods.flights-77",
+ "from": "ods.routes",
+ "to": "ods.flights",
+ "label": "Маршрут → рейсы",
+ "join": "`routes.route_no = flights.route_no` связывает рейс с маршрутом, но route_no в routes не уникален: есть варианты validity. Простой JOIN только по номеру может размножить рейс. В сборке факта ODS-маршрут сначала сокращается до одной строки. Это логическая связь, не объявление FOREIGN KEY и не выбор типа JOIN.",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "ods.routes",
+ "columns": [
+ "route_no",
+ "validity"
+ ]
+ },
+ {
+ "node": "ods.flights",
+ "columns": [
+ "route_no"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/ods/flights_ddl.sql"
+ }
+ ]
+ },
+ {
+ "id": "keys-ods.airports-ods.routes-78",
+ "from": "ods.airports",
+ "to": "ods.routes",
+ "label": "Аэропорт вылета → маршруты",
+ "join": "`airports.airport_code = routes.departure_airport`: один аэропорт является местом вылета многих маршрутов. Это логическая связь, не объявление FOREIGN KEY и не выбор типа JOIN.",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "ods.airports",
+ "columns": [
+ "airport_code"
+ ]
+ },
+ {
+ "node": "ods.routes",
+ "columns": [
+ "departure_airport"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/ods/routes_ddl.sql"
+ }
+ ]
+ },
+ {
+ "id": "keys-ods.airports-ods.routes-79",
+ "from": "ods.airports",
+ "to": "ods.routes",
+ "label": "Аэропорт прилета → маршруты",
+ "join": "`airports.airport_code = routes.arrival_airport`: тот же справочник играет роль места прилета. Это логическая связь, не объявление FOREIGN KEY и не выбор типа JOIN.",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "ods.airports",
+ "columns": [
+ "airport_code"
+ ]
+ },
+ {
+ "node": "ods.routes",
+ "columns": [
+ "arrival_airport"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/ods/routes_ddl.sql"
+ }
+ ]
+ },
+ {
+ "id": "keys-ods.airplanes-ods.seats-80",
+ "from": "ods.airplanes",
+ "to": "ods.seats",
+ "label": "Модель → места",
+ "join": "`airplanes.airplane_code = seats.airplane_code`: одной модели соответствует много мест. Номер seat_no сам по себе модель не определяет. Это логическая связь, не объявление FOREIGN KEY и не выбор типа JOIN.",
+ "mode": "keys",
+ "status": "student",
+ "refs": [
+ {
+ "node": "ods.airplanes",
+ "columns": [
+ "airplane_code"
+ ]
+ },
+ {
+ "node": "ods.seats",
+ "columns": [
+ "airplane_code",
+ "seat_no"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../../sql/ods/seats_ddl.sql"
+ }
+ ]
+ },
+ {
+ "id": "keys-ods.airplanes-ods.routes-81",
+ "from": "ods.airplanes",
+ "to": "ods.routes",
+ "label": "Модель → маршруты",
+ "join": "`airplanes.airplane_code = routes.airplane_code`: одна модель используется на разных маршрутах. Это логическая связь, не объявление FOREIGN KEY и не выбор типа JOIN.",
+ "mode": "keys",
+ "status": "student",
+ "refs": [
+ {
+ "node": "ods.airplanes",
+ "columns": [
+ "airplane_code"
+ ]
+ },
+ {
+ "node": "ods.routes",
+ "columns": [
+ "airplane_code"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "ТЗ",
+ "url": "../../sql/ods/routes_ddl.sql"
+ }
+ ]
+ },
+ {
+ "id": "keys-dds.dim_calendar-dds.fact_flight_sales-82",
+ "from": "dds.dim_calendar",
+ "to": "dds.fact_flight_sales",
+ "label": "Дата вылета",
+ "join": "`dim_calendar.calendar_sk = fact_flight_sales.calendar_sk`: одна строка измерения описывает много сегментов. LEFT JOIN при вставке факта допускает незаполненный ключ; подробный путь получения SK показан в виде «Поток данных».",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "dds.dim_calendar",
+ "columns": [
+ "calendar_sk"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "calendar_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dds/fact_flight_sales_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "keys-dds.dim_airports-dds.fact_flight_sales-83",
+ "from": "dds.dim_airports",
+ "to": "dds.fact_flight_sales",
+ "label": "Аэропорт вылета",
+ "join": "`dim_airports.airport_sk = fact_flight_sales.departure_airport_sk`: одна строка измерения описывает много сегментов. LEFT JOIN при вставке факта допускает незаполненный ключ; подробный путь получения SK показан в виде «Поток данных».",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "dds.dim_airports",
+ "columns": [
+ "airport_sk"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "departure_airport_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dds/fact_flight_sales_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "keys-dds.dim_airports-dds.fact_flight_sales-84",
+ "from": "dds.dim_airports",
+ "to": "dds.fact_flight_sales",
+ "label": "Аэропорт прилета",
+ "join": "`dim_airports.airport_sk = fact_flight_sales.arrival_airport_sk`: одна строка измерения описывает много сегментов. LEFT JOIN при вставке факта допускает незаполненный ключ; подробный путь получения SK показан в виде «Поток данных».",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "dds.dim_airports",
+ "columns": [
+ "airport_sk"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "arrival_airport_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dds/fact_flight_sales_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "keys-dds.dim_airplanes-dds.fact_flight_sales-85",
+ "from": "dds.dim_airplanes",
+ "to": "dds.fact_flight_sales",
+ "label": "Модель самолета",
+ "join": "`dim_airplanes.airplane_sk = fact_flight_sales.airplane_sk`: одна строка измерения описывает много сегментов. LEFT JOIN при вставке факта допускает незаполненный ключ; подробный путь получения SK показан в виде «Поток данных».",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "dds.dim_airplanes",
+ "columns": [
+ "airplane_sk"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "airplane_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dds/fact_flight_sales_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "keys-dds.dim_tariffs-dds.fact_flight_sales-86",
+ "from": "dds.dim_tariffs",
+ "to": "dds.fact_flight_sales",
+ "label": "Класс обслуживания",
+ "join": "`dim_tariffs.tariff_sk = fact_flight_sales.tariff_sk`: одна строка измерения описывает много сегментов. LEFT JOIN при вставке факта допускает незаполненный ключ; подробный путь получения SK показан в виде «Поток данных».",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "dds.dim_tariffs",
+ "columns": [
+ "tariff_sk"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "tariff_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dds/fact_flight_sales_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "keys-dds.dim_passengers-dds.fact_flight_sales-87",
+ "from": "dds.dim_passengers",
+ "to": "dds.fact_flight_sales",
+ "label": "Пассажир",
+ "join": "`dim_passengers.passenger_sk = fact_flight_sales.passenger_sk`: одна строка измерения описывает много сегментов. LEFT JOIN при вставке факта допускает незаполненный ключ; подробный путь получения SK показан в виде «Поток данных».",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "dds.dim_passengers",
+ "columns": [
+ "passenger_sk"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "passenger_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dds/fact_flight_sales_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "keys-dds.dim_routes-dds.fact_flight_sales-88",
+ "from": "dds.dim_routes",
+ "to": "dds.fact_flight_sales",
+ "label": "Версия маршрута",
+ "join": "`dim_routes.route_sk = fact_flight_sales.route_sk`: одна строка измерения описывает много сегментов. LEFT JOIN при вставке факта допускает незаполненный ключ; подробный путь получения SK показан в виде «Поток данных».",
+ "mode": "keys",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "dds.dim_routes",
+ "columns": [
+ "route_sk"
+ ]
+ },
+ {
+ "node": "dds.fact_flight_sales",
+ "columns": [
+ "route_sk"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/dds/fact_flight_sales_load.sql"
+ }
+ ]
+ },
+ {
+ "id": "flow-ods.segments-ods.flights-89",
+ "from": "ods.segments",
+ "to": "ods.flights",
+ "label": "Граница отбора сегментов",
+ "join": "`MAX(ods.segments._load_ts)` задает нижнюю границу новых `stg.segments`. Их `flight_id` помогают добрать рейсы из истории STG. Это чтение водяного знака, а не загрузка рейсов из сегментов и не зависимость задач.",
+ "mode": "flow",
+ "status": "ready",
+ "refs": [
+ {
+ "node": "ods.segments",
+ "columns": [
+ "_load_ts"
+ ]
+ },
+ {
+ "node": "stg.segments",
+ "columns": [
+ "_load_ts",
+ "flight_id"
+ ]
+ }
+ ],
+ "links": [
+ {
+ "label": "SQL",
+ "url": "../../sql/ods/flights_load.sql"
+ }
+ ]
+ }
+ ]
+}
diff --git a/docs/design/architecture-map/render.py b/docs/design/architecture-map/render.py
new file mode 100644
index 0000000..32d92fd
--- /dev/null
+++ b/docs/design/architecture-map/render.py
@@ -0,0 +1,160 @@
+# /// script
+# requires-python = ">=3.11"
+# dependencies = []
+# ///
+
+"""Проверить данные карты и собрать автономный HTML без внешних зависимостей."""
+
+from __future__ import annotations
+
+import argparse
+import json
+import re
+from pathlib import Path
+from urllib.parse import unquote, urlsplit
+
+HERE = Path(__file__).resolve().parent
+ROOT = HERE.parents[2]
+OUTPUT = HERE.parent / "architecture-map.html"
+MARKER = "/*__MAP_DATA__*/"
+NOTICE = (
+ "\n"
+)
+
+
+def headings(text: str) -> set[str]:
+ """Якоря Markdown для ссылок на существующие разделы документации."""
+ result = set(re.findall(r' None:
+ """Сверить объекты, поля, статусы, входы готовых SQL и локальные ссылки."""
+ nodes = {node["id"]: node for node in data["nodes"]}
+ if len(nodes) != len(data["nodes"]):
+ raise ValueError("Повторяется идентификатор объекта")
+ edge_ids = [e["id"] for e in data["edges"]]
+ if len(set(edge_ids)) != len(edge_ids):
+ raise ValueError("Повторяется идентификатор связи")
+ columns, externals, tables = {}, {}, set()
+ for path in sorted((ROOT / "sql").glob("*/*_ddl.sql")):
+ text = re.sub(r"--[^\n]*", "", path.read_text(encoding="utf-8"))
+ pattern = r"CREATE\s+(EXTERNAL\s+)?TABLE\s+(?:IF NOT EXISTS\s+)?(\w+\.\w+)\s*\((.*?)\n\)"
+ for external, table, body in re.findall(pattern, text, re.S | re.I):
+ columns[table] = set(re.findall(r"^\s*(\w+)\s+\w+", body, re.M))
+ if external:
+ source = re.search(r"pxf://([^?]+)", text)[1]
+ columns[source] = columns[table]
+ externals[table] = source
+ else:
+ tables.add(table)
+ expected = tables | set(externals.values())
+ if set(nodes) != expected:
+ raise ValueError(f"Карта и DDL расходятся: {set(nodes) ^ expected}")
+ parts = {part for n in nodes.values() for part in n["parts"]}
+ if parts != set(externals):
+ raise ValueError("В карточках STG должны быть представлены все внешние таблицы")
+ layers = {layer["id"] for layer in data["layers"]}
+ positions = set()
+ for node in nodes.values():
+ if node["layer"] not in layers:
+ raise ValueError(f"Неизвестный слой: {node['id']}")
+ position = (node["layer"], node["order"])
+ if position in positions:
+ raise ValueError(f"Объекты перекрываются: {position}")
+ positions.add(position)
+ for field in ("what", "grain", "keys", "fields", "fill", "links"):
+ if not node[field]:
+ raise ValueError(f"Пустое поле {field}: {node['id']}")
+ for field in node["fields"]:
+ if field["name"] not in columns[node["id"]]:
+ raise ValueError(f"Нет поля {node['id']}.{field['name']} в DDL/PXF")
+ if node["layer"] == "source":
+ continue
+ layer, name = node["id"].split(".")
+ for role in ("load", "dq"):
+ path = ROOT / f"sql/{layer}/{name}_{role}.sql"
+ sql = re.sub(r"--[^\n]*", "", path.read_text(encoding="utf-8")).strip()
+ stub = sql.upper() == "SELECT 1;"
+ if stub != (node["status"] == "student"):
+ raise ValueError(
+ f"Статус карты не совпадает с {path.relative_to(ROOT)}"
+ )
+ if role != "load" or stub:
+ continue
+ sources = set(re.findall(r"\b(?:FROM|JOIN)\s+(\w+\.\w+)", sql, re.I))
+ sources = {externals.get(s, s) for s in sources if s != node["id"]}
+ sources &= expected
+ drawn = {
+ e["from"]
+ for e in data["edges"]
+ if e["mode"] == "flow" and e["to"] == node["id"]
+ }
+ if sources != drawn:
+ raise ValueError(
+ f"Входы SQL и карты расходятся у {node['id']}: {sources ^ drawn}"
+ )
+ for edge in data["edges"]:
+ if edge["from"] not in nodes or edge["to"] not in nodes:
+ raise ValueError(f"Неизвестный конец связи {edge['id']}")
+ if edge["mode"] not in ("flow", "keys") or not edge["join"] or not edge["refs"]:
+ raise ValueError(f"Неполное описание связи {edge['id']}")
+ for ref in edge["refs"]:
+ if (
+ ref["node"] not in nodes
+ or not set(ref["columns"]) <= columns[ref["node"]]
+ ):
+ raise ValueError(f"Неизвестные поля связи {edge['id']}: {ref}")
+ count = 0
+ for item in [*nodes.values(), *data["edges"]]:
+ for link in item["links"]:
+ url = urlsplit(link["url"])
+ if url.scheme or url.netloc:
+ raise ValueError(f"Ссылка должна вести в локальный репозиторий: {link}")
+ path = (OUTPUT.parent / unquote(url.path)).resolve()
+ if not path.is_relative_to(ROOT) or not path.is_file():
+ raise ValueError(f"Файл по ссылке не найден: {link['url']}")
+ if url.fragment and unquote(url.fragment) not in headings(
+ path.read_text(encoding="utf-8")
+ ):
+ raise ValueError(f"Раздел по ссылке не найден: {link['url']}")
+ count += 1
+ print(
+ f"Проверены {len(nodes)} объектов, {len(edge_ids)} связей, {count} ссылок; поля, статусы и входы SQL совпадают"
+ )
+
+
+def main() -> int:
+ """Собрать HTML либо проверить его свежесть вместе с исходными данными."""
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--check", action="store_true", help="проверить без записи")
+ args = parser.parse_args()
+ try:
+ data = json.loads((HERE / "map.json").read_text(encoding="utf-8"))
+ validate(data)
+ template = (HERE / "template.html").read_text(encoding="utf-8")
+ if template.count(MARKER) != 1:
+ raise ValueError("В шаблоне должен быть ровно один маркер данных")
+ payload = json.dumps(data, ensure_ascii=False, indent=2).replace("<", "\\u003c")
+ result = NOTICE + template.replace(MARKER, payload)
+ if args.check:
+ if not OUTPUT.exists() or OUTPUT.read_text(encoding="utf-8") != result:
+ raise ValueError(
+ "HTML устарел. Выполните uv run docs/design/architecture-map/render.py"
+ )
+ print("HTML соответствует исходникам")
+ else:
+ OUTPUT.write_text(result, encoding="utf-8")
+ print(f"Собрана карта: {OUTPUT.relative_to(ROOT)}")
+ return 0
+ except (ValueError, KeyError, OSError) as exc:
+ print(f"Не удалось собрать карту: {exc}")
+ return 1
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/docs/design/architecture-map/template.html b/docs/design/architecture-map/template.html
new file mode 100644
index 0000000..60cede3
--- /dev/null
+++ b/docs/design/architecture-map/template.html
@@ -0,0 +1,448 @@
+
+
+
+
+
+ Карта DWH авиаперевозок
+
+
+
+
+
+
+
+
+ Сплошная рамка: готово Пунктирная рамка: задание
+ Синий: источники Зеленый: потребители Оранжевый: выбор
+
+
+
+
+
+
+
+
+ Для интерактивной карты включите JavaScript. Объяснение модели доступно в руководстве .
+
+
+
diff --git a/docs/design/db_schema.md b/docs/design/db_schema.md
index 4eccb71..7accb56 100644
--- a/docs/design/db_schema.md
+++ b/docs/design/db_schema.md
@@ -1,399 +1,207 @@
-# Схема БД DWH (Bookings → Greenplum)
+# Как устроено хранилище авиаперевозок
-> **Статус:** Все слои реализованы в ветке `solution` (STG, ODS, DDS, DM).
-> На ветке `main` студенческие таблицы ODS (`airplanes`, `seats`), DDS-измерения
-> (`dim_airplanes`, `dim_passengers`, `dim_routes`) и студенческие DM-витрины
-> (`airport_traffic`, `route_performance`, `monthly_overview`, `passenger_loyalty`)
-> — заглушки (`SELECT 1;`). Данные появятся после реализации заданий.
+Разберем одну поездку: как бронирование связано с билетами и перелетами,
+какие строки попадут в факт и как сложится выручка по направлениям.
+Найти эти строки в структуре хранилища поможет [интерактивная карта](architecture-map.html).
+Карта хранит подробности объектов и связей, а это руководство задает маршрут чтения.
-Архитектура хранилища данных (DWH) для учебного проекта Airflow + Greenplum.
-Источник — демо-БД `bookings` (Postgres). Одна диаграмма отвечает на один вопрос:
-карта, которая показывает всё сразу, не даёт ясного ответа ни на один из них.
+## Как открыть карту
-Понять, как устроен проект → лесенка ниже.
+Из локальной копии репозитория откройте `docs/design/architecture-map.html`
+в браузере: через файловый менеджер или командой из корня проекта:
-Пишете свой load-скрипт и хотите свериться → [`reading_the_pipeline.md`](reading_the_pipeline.md).
+```bash
+xdg-open docs/design/architecture-map.html
+```
-Какие DQ-проверки писать → [`dq_taxonomy.md`](../reference/dq_taxonomy.md).
+На Windows можно открыть этот же файл в Проводнике. Сервер, интернет и работающий
+Docker для карты не нужны. Храните HTML рядом с остальными файлами репозитория:
+ссылки из карточек ведут в локальные SQL и Markdown.
+Браузер может показать Markdown как текст, а SQL предложить скачать.
+Скачанный файл можно открыть в редакторе; для форматированного чтения
+руководства используйте Markdown preview.
-Точечный вопрос «что кормит X?» → полная карта в конце.
+**В 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`.
+Для связи сегмента с талоном понадобятся оба поля ключа, иначе талон
+первого рейса можно ошибочно присоединить ко второму.
## Как устроен поток целиком?
-Здесь показано движение данных через пять слоёв DWH.
+Оставьте билет выбранным и переключитесь на **"Поток данных"**.
+Таблицы останутся на местах, выбранный билет сохранится; теперь стрелки
+показывают чтение данных загрузчиками. Синий цвет отмечает источники выбранной
+таблицы, зеленый - потребителей, оранжевый - сам выбор. Несвязанные объекты
+остаются видны приглушенными. Повторный выбор, **"Общий вид"** или `Esc`
+возвращает обзор.
-```mermaid
-graph LR
- SRC["Source Postgres bookings"] --> STG["STG сырые данные"]
- STG --> ODS["ODS очищенные данные и типы"]
- ODS --> DDS["DDS факты и измерения"]
- DDS --> DM["DM витрины для бизнес-вопросов"]
-```
+Проследите путь
+[сегмента](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: Сегмент обогащается ключами измерений в факте,
+ затем участвует в агрегатах витрины.
-Здесь показан факт и шесть его измерений.
-
-```mermaid
-graph TD
- CAL["dds.dim_calendar"] --> FACT["dds.fact_flight_sales"]
- AIRPORT["dds.dim_airports"] --> FACT
- AIRPLANE["dds.dim_airplanes"] --> FACT
- TARIFF["dds.dim_tariffs"] --> FACT
- PASSENGER["dds.dim_passengers"] --> FACT
- ROUTE["dds.dim_routes SCD2"] --> FACT
-```
+Стрелка данных не означает очередность задач Airflow. Например, календарь
+генерируется без чтения ODS, хотя в DDS DAG задачи других измерений ждут
+проверки календаря. Порядок выполнения смотрите по ссылке **"DAG · порядок задач"**
+в карточке. Поля, SQL и проверки раскрываются там же, после выбора таблицы.
## Как собирается факт?
-Здесь показаны таблицы, которые `fact_src` читает при сборке факта.
-Сплошные стрелки обозначают `INNER JOIN`. Пунктирные стрелки обозначают `LEFT JOIN`.
+Откройте [dds.fact_flight_sales](architecture-map.html#node=dds.fact_flight_sales).
+В нашем примере в нем будет **три строки**, по одной на каждый сегмент.
+Важные значения до выполнения студенческих заданий выглядят так:
-```mermaid
-graph LR
- SEG["ods.segments"] -->|"Main stream: grain, price"| FACT["dds.fact_flight_sales"]
- TKT["ods.tickets"] -->|"book_ref, passenger_id"| FACT
- BKG["ods.bookings"] -->|"book_date"| FACT
- FLT["ods.flights"] -->|"schedule, route_no"| FACT
- ODS_RTE["ods.routes"] -.->|"dep/arr airports"| FACT
- CAL["dds.dim_calendar"] -.->|"calendar_sk"| FACT
- AIRPORT["dds.dim_airports"] -.->|"dep/arr _sk"| FACT
- AIRPLANE["dds.dim_airplanes"] -.->|"airplane_sk"| FACT
- TARIFF["dds.dim_tariffs"] -.->|"tariff_sk"| FACT
- PASSENGER["dds.dim_passengers"] -.->|"passenger_sk"| FACT
- ROUTE["dds.dim_routes"] -.->|"route_sk"| FACT
- BOARDING["ods.boarding_passes"] -.->|"LEFT JOIN seat_no"| FACT
-```
+| 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 |
-`ods.routes` оставлен отдельным узлом: через него факт находит аэропорты
-вылета и прилёта, не завися от студенческого `dim_routes`. Схема показывает
-замысел загрузки. На ветке `main` поля `route_sk`, `airplane_sk` и
-`passenger_sk` могут быть `NULL`, пока студент не реализовал измерения.
-Почему это не всегда ошибка, объяснено в [справочнике DQ](../reference/dq_taxonomy.md#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).
+Зерно меняется: день вылета × два аэропорта × класс обслуживания.
+Три строки примера дадут две строки витрины:
-```mermaid
-graph LR
- FACT["dds.fact_flight_sales"] -->|"день × аэропорты × тариф"| SALES["dm.sales_report выручка и посадка"]
- FACT -->|"день × аэропорт"| TRAFFIC["dm.airport_traffic пассажиропоток"]
- FACT -->|"route_bk"| ROUTE["dm.route_performance эффективность маршрута"]
- FACT -->|"месяц × airplane_sk"| MONTHLY["dm.monthly_overview динамика по самолётам"]
- FACT -->|"passenger_sk"| LOYALTY["dm.passenger_loyalty профиль лояльности"]
-```
+| Дата вылета | Направление | Класс | tickets_sold | total_revenue |
+|---|---|---|---:|---:|
+| 1 сентября | SVO → LED | Economy | 2 | 7000 |
+| 1 сентября | LED → KZN | Economy | 1 | 5000 |
-Измерения уточняют даты, аэропорты, тарифы, маршруты, самолёты и пассажиров,
-но основной поток во все пять витрин начинается с `fact_flight_sales`.
+`tickets_sold` здесь считает **сегменты**. Один билет может участвовать несколько раз.
+Если сложить его по направлениям, получится 3 при двух билетах.
+`total_revenue` складывается из цен сегментов, поэтому итог равен 12000.
+Доля посадок рассчитывается по `is_boarded`.
----
+Готовая витрина использует календарь, две роли аэропорта и тариф через
+`INNER JOIN`: факт без совпадения в одном из этих измерений не попадет в агрегат.
+Поля `route_sk`, `airplane_sk`, `passenger_sk` ей не требуются, поэтому
+эталон работает до выполнения заданий. Остальные витрины видны рядом
+пунктирными рамками: в их карточках описан требуемый результат по ТЗ.
-## Сводка объектов по слоям
+## Модели самолетов и места: начало задания
-### STG (Staging)
+Откройте [dds.dim_airplanes](architecture-map.html#node=dds.dim_airplanes).
+У него два источника: `ods.airplanes` дает характеристики модели,
+а `ods.seats` - места, по которым считается вместимость.
+Строка измерения описывает **модель**, например тип самолета, а не отдельный борт.
+`seat_no` уникален только вместе с `airplane_code`.
-Сырые данные из источника, все бизнес-поля как TEXT. Хранение: AO Row (zstd).
-Служебные поля: `event_ts`, `_load_ts`, `_load_id`.
-
-| Таблица | Источник (PXF) | Ключ | Стратегия загрузки | Distribution |
-|---------|---------------|------|-------------------|--------------|
-| `stg.bookings` | `bookings.bookings` | `book_ref` | Инкремент (book_date) | `book_ref` |
-| `stg.tickets` | `bookings.tickets` | `ticket_no` | Инкремент (через bookings) | `book_ref` |
-| `stg.flights` | `bookings.flights` | `flight_id` | Инкремент (scheduled_departure) | `flight_id` |
-| `stg.segments` | `bookings.segments` | `(ticket_no, flight_id)` | Инкремент (через tickets) | `ticket_no` |
-| `stg.boarding_passes` | `bookings.boarding_passes` | `(ticket_no, flight_id)` | Инкремент (через tickets/bookings) | `ticket_no` |
-| `stg.airports` | `bookings.airports_data` | `airport_code` | Full snapshot | `airport_code` |
-| `stg.airplanes` | `bookings.airplanes_data` | `airplane_code` | Full snapshot | `airplane_code` |
-| `stg.routes` | `bookings.routes` | `(route_no, validity)` | Full snapshot | `route_no` |
-| `stg.seats` | `bookings.seats` | `(airplane_code, seat_no)` | Full snapshot | `airplane_code` |
-
-Детали: [`bookings_stg_design.md`](bookings_stg_design.md)
-
-### ODS (Operational Data Store)
-
-Очищенные данные с корректными типами. Справочники: AO Row (TRUNCATE+INSERT).
-Транзакции: Heap (SCD1 UPSERT). Служебные поля: `_load_id`, `_load_ts`,
-`event_ts` (для транзакционных таблиц).
-
-| Таблица | Стратегия | Storage | Ключевые преобразования |
-|---------|-----------|---------|------------------------|
-| `ods.bookings` | SCD1 UPSERT (HWM) | Heap | `total_amount` TEXT → NUMERIC |
-| `ods.tickets` | SCD1 UPSERT (HWM) | Heap | `outbound` TEXT → BOOLEAN |
-| `ods.flights` | SCD1 UPSERT (HWM) | Heap | TEXT → INTEGER, TIMESTAMP WITH TIME ZONE |
-| `ods.segments` | SCD1 UPSERT (HWM) | Heap | `price` TEXT → `amount` NUMERIC |
-| `ods.boarding_passes` | SCD1 UPSERT (HWM) | Heap | `boarding_no` TEXT → INTEGER |
-| `ods.airports` | TRUNCATE+INSERT | AO Row | JSON → отдельные поля (`airport_name`, `city`) |
-| `ods.airplanes` | TRUNCATE+INSERT | AO Row | `range`/`speed` TEXT → INTEGER |
-| `ods.routes` | TRUNCATE+INSERT | AO Row | `days_of_week` → INTEGER[], `scheduled_time` → TIME |
-| `ods.seats` | TRUNCATE+INSERT | AO Row | Без преобразований |
-
-Детали: [`bookings_ods_design.md`](bookings_ods_design.md)
-
-### DDS (Detailed Data Store — Star Schema)
-
-Измерения с суррогатными ключами. Факт в центре звезды.
-SK генерация: `MAX(sk) + ROW_NUMBER()` (безопасно при `concurrency=1`).
-
-#### Измерения
-
-| Измерение | BK | SK | SCD | Storage | Источник |
-|-----------|----|----|-----|---------|----------|
-| `dim_calendar` | `date_actual` | `calendar_sk` | Static | AO Row | Генерация (2016–2030) |
-| `dim_airports` | `airport_bk` | `airport_sk` | SCD1 | Heap | `ods.airports` |
-| `dim_airplanes` | `airplane_bk` | `airplane_sk` | SCD1 | Heap | `ods.airplanes` + `ods.seats` (total_seats) |
-| `dim_tariffs` | `fare_conditions` | `tariff_sk` | SCD1 | AO Row | `ods.segments` (DISTINCT) |
-| `dim_passengers` | `passenger_id` | `passenger_sk` | SCD1 | Heap | `ods.tickets` (дедупликация) |
-| `dim_routes` | `route_bk` | `route_sk` | **SCD2** | Heap | `ods.routes` + `dim_airports` + `dim_airplanes` |
-
-#### Факт
-
-`dds.fact_flight_sales` — зерно: 1 строка = 1 сегмент билета (`ticket_no` + `flight_id`).
-
-| FK | Lookup-путь | Примечание |
-|----|-------------|------------|
-| `calendar_sk` | `dim_calendar` | Дата вылета |
-| `departure_airport_sk` | `ods.routes` → `dim_airports` | Аэропорт вылета (эталонный справочник) |
-| `arrival_airport_sk` | `ods.routes` → `dim_airports` | Аэропорт прилёта (эталонный справочник) |
-| `airplane_sk` | `dim_routes` → `dim_airplanes` | Самолёт (point-in-time SCD2) |
-| `tariff_sk` | `dim_tariffs` | Тариф |
-| `passenger_sk` | `dim_passengers` | Пассажир |
-| `route_sk` | `dim_routes` | Версия маршрута (SCD2, point-in-time) |
-
-Метрики: `price` (NUMERIC), `is_boarded` (BOOLEAN).
-Degenerate keys: `book_ref`, `ticket_no`, `flight_id`, `book_date`, `seat_no`.
-
-Детали: [`bookings_dds_design.md`](bookings_dds_design.md)
-
-### DM (Data Marts — Витрины)
-
-Аналитические витрины поверх DDS. Каждая отвечает на конкретный бизнес-вопрос.
-
-| Витрина | Бизнес-вопрос | Зерно | Стратегия | Storage |
-|---------|--------------|-------|-----------|---------|
-| `dm.sales_report` | Выручка и boarding rate по направлениям/тарифам/дням | (flight_date, dep_sk, arr_sk, tariff_sk) | UPSERT (HWM) | Heap |
-| `dm.airport_traffic` | Пассажиропоток аэропортов по дням | (traffic_date, airport_sk) | UPSERT (HWM) | Heap |
-| `dm.route_performance` | Эффективность маршрутов за всё время | route_bk | Full Rebuild | AO Column (zstd) |
-| `dm.monthly_overview` | Помесячная динамика по типам самолётов | (year, month, airplane_sk) | UPSERT (HWM) | Heap |
-| `dm.passenger_loyalty` | Профиль лояльности пассажиров | passenger_sk | UPSERT (затронутые ключи) | Heap |
-
-Детали: [`bookings_dm_design.md`](bookings_dm_design.md)
-
----
+На `main` STG уже заполняется, а загрузки `ods.airplanes`, `ods.seats`
+и `dds.dim_airplanes` пока содержат `SELECT 1;`. Карточки помечены
+**"Задание"** и ведут к соответствующим пунктам ТЗ. Ожидаемые поля
+известны из DDL, но готовой загрузки этих объектов еще нет.
## Ключевые договорённости
-- **Нейминг полей**: [`naming_conventions.md`](naming_conventions.md)
-- **DQ-проверки**: SQL-скрипты с `RAISE EXCEPTION` (не отдельный DQ-слой).
- На ветке `main` студенческие DQ-скрипты (`airplanes_dq.sql`, `seats_dq.sql`,
- student dims/DM) — заглушки (`SELECT 1;`) без проверок.
-- **Инкремент STG**: для `tickets` опорная дата — из `bookings.book_date`
-- **Point-in-time JOIN**: факт ↔ `dim_routes` по `[valid_from, valid_to)`
-- **Суррогатные ключи**: `MAX(sk) + ROW_NUMBER()` (не SERIAL — GP-специфика)
+- Ключи на карте логические. В 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.
-### Глоссарий
-
-| Термин | Объяснение |
-|--------|-----------|
-| **Зерно факта (Fact Grain)** | Минимальная единица в факте. Здесь — один сегмент билета. |
-| **Суррогатный ключ (SK)** | Технический INT-ключ, генерируемый в DWH. |
-| **Бизнес-ключ (BK)** | Ключ из источника (`airport_code`, `passenger_id`). |
-| **Star Schema** | Факт в центре, измерения вокруг (без snowflake-подтаблиц). |
-| **SCD Type 1** | Перезапись атрибутов без истории. |
-| **SCD Type 2** | Версионирование: `valid_from`/`valid_to`, `hashdiff`. |
-| **AO Row/Column** | Append-Only хранение (Row или Column). Не поддерживает UPDATE. |
-| **Heap** | Стандартное хранение с поддержкой UPDATE/DELETE. |
-| **HWM (High Water Mark)** | Отсечка по `MAX(_load_ts)` для инкрементальной загрузки. |
-
-### На что обратить внимание
-
-**Обогащение измерений:**
-`seats` + `airplanes` → `dim_airplanes` — пример обогащения (total_seats).
-
-**«Майнинг» измерений из транзакций:**
-`tickets` → `dim_passengers` — в источнике нет таблицы «Пассажиры». Извлекаем
-уникальных пассажиров из билетов с дедупликацией.
-
-**Нормализация:**
-`segments.fare_conditions` → `dim_tariffs` — выносим строковый атрибут в отдельный
-справочник для компактного INT-ключа в факте.
-
-**Почему нет `dim_bookings`:**
-`bookings` — транзакция, не справочник. `book_ref` и `book_date` хранятся
-как degenerate keys в факте.
-
-**Point-in-time JOIN (SCD2):**
-Факт присоединяется к той версии маршрута, которая действовала на дату вылета:
-`scheduled_departure::DATE >= valid_from AND (valid_to IS NULL OR ... < valid_to)`.
-
----
-
-## Связанные документы
-
-- [`bookings_stg_design.md`](bookings_stg_design.md) — дизайн STG
-- [`bookings_ods_design.md`](bookings_ods_design.md) — дизайн ODS (SCD1, batch contract, DQ)
-- [`bookings_dds_design.md`](bookings_dds_design.md) — дизайн DDS (Star Schema, SCD2)
-- [`bookings_dm_design.md`](bookings_dm_design.md) — дизайн DM (5 витрин)
-- [`naming_conventions.md`](naming_conventions.md) — нейминг полей
-- [`reading_the_pipeline.md`](reading_the_pipeline.md) — как читать связи из скрипта загрузки
-- [`../reference/dq_taxonomy.md`](../reference/dq_taxonomy.md) — классы DQ-проверок и допустимые NULL
-- Часовые пояса и настройка PXF — в ветке `solution` (`docs/reference/`)
-- [`../assignment/analyst_spec.md`](../assignment/analyst_spec.md) — ТЗ от аналитика (курсовое задание)
-
----
-
-## Полная карта lineage (справочная)
-
-Здесь собран цельный взгляд «сверху». Эта карта нужна для точечного поиска:
-например, «что кормит X?» или «куда идут данные из X?». Читать весь пайплайн
-по ней подряд не нужно.
-
-```mermaid
-graph LR
- %% Стили
- classDef source fill:#e1f5fe,stroke:#01579b,stroke-width:2px;
- classDef stg fill:#fff9c4,stroke:#fbc02d,stroke-width:2px;
- classDef ods fill:#e0f2f1,stroke:#00695c,stroke-width:2px;
- classDef dim fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px;
- classDef fact fill:#ffccbc,stroke:#bf360c,stroke-width:4px;
- classDef dm fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
-
- %% 1. Source
- subgraph Source_Postgres [Source: Postgres Bookings]
- direction TB
- SRC_Airports[airports_data]:::source
- SRC_Airplanes[airplanes_data]:::source
- SRC_Routes[routes]:::source
- SRC_Seats[seats]:::source
- SRC_Bookings[bookings]:::source
- SRC_Tickets[tickets]:::source
- SRC_Flights[flights]:::source
- SRC_Segments[segments]:::source
- SRC_Boarding[boarding_passes]:::source
- end
-
- %% 2. STAGING (Load 1-to-1, AO-Row)
- subgraph STG_Layer [Layer: STG Staging]
- direction TB
- STG_Airports[stg.airports]:::stg
- STG_Airplanes[stg.airplanes]:::stg
- STG_Routes[stg.routes]:::stg
- STG_Seats[stg.seats]:::stg
- STG_Bookings[stg.bookings]:::stg
- STG_Tickets[stg.tickets]:::stg
- STG_Flights[stg.flights]:::stg
- STG_Segments[stg.segments]:::stg
- STG_Boarding[stg.boarding_passes]:::stg
- end
-
- %% Links Source to STG
- SRC_Airports --> STG_Airports
- SRC_Airplanes --> STG_Airplanes
- SRC_Routes --> STG_Routes
- SRC_Seats --> STG_Seats
- SRC_Bookings --> STG_Bookings
- SRC_Tickets --> STG_Tickets
- SRC_Flights --> STG_Flights
- SRC_Segments --> STG_Segments
- SRC_Boarding --> STG_Boarding
-
- %% 3. ODS (3NF, Clean, Type)
- subgraph ODS_Layer [Layer: ODS Operational Core]
- direction TB
- ODS_Airports[ods.airports]:::ods
- ODS_Airplanes[ods.airplanes]:::ods
- ODS_Routes[ods.routes]:::ods
- ODS_Seats[ods.seats]:::ods
- ODS_Bookings[ods.bookings]:::ods
- ODS_Tickets[ods.tickets]:::ods
- ODS_Flights[ods.flights]:::ods
- ODS_Segments[ods.segments]:::ods
- ODS_Boarding[ods.boarding_passes]:::ods
- end
-
- %% Links STG to ODS
- STG_Airports --> ODS_Airports
- STG_Airplanes --> ODS_Airplanes
- STG_Routes --> ODS_Routes
- STG_Seats --> ODS_Seats
- STG_Bookings --> ODS_Bookings
- STG_Tickets --> ODS_Tickets
- STG_Flights --> ODS_Flights
- STG_Segments --> ODS_Segments
- STG_Boarding --> ODS_Boarding
-
- %% 4. DDS (Star Schema)
- subgraph DDS_Layer [Layer: DDS Star Schema]
- direction TB
- DIM_Calendar[dds.dim_calendar]:::dim
- DIM_Airports[dds.dim_airports]:::dim
- DIM_Airplanes[dds.dim_airplanes]:::dim
- DIM_Tariffs[dds.dim_tariffs]:::dim
- DIM_Passengers[dds.dim_passengers]:::dim
- DIM_Routes[dds.dim_routes SCD2]:::dim
- FACT_Sales[dds.fact_flight_sales]:::fact
- end
-
- %% ODS to DDS Dimensions
- ODS_Airports --> DIM_Airports
- ODS_Airplanes --> DIM_Airplanes
- ODS_Seats -.->|total_seats| DIM_Airplanes
- ODS_Segments -.->|DISTINCT| DIM_Tariffs
- ODS_Tickets -->|Unique passengers| DIM_Passengers
- ODS_Routes -->|SCD2 hashdiff| DIM_Routes
- DIM_Airports -.->|cities| DIM_Routes
- DIM_Airplanes -.->|model, seats| DIM_Routes
-
- %% ODS to Fact
- ODS_Segments -->|Main stream| FACT_Sales
- ODS_Tickets -->|book_ref, passenger_id| FACT_Sales
- ODS_Bookings -->|book_date| FACT_Sales
- ODS_Flights -->|schedule, route_no| FACT_Sales
- ODS_Routes -->|dep/arr airports| FACT_Sales
- ODS_Boarding -->|LEFT JOIN seat_no| FACT_Sales
-
- %% Dimensions to Fact
- DIM_Calendar -->|calendar_sk| FACT_Sales
- DIM_Airports -->|dep/arr _sk| FACT_Sales
- DIM_Airplanes -->|airplane_sk| FACT_Sales
- DIM_Tariffs -->|tariff_sk| FACT_Sales
- DIM_Passengers -->|passenger_sk| FACT_Sales
- DIM_Routes -->|route_sk| FACT_Sales
-
- %% 5. DM (Vitrines)
- subgraph DM_Layer [Layer: DM Data Marts]
- direction TB
- DM_Sales[dm.sales_report]:::dm
- DM_Traffic[dm.airport_traffic]:::dm
- DM_Route[dm.route_performance]:::dm
- DM_Monthly[dm.monthly_overview]:::dm
- DM_Loyalty[dm.passenger_loyalty]:::dm
- end
-
- %% DDS to DM
- FACT_Sales --> DM_Sales
- FACT_Sales --> DM_Traffic
- FACT_Sales --> DM_Route
- FACT_Sales --> DM_Monthly
- FACT_Sales --> DM_Loyalty
- DIM_Airports -.-> DM_Sales
- DIM_Airports -.-> DM_Traffic
- DIM_Tariffs -.-> DM_Sales
- DIM_Tariffs -.-> DM_Loyalty
- DIM_Calendar -.-> DM_Sales
- DIM_Calendar -.-> DM_Traffic
- DIM_Calendar -.-> DM_Route
- DIM_Calendar -.-> DM_Monthly
- DIM_Calendar -.-> DM_Loyalty
- DIM_Routes -.-> DM_Route
- DIM_Routes -.-> DM_Monthly
- DIM_Routes -.-> DM_Loyalty
- DIM_Airplanes -.-> DM_Monthly
- DIM_Passengers -.-> DM_Loyalty
-```
+Для проверки результата пригодится [справочник 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,
+а требования к самостоятельной работе - по ТЗ.