Files
airflow-greenplum/docs/design/architecture-map.html
T

5260 lines
183 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- Собрано из architecture-map/map.json и template.html. Команда: uv run docs/design/architecture-map/render.py -->
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Карта DWH авиаперевозок</title>
<style>
:root {
color-scheme: light;
--paper: #f7f7f5;
--panel: #ffffff;
--ink: #272a2d;
--muted: #6b7177;
--line: #8c9298;
--soft: #e6e8e9;
--accent: #b54a2a;
--accent-soft: #f5ded7;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font: 15px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
header { padding: 22px 26px 12px; }
h1 { margin: 0 0 5px; font-size: 25px; }
.lead { margin: 0; color: var(--muted); }
.controls {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
padding: 8px 26px 16px;
}
.switcher { display: flex; flex-wrap: wrap; gap: 6px; }
button {
border: 1px solid #aeb3b7;
border-radius: 18px;
background: var(--panel);
color: var(--ink);
padding: 6px 13px;
font: inherit;
cursor: pointer;
}
button[aria-pressed="true"] {
border-color: var(--accent);
background: var(--accent-soft);
color: #71301e;
}
button:focus-visible, [tabindex="0"]:focus-visible {
outline: 3px solid #d79078;
outline-offset: 2px;
}
.legend { display: flex; flex-wrap: wrap; gap: 8px 20px; color: var(--muted); }
.legend-item { display: inline-flex; align-items: center; gap: 7px; }
.legend-icon { display: block; width: 50px; height: 20px; flex: none; }
main {
display: grid;
grid-template-columns: minmax(0, 1fr) 370px;
gap: 16px;
padding: 0 18px 24px;
align-items: start;
}
.map-scroll {
overflow-x: auto;
border: 1px solid #d7d9da;
border-radius: 10px;
background: var(--panel);
}
#map { display: block; width: 100%; min-width: 1040px; height: auto; }
.layer-title { font-size: 15px; font-weight: 700; text-anchor: middle; }
.layer-caption { font-size: 11px; fill: var(--muted); text-anchor: middle; }
.zone-caption { font-size: 12px; fill: var(--muted); }
.column-guide { stroke: #eef0f1; stroke-width: 1; }
.edge-line {
fill: none;
stroke: var(--line);
stroke-width: 1.8;
marker-end: url(#arrowhead);
transition: opacity .15s, stroke .15s, stroke-width .15s;
}
.edge-line.dashed { stroke-dasharray: 8 6; }
.edge-line.dotted { stroke-dasharray: 2 6; stroke-linecap: round; }
.edge-hit { fill: none; stroke: transparent; stroke-width: 14; cursor: pointer; }
.node { cursor: pointer; transition: opacity .15s; }
.node rect, .legend-node rect {
fill: #f1f2f2;
stroke: #858b90;
stroke-width: 1.2;
rx: 7;
transition: fill .15s, stroke .15s, stroke-width .15s;
}
.node text {
fill: var(--ink);
font: 14px ui-monospace, SFMono-Regular, Consolas, monospace;
text-anchor: middle;
pointer-events: none;
}
.node .stream-mark { fill: var(--muted); font: 11px system-ui, sans-serif; }
.is-filtered, .is-context-dim { opacity: .16; }
.is-path:not(.is-filtered), .is-hover:not(.is-filtered) { opacity: 1; }
.is-path .edge-line, .edge.is-path .edge-line {
stroke: var(--accent); stroke-width: 2.8; marker-end: url(#arrowhead-active);
}
.node.is-path rect { fill: var(--accent-soft); stroke: var(--accent); stroke-width: 2; }
.node.is-hover rect { fill: #faefeb; stroke: #d79078; }
.edge.is-hover .edge-line { stroke: #d79078; stroke-width: 2.2; }
.node.is-selected rect { stroke-width: 3; }
.edge.is-selected .edge-line { stroke-width: 4; }
aside {
min-height: 470px; max-height: calc(100vh - 32px); overflow-y: auto; position: sticky; top: 16px;
padding: 20px;
border: 1px solid #d7d9da;
border-radius: 10px;
background: var(--panel);
}
@media (max-width: 900px) { main { grid-template-columns: minmax(0, 1fr); } aside { min-height: 0; max-height: none; position: static; } .map-scroll { max-height: 50vh; overflow: auto; } }
aside h2 { margin: 0 0 3px; font-size: 20px; overflow-wrap: anywhere; }
.meta { margin: 0 0 18px; color: var(--muted); }
.card-section { margin-top: 17px; }
.card-section h3 { margin: 0 0 5px; font-size: 13px; }
.card-section p { margin: 0; }
.card-section ul { margin: 5px 0 0; padding-left: 20px; }
.card-section li + li { margin-top: 5px; }
code { font: .92em ui-monospace, SFMono-Regular, Consolas, monospace; }
a { color: #8a351e; text-underline-offset: 2px; }
.hint { color: var(--muted); font-size: 13px; }
.edge[hidden] { display: none; }
.node.student rect, .legend-node.student rect { stroke-dasharray: 5 3; }
.node.student .stream-mark { fill: #875a14; }
.edge.expected .edge-line, .legend-arrow.expected .edge-line { stroke-dasharray: 5 4; }
.node.is-upstream rect, .legend-node.is-upstream rect { fill: #e3eff5; stroke: #307c9a; }
.node.is-downstream rect, .legend-node.is-downstream rect { fill: #e7f1e5; stroke: #527a43; }
.node.is-selected rect, .legend-node.is-selected rect { fill: var(--accent-soft); stroke: var(--accent); }
.relation-button { border-radius: 5px; padding: 5px 7px; text-align: left; width: 100%; margin-top: 5px; font-size: 13px; }
.badge { font-size: 13px; padding: 3px 8px; border-radius: 4px; background: #eef1ed; display: inline-block; }
.badge.student { background: #fff1cf; color: #775018; }
.navigation { margin: 8px 0; }
code { overflow-wrap: anywhere; }
summary { cursor: pointer; font-weight: 600; }
#view-hint { margin: 0; max-width: 1050px; }
noscript { display: block; padding: 20px; }
</style>
</head>
<body>
<header>
<h1 id="page-title"></h1>
<p class="lead" id="lead"></p>
<p class="navigation"><a href="https://git.dementev.space/ddmitry/airflow-greenplum/src/docs/design/db_schema.md" target="_blank" rel="noopener noreferrer">Руководство: от бронирования до витрины (Gitea)</a> · Учебная ветка main</p>
</header>
<div class="controls">
<div class="switcher" id="switcher" aria-label="Вид связей"></div>
<p id="view-hint" class="hint"></p>
<div class="legend" aria-label="Обозначения стрелок">
<span class="legend-item">
<svg class="legend-icon legend-arrow" viewBox="0 0 50 20" aria-hidden="true"><path class="edge-line" d="M 2 10 H 43"/></svg>
<span id="ready-edge-legend">Готовая загрузка</span>
</span>
<span class="legend-item">
<svg class="legend-icon legend-arrow expected" viewBox="0 0 50 20" aria-hidden="true"><path class="edge-line" d="M 2 10 H 43"/></svg>
<span id="expected-edge-legend">Загрузка по ТЗ</span>
</span>
<span class="legend-item">
<svg class="legend-icon legend-arrow is-path" viewBox="0 0 50 20" aria-hidden="true"><path class="edge-line" d="M 2 10 H 43"/></svg>
<span>Выделенные связи</span>
</span>
</div>
<div class="legend" aria-label="Обозначения таблиц">
<span class="legend-item">
<svg class="legend-icon legend-node" viewBox="0 0 50 20" aria-hidden="true"><rect x="4" y="2" width="40" height="16"/></svg>
<span>Готовая таблица</span>
</span>
<span class="legend-item">
<svg class="legend-icon legend-node student" viewBox="0 0 50 20" aria-hidden="true"><rect x="4" y="2" width="40" height="16"/></svg>
<span>Учебная заглушка</span>
</span>
<span class="legend-item">
<svg class="legend-icon legend-node is-upstream" viewBox="0 0 50 20" aria-hidden="true"><rect x="4" y="2" width="40" height="16"/></svg>
<span id="upstream-legend">Источники</span>
</span>
<span class="legend-item">
<svg class="legend-icon legend-node is-downstream" viewBox="0 0 50 20" aria-hidden="true"><rect x="4" y="2" width="40" height="16"/></svg>
<span id="downstream-legend">Потребители</span>
</span>
<span class="legend-item">
<svg class="legend-icon legend-node is-selected" viewBox="0 0 50 20" aria-hidden="true"><rect x="4" y="2" width="40" height="16"/></svg>
<span>Выбранная таблица</span>
</span>
</div>
</div>
<main>
<div class="map-scroll">
<svg id="map" viewBox="0 0 1250 725" role="group" aria-label="Путь данных через слои хранилища"></svg>
</div>
<aside id="details" aria-live="polite" aria-label="Карточка выбранного объекта"></aside>
</main>
<noscript>Для интерактивной карты включите JavaScript. Объяснение модели доступно в <a href="https://git.dementev.space/ddmitry/airflow-greenplum/src/docs/design/db_schema.md">руководстве в Gitea</a>.</noscript>
<script>
const MAP = {
"title": "Хранилище авиаперевозок",
"lead": "От бронирования до витрины: выберите таблицу или связь.",
"repository_url": "https://git.dementev.space/ddmitry/airflow-greenplum/src/",
"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"
},
{
"label": "Разбор SQL загрузки",
"url": "reading_the_pipeline.md#ods-airports"
}
],
"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#пересчёт-факта-после-реализации-измерений"
},
{
"label": "Разбор SQL загрузки",
"url": "reading_the_pipeline.md#fact-flight-sales"
}
],
"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"
},
{
"label": "Разбор SQL загрузки",
"url": "reading_the_pipeline.md#ods-airports"
}
]
},
{
"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"
},
{
"label": "Разбор SQL загрузки",
"url": "reading_the_pipeline.md#fact-joins"
}
]
},
{
"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` или дата `\u003c 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"
},
{
"label": "Разбор SQL загрузки",
"url": "reading_the_pipeline.md#fact-joins"
}
]
},
{
"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"
},
{
"label": "Разбор SQL загрузки",
"url": "reading_the_pipeline.md#fact-joins"
}
]
},
{
"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"
},
{
"label": "Разбор SQL загрузки",
"url": "reading_the_pipeline.md#fact-joins"
}
]
},
{
"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"
}
]
}
]
};
const NS = "http://www.w3.org/2000/svg";
const state = { view: "flow", selected: null };
const nodeById = new Map(MAP.nodes.map(node => [node.id, node]));
const edgeById = new Map(MAP.edges.map(edge => [edge.id, edge]));
const positions = new Map();
const svg = document.getElementById("map");
const details = document.getElementById("details");
const statusLabel = {ready: "Готовая загрузка", student: "Задание · загрузка и DQ — заглушки", source: "Источник · демо-БД"};
function svgElement(name, attributes = {}) {
const element = document.createElementNS(NS, name);
Object.entries(attributes).forEach(([key, value]) => element.setAttribute(key, value));
return element;
}
function layerTitle(layerId) { return MAP.layers.find((layer) => layer.id === layerId)?.title ?? ""; }
function appendRichText(parent, value) {
value.split("`").forEach((part, index) => {
const element = index % 2 ? document.createElement("code") : document.createTextNode(part);
if (index % 2) element.textContent = part;
parent.append(element);
});
}
function addSection(title, value) {
if (!value || (Array.isArray(value) && !value.length)) return;
const section = document.createElement("section");
section.className = "card-section";
const heading = document.createElement("h3");
heading.textContent = title;
section.append(heading);
if (Array.isArray(value)) {
const list = document.createElement("ul");
value.forEach((item) => {
const row = document.createElement("li");
appendRichText(row, item);
list.append(row);
});
section.append(list);
} else {
const paragraph = document.createElement("p");
appendRichText(paragraph, value);
section.append(paragraph);
}
details.append(section);
}
function addLinks(links) {
if (!links?.length) return;
const section = document.createElement("section");
section.className = "card-section";
const heading = document.createElement("h3");
heading.textContent = "Ссылки";
const list = document.createElement("ul");
links.forEach((item) => {
const row = document.createElement("li");
const link = document.createElement("a");
// Выбор таблицы остается в локальной карте; файлы открываются в Gitea.
link.href = item.url.startsWith("#") ? item.url : new URL(item.url, new URL("docs/design/", MAP.repository_url)).href;
link.target = "_blank";
link.rel = "noopener noreferrer";
link.textContent = item.label;
row.append(link);
list.append(row);
});
section.append(heading, list);
details.append(section);
}
function renderIntro(message = "") {
details.replaceChildren();
const heading = document.createElement("h2");
heading.textContent = message || "Начните с билета";
details.append(heading);
addSection("Маршрут чтения", "Один билет может включать несколько перелетов. Выберите bookings.tickets, затем проследите сегменты до факта и витрины sales_report.");
addSection("Как исследовать", "Выбор таблицы подсвечивает ее окружение: источники и потребителей в потоке данных, родителей и потомков в связях по ключам. Их положение остается прежним. Соседние связи доступны кнопками в карточке. Повторный выбор, Esc или «Общий вид» снимает выделение.");
addSection("Что показывает карта", "Стрелки описывают данные. Порядок задач Airflow смотрите отдельно по ссылке на DAG в карточке. Пунктирная стрелка означает ожидаемую загрузку из задания.");
addSection("Вход через PXF", "Внешние stg.*_ext показаны внутри карточек STG: они читают Postgres, но сами не хранят копию данных.");
addSection("Ключи", "Все ключи и связи здесь логические. DDL таблиц DWH не объявляет PRIMARY KEY, UNIQUE и FOREIGN KEY. DISTRIBUTED BY задает размещение, а не уникальность.");
addLinks([{label: "Открыть руководство о модели", url: "db_schema.md"}]);
}
function relationButtons(node) {
const section = document.createElement("section");
section.className = "card-section";
const h = document.createElement("h3"); h.textContent = "Связи рядом с таблицей";
section.append(h);
const edges = MAP.edges.filter(e => e.mode === state.view && (e.from === node.id || e.to === node.id));
edges.forEach(edge => {
const b = document.createElement("button"); b.className = "relation-button";
const neighbor = edge.from === node.id ? edge.to : edge.from;
b.textContent = `${edge.from === node.id ? "→" : "←"} ${neighbor}: ${edge.label}`;
b.dataset.edge = edge.id;
b.addEventListener("click", () => selectItem("edge", edge.id));
section.append(b);
});
if (!edges.length) {
const p = document.createElement("p"); p.textContent = "В этом виде рядом нет связей. Переключите вид, чтобы увидеть путь загрузки."; section.append(p);
}
details.append(section);
}
function renderNodeCard(node) {
details.replaceChildren();
const heading = document.createElement("h2"); heading.textContent = node.name;
const meta = document.createElement("p"); meta.className = `badge ${node.status}`; meta.textContent = statusLabel[node.status];
details.append(heading, meta);
addSection("Назначение", node.what);
addSection("Одна строка", node.grain);
addSection("Логические ключи", node.keys);
addSection(node.status === "student" ? "Ожидаемый результат по ТЗ" : "Как наполняется", node.fill);
if (node.parts.length) addSection("Вход через PXF", node.parts.map(id => `\`${id}\` — внешняя таблица для чтения Postgres, без локального хранения. Ее определение находится в том же DDL.`));
const fields = document.createElement("details"); fields.className = "card-section";
const summary = document.createElement("summary"); summary.textContent = `Важные поля (${node.fields.length})`;
const list = document.createElement("ul");
node.fields.forEach(field => {
const li = document.createElement("li"); appendRichText(li, "`" + field.name + "` — " + field.meaning); list.append(li);
});
fields.append(summary,list); details.append(fields);
relationButtons(node);
const dag = {stg:"stage",ods:"ods",dds:"dds",dm:"dm"}[node.layer];
addLinks([...node.links, ...(dag ? [{label:"DAG · порядок задач",url:`../../airflow/dags/bookings_to_gp_${dag}.py`}] : []), {label:"Ссылка на эту таблицу",url:`#node=${node.id}&view=${state.view}`}]);
}
function renderEdgeCard(edge) {
details.replaceChildren();
const heading = document.createElement("h2"); heading.textContent = edge.label; details.append(heading);
addSection("Таблицы", "`" + edge.from + "` → `" + edge.to + "`");
addSection("Вид связи", edge.mode === "flow" ? (edge.status === "student" ? "Ожидаемый поток по ТЗ; загрузка пока не реализована." : "Чтение и преобразование данных в готовом SQL.") : "Логическое сопоставление строк по ключам. Это не зависимость задач Airflow и не объявленное ограничение БД.");
addSection("Что связывает", edge.join);
addSection("Участвующие поля", edge.refs.map(r => `\`${r.node}\`: ${r.columns.map(c => "`"+c+"`").join(", ")}`));
[edge.from, edge.to].forEach(id => {
const b = document.createElement("button"); b.className = "relation-button"; b.textContent = `Открыть ${id}`;
b.addEventListener("click", () => selectItem("node", id)); details.append(b);
});
addLinks(edge.links);
}
function layoutMap() {
MAP.layers.forEach((layer, index) => {
MAP.nodes.filter(node => node.layer === layer.id).forEach(node => {
positions.set(node.id, {x: 20 + index * 250, y: 72 + node.order * 70, width: 195, height: 48});
});
});
}
function edgePath(edge, offset) {
const a = positions.get(edge.from), b = positions.get(edge.to);
const y1 = a.y + a.height / 2 + offset, y2 = b.y + b.height / 2 + offset;
if (a.x === b.x) {
const x = a.x + a.width, bend = x + 20 + Math.abs(a.y - b.y) * .08 + offset;
return `M ${x} ${y1} C ${bend} ${y1}, ${bend} ${y2}, ${x} ${y2}`;
}
const x1 = a.x + a.width, x2 = b.x, bend = Math.max(18, (x2 - x1) / 2);
return `M ${x1} ${y1} C ${x1 + bend} ${y1}, ${x2 - bend} ${y2}, ${x2} ${y2}`;
}
function drawMap() {
const defs = svgElement("defs");
[["arrowhead", "#8c9298"], ["arrowhead-active", "#b54a2a"]].forEach(([id, fill]) => {
const marker = svgElement("marker", {id, viewBox:"0 0 8 8", refX:"7", refY:"4", markerWidth:"6", markerHeight:"6", orient:"auto"});
marker.append(svgElement("path", {d:"M 0 0 L 8 4 L 0 8 z", fill})); defs.append(marker);
});
svg.append(defs);
MAP.layers.forEach((layer,index) => {
const center = 117.5 + index * 250;
const title = svgElement("text", {x:center, y:27, class:"layer-title"}); title.textContent = layer.title;
const caption = svgElement("text", {x:center, y:47, class:"layer-caption"}); caption.textContent = layer.caption;
svg.append(title,caption);
});
MAP.edges.forEach(edge => {
const siblings = MAP.edges.filter(e => e.from === edge.from && e.to === edge.to && e.mode === edge.mode);
const offset = (siblings.indexOf(edge) - (siblings.length - 1) / 2) * 13;
const group = svgElement("g", {class:`edge ${edge.status === "student" ? "expected" : ""}`, "data-id":edge.id, tabindex:"0", role:"button", "aria-label":`${edge.label}: ${edge.from} → ${edge.to}`});
const d = edgePath(edge,offset);
group.append(svgElement("path", {d,class:"edge-line"}),svgElement("path", {d,class:"edge-hit"}));
bindSelection(group,"edge",edge.id); svg.append(group);
});
MAP.nodes.forEach(node => {
const p = positions.get(node.id);
const g = svgElement("g", {class:`node ${node.status}`, "data-id":node.id, tabindex:"0", role:"button", "aria-label":`${node.name}: ${statusLabel[node.status]}`});
g.append(svgElement("rect",p));
const label = svgElement("text", {x:p.x+p.width/2,y:p.y+21}); label.textContent = node.name;
const mark = svgElement("text", {x:p.x+p.width/2,y:p.y+38,class:"stream-mark"}); mark.textContent = {ready:"готово",student:"задание",source:"источник"}[node.status];
g.append(label,mark); bindSelection(g,"node",node.id); svg.append(g);
});
}
function bindSelection(element,type,id) {
element.addEventListener("click", () => selectItem(type,id));
element.addEventListener("keydown", event => {
if (event.key === "Enter" || event.key === " ") { event.preventDefault(); selectItem(type,id); }
});
}
function walk(nodeId,direction) {
const nodes = new Set([nodeId]), edges = new Set(), queue = [nodeId];
while (queue.length) {
const current = queue.shift();
MAP.edges.filter(e => e.mode === state.view).forEach(edge => {
if ((direction === "in" ? edge.to : edge.from) !== current) return;
edges.add(edge.id);
const next = direction === "in" ? edge.from : edge.to;
if (!nodes.has(next)) { nodes.add(next); queue.push(next); }
});
}
return {nodes,edges};
}
function updateHighlights() {
let upstream = new Set(), downstream = new Set(), activeEdges = new Set(), activeNodes = new Set();
if (state.selected?.type === "node") {
const a = walk(state.selected.id,"in"), b = walk(state.selected.id,"out");
upstream = a.nodes; downstream = b.nodes;
activeNodes = new Set([...upstream,...downstream]); activeEdges = new Set([...a.edges,...b.edges]);
} else if (state.selected?.type === "edge") {
const edge = edgeById.get(state.selected.id);
activeNodes = new Set([edge.from,edge.to]); activeEdges.add(edge.id);
}
svg.querySelectorAll(".node").forEach(el => {
const id = el.dataset.id, selected = state.selected?.type === "node" && state.selected.id === id;
el.classList.toggle("is-context-dim", Boolean(state.selected) && !activeNodes.has(id));
el.classList.toggle("is-upstream", upstream.has(id)); el.classList.toggle("is-downstream", downstream.has(id));
el.classList.toggle("is-selected", selected); el.setAttribute("aria-pressed",String(selected));
});
svg.querySelectorAll(".edge").forEach(el => {
const edge = edgeById.get(el.dataset.id), visible = edge.mode === state.view;
el.toggleAttribute("hidden", !visible); el.setAttribute("tabindex",visible ? "0" : "-1");
el.setAttribute("aria-hidden",String(!visible));
el.classList.toggle("is-context-dim",Boolean(state.selected) && !activeEdges.has(edge.id));
el.classList.toggle("is-path",activeEdges.has(edge.id));
const selected = state.selected?.type === "edge" && state.selected.id === edge.id;
el.classList.toggle("is-selected", selected); el.setAttribute("aria-pressed",String(selected));
});
}
function renderState(message = "") {
document.querySelectorAll("[data-view]").forEach(b => b.setAttribute("aria-pressed",String(b.dataset.view === state.view)));
document.getElementById("upstream-legend").textContent = state.view === "flow" ? "Источники" : "Связанные родители";
document.getElementById("downstream-legend").textContent = state.view === "flow" ? "Потребители" : "Связанные потомки";
document.getElementById("ready-edge-legend").textContent = state.view === "flow" ? "Готовая загрузка" : "Связь по ключам";
document.getElementById("expected-edge-legend").textContent = state.view === "flow" ? "Загрузка по ТЗ" : "Связь с учебной заглушкой";
document.getElementById("view-hint").textContent = state.view === "flow" ? "Стрелка: откуда загрузчик читает данные и куда записывает результат. Календарь генерируется отдельно." : "Стрелка: от родительской строки к связанным строкам. Например, от бронирования к билетам. Это не порядок загрузки и не физические FK.";
if (state.selected?.type === "node") renderNodeCard(nodeById.get(state.selected.id));
else if (state.selected?.type === "edge") renderEdgeCard(edgeById.get(state.selected.id));
else renderIntro(message);
details.scrollTop = 0; updateHighlights();
}
function saveAddress() {
const p = new URLSearchParams();
if (state.selected) p.set(state.selected.type,state.selected.id);
if (state.view !== "flow") p.set("view",state.view);
const hash = p.toString();
if (location.hash.slice(1) !== hash) location.hash = hash;
renderState();
}
function selectItem(type,id) {
const repeated = state.selected?.type === type && state.selected.id === id;
state.selected = repeated ? null : {type,id}; saveAddress();
}
function readAddress() {
const p = new URLSearchParams(location.hash.slice(1));
state.view = p.get("view") === "keys" ? "keys" : "flow";
state.selected = null;
if (nodeById.has(p.get("node"))) state.selected = {type:"node",id:p.get("node")};
else if (edgeById.has(p.get("edge"))) {
const edge = edgeById.get(p.get("edge")); state.view = edge.mode; state.selected = {type:"edge",id:edge.id};
}
renderState(!state.selected && (p.has("node") || p.has("edge")) ? "Объект по ссылке не найден. Выберите таблицу на карте." : "");
}
function renderControls() {
const switcher = document.getElementById("switcher");
[["flow","Поток данных"],["keys","Связи по ключам"]].forEach(([id,label]) => {
const b = document.createElement("button"); b.textContent = label; b.dataset.view = id;
b.addEventListener("click", () => {
state.view = id;
if (state.selected?.type === "edge") state.selected = {type:"node",id:edgeById.get(state.selected.id).to};
saveAddress();
}); switcher.append(b);
});
const clear = document.createElement("button"); clear.textContent = "Общий вид"; clear.id = "clear-selection";
clear.addEventListener("click", () => {state.selected = null; saveAddress();}); switcher.append(clear);
}
document.getElementById("page-title").textContent = MAP.title;
document.getElementById("lead").textContent = MAP.lead;
layoutMap(); drawMap(); renderControls(); readAddress();
window.addEventListener("hashchange",readAddress);
document.addEventListener("keydown",event => {
if (event.key === "Escape" && state.selected) {state.selected = null; saveAddress();}
});
</script>
</body>
</html>