From b7ecfb1bb766f18f04380a3b2c59857fb198fdd1 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sun, 20 Sep 2026 14:31:49 -0400 Subject: [PATCH 1/2] =?UTF-8?q?docs:=20=D1=81=D0=B2=D1=8F=D0=B7=D0=B0?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BA=D0=B0=D1=80=D1=82=D0=B0=20DWH,=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B7=D0=B1=D0=BE=D1=80=20SQL=20=D0=B8=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 +- docs/README.md | 33 +++-- docs/assignment/README.md | 129 +++++++++++--------- docs/assignment/analyst_spec.md | 92 +++++++++++--- docs/design/architecture-map.html | 28 +++++ docs/design/architecture-map/map.json | 28 +++++ docs/design/db_schema.md | 4 +- docs/design/reading_the_pipeline.md | 168 ++++++++++++++++++-------- 8 files changed, 342 insertions(+), 149 deletions(-) diff --git a/README.md b/README.md index 10fe0a7..bdfde28 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,9 @@ SELECT COUNT(*) FROM dm.sales_report; оно проведет от билетов и сегментов к факту и витрине. Рядом держите [интерактивную карту](docs/design/architecture-map.html) — локальный файл `docs/design/architecture-map.html` нужно открыть в браузере; в Gitea HTML -показывается как исходный текст. Затем переходите к -[учебным заданиям](docs/assignment/README.md). +показывается как исходный текст. Затем разберите +[готовую загрузку справочника](docs/design/reading_the_pipeline.md#ods-airports) +и переходите к [первому заданию ODS](docs/assignment/README.md#первая-загрузка-ods). ## DAG-и в стенде @@ -154,10 +155,10 @@ make clean # полный reset: удалить контейнер ## Документация -- [Учебные задания](docs/assignment/README.md) - [Структура DWH: от бронирования до витрины](docs/design/db_schema.md) +- [От карты к SQL загрузки](docs/design/reading_the_pipeline.md) +- [Учебные задания: порядок работы и проверка](docs/assignment/README.md) - [Интерактивная карта таблиц и связей](docs/design/architecture-map.html) -- [Как читать пайплайн по скрипту загрузки](docs/design/reading_the_pipeline.md) - [План тестирования/проверок и негативные кейсы](TESTING.md) - [Дополнительные заметки и технические детали](docs/README.md) - [Детали по STG DAG](docs/bookings_to_gp_stage.md) diff --git a/docs/README.md b/docs/README.md index 04ecee3..14b0c1a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,29 +1,36 @@ # Документация -Этот каталог содержит дополнительные материалы к учебному стенду. +## Знакомство и первое задание -## Быстрый путь (для менти) +После [запуска стенда](../README.md) пройдите три шага: -- [Быстрый старт и команды](../README.md) -- [Структура DWH: от бронирования до витрины](design/db_schema.md) -- [Интерактивная карта таблиц и связей](design/architecture-map.html) -- [Учебные задания](assignment/README.md) -- [Как читать пайплайн по скрипту загрузки](design/reading_the_pipeline.md) -- [План тестирования и проверки](../TESTING.md) -- [Главный учебный DAG: bookings → stg](bookings_to_gp_stage.md) -- [Учебный DAG: stg -> ods](bookings_to_gp_ods.md) -- [Учебный DAG: ods -> dds](bookings_to_gp_dds.md) -- [Учебный DAG: dds -> dm](bookings_to_gp_dm.md) +1. [Структура DWH](design/db_schema.md): смысл строк и связей на примере поездки. +2. [От карты к SQL загрузки](design/reading_the_pipeline.md#ods-airports): + разбор готового справочника ODS. Сборка факта понадобится позже, перед DDS. +3. [Учебные задания](assignment/README.md): файлы для работы, порядок запуска + и проверка результата. Поля и бизнес-правила находятся в [ТЗ](assignment/analyst_spec.md). + +## Найти деталь во время работы + +[Интерактивная карта](design/architecture-map.html) ведет от таблицы к ее +источникам, потребителям, SQL и пункту задания. Карту открывайте из локальной копии: `docs/design/architecture-map.html` в браузере. Сервер не нужен. Просмотр исходного HTML в Gitea не показывает карту. [Руководство](design/db_schema.md#как-открыть-карту) объясняет открытие и адресные ссылки. +Если вопрос о запуске или проверке, откройте нужный материал: + +- [Порядок запуска DAG](dag_execution_order.md) и [план проверки стенда](../TESTING.md). +- Руководства загрузок: [STG](bookings_to_gp_stage.md), [ODS](bookings_to_gp_ods.md), + [DDS](bookings_to_gp_dds.md), [DM](bookings_to_gp_dm.md). +- [Соглашения об именах](design/naming_conventions.md) и [классы DQ-проверок](reference/dq_taxonomy.md). + ## Дизайн (`design/`) - [Единые конвенции нейминга DWH (служебные поля и SCD)](design/naming_conventions.md) - [Структура DWH и маршрут по карте](design/db_schema.md) -- [Как читать пайплайн по скрипту загрузки](design/reading_the_pipeline.md) +- [От карты к SQL загрузки](design/reading_the_pipeline.md) - [Дизайн-документ STG](design/bookings_stg_design.md) - [Дизайн-документ ODS](design/bookings_ods_design.md) - [Дизайн-документ DDS](design/bookings_dds_design.md) diff --git a/docs/assignment/README.md b/docs/assignment/README.md index d779a8f..3880083 100644 --- a/docs/assignment/README.md +++ b/docs/assignment/README.md @@ -1,85 +1,94 @@ # Учебные задания -## О чём задание +В стенде уже работает цепочка от Bookings до `dm.sales_report`. +Вы дополните ее загрузками ODS, измерениями DDS и четырьмя витринами DM. +Поля, преобразования и бизнес-правила описаны в [ТЗ](analyst_spec.md). -В стенде уже работает **эталонный вертикальный срез** — полная цепочка -`STG → ODS → DDS → DM` для витрины `dm.sales_report`. Ваша задача — -реализовать недостающие загрузки по аналогии с эталоном. +## Подготовка -На месте ваших заданий сейчас стоят **заглушки** (`SELECT 1;`). Вы замените -их на реальную SQL-логику, и после этого весь DWH будет заполнен. +Пройдите шаги 1–6 [быстрого старта](../../README.md#быстрый-старт-основной-сценарий-bookings--stg--ods--dds--dm): +поднимите стенд, создайте таблицы, запустите эталонную цепочку и проверьте данные. +Затем прочитайте [руководство о структуре DWH](../design/db_schema.md). +Открывайте [карту](../design/architecture-map.html) рядом с заданием; +для Gitea есть [инструкция открытия локального HTML](../design/db_schema.md#как-открыть-карту). -## С чего начать чтение +Таблицы уже созданы, задачи Airflow подключены к SQL-файлам. +В учебных `*_load.sql` и `*_dq.sql` пока стоит `SELECT 1;`. +Вы заменяете эти заглушки; менять DAG и создавать таблицы заново не требуется. -Сначала посмотрите [руководство о структуре DWH и карту](../design/db_schema.md). -Пока пишете свой слой, держите рядом [метод чтения скрипта загрузки](../design/reading_the_pipeline.md). -Для `*_dq.sql` сверяйтесь с [классами DQ-проверок](../reference/dq_taxonomy.md). +## Первая загрузка: ODS -## Перед началом +Начните с `ods.airplanes`, затем добавьте `ods.seats`. +На карте откройте [модели самолетов](../design/architecture-map.html#node=ods.airplanes) +и [места](../design/architecture-map.html#node=ods.seats): источники STG уже заполнены. +Позже обе таблицы понадобятся `dds.dim_airplanes` для характеристик модели +и расчета ее вместимости. -Убедитесь, что стенд запущен и данные загружены — -пройдите шаги 1–6 из [README (Быстрый старт)](../../README.md#быстрый-старт-основной-сценарий-bookings--stg--ods--dds--dm). +### Готовый пример -После этого в Greenplum будут данные в эталонных таблицах (`stg.*`, `ods.bookings`, -`dds.dim_airports`, `dds.fact_flight_sales`, `dm.sales_report` и др.). +Пройдите [разбор stg.airports → ods.airports](../design/reading_the_pipeline.md#ods-airports). +Это близкий пример полного снимка с выбором батча, преобразованиями и дедупликацией. +Для первого задания этого разбора достаточно. -## Шаг 1. Изучите эталон +### Самостоятельная работа -Прежде чем писать код, разберите, как устроены эталонные загрузки. -По одному файлу на каждый ключевой паттерн: +Откройте [часть 1 ТЗ](analyst_spec.md#часть-1-ods-слой-operational-data-store). +Сверяйте с ней поля и правила каждой таблицы: -| Паттерн | Файл для изучения | Что посмотреть | -|---------|-------------------|----------------| -| ODS: TRUNCATE + INSERT (snapshot-справочник) | `sql/ods/airports_load.sql` | Фильтрация по `stg_batch_id`, дедупликация через `ROW_NUMBER()` | -| DDS: SCD1 UPSERT (измерение) | `sql/dds/dim_airports_load.sql` | TEMP TABLE → UPDATE (`IS DISTINCT FROM`) → INSERT | -| DM: витрина с UPSERT | `sql/dm/sales_report_load.sql` | HWM-инкремент, агрегация фактов по измерениям | +| Объект и требования | Файлы для реализации | DDL для сверки | +|---|---|---| +| [ods.airplanes](analyst_spec.md#11-odsairplanes) | [airplanes_load.sql](../../sql/ods/airplanes_load.sql), [airplanes_dq.sql](../../sql/ods/airplanes_dq.sql) | [airplanes_ddl.sql](../../sql/ods/airplanes_ddl.sql) | +| [ods.seats](analyst_spec.md#12-odsseats) | [seats_load.sql](../../sql/ods/seats_load.sql), [seats_dq.sql](../../sql/ods/seats_dq.sql) | [seats_ddl.sql](../../sql/ods/seats_ddl.sql) | -Описания каждого DAG — в `docs/`: -[STG](../bookings_to_gp_stage.md) · -[ODS](../bookings_to_gp_ods.md) · -[DDS](../bookings_to_gp_dds.md) · -[DM](../bookings_to_gp_dm.md) +Пример проверок: [airports_dq.sql](../../sql/ods/airports_dq.sql). +Учтите разницу ключей: у модели это `airplane_code`, у места - +`(airplane_code, seat_no)`. -## Шаг 2. Откройте ТЗ +### Запуск и проверка -Техническое задание от аналитика: **[analyst_spec.md](analyst_spec.md)**. +1. Дождитесь успешного `bookings_to_gp_stage` из подготовки. + Если STG уже заполнен, новый запуск для этой работы не нужен. +2. Запустите `bookings_to_gp_ods` целиком. Он выберет согласованный снимок + в `resolve_stg_batch_id`, затем выполнит + `load_ods_airplanes → dq_ods_airplanes → load_ods_seats → dq_ods_seats`. + SQL использует XCom этого запуска, поэтому отдельно от выбора батча + загрузку запускать не стоит. +3. В Greenplum посмотрите строки обеих таблиц: модели, типы числовых полей, + ключи мест и метки загрузки. Повторите ODS без нового запуска STG: + бизнес-строки и количество ключей должны сохраниться без дублей. +4. После реализации обеих таблиц запустите `bookings_validate` и откройте + группу `validate_ods`. Она проверит наличие данных, совпадение ключей + с выбранным STG-батчем, отсутствие дублей и NULL в ключах, + а также общий `_load_id` двух таблиц. -Там описаны все таблицы, которые нужно реализовать, с маппингами, бизнес-правилами -и подсказками. Рекомендуемый порядок (от простого к сложному): +В `bookings_validate` автоматически запускаются все три группы. +Пока готовы только задания ODS, оценивайте `validate_ods`: ошибки DDS и DM +на этом этапе ожидаемы. После выполнения всех заданий должны пройти все группы. +`*_dq.sql` тоже нужно реализовать: успешный `SELECT 1;` ничего не проверяет. -1. ODS: `airplanes`, `seats` -2. DDS: `dim_airplanes`, `dim_passengers` (SCD1) -3. DDS: `dim_routes` (SCD2 — ключевой вызов курсовой) -4. DM: `airport_traffic`, `route_performance`, `monthly_overview`, `passenger_loyalty` +## Следующие этапы -## Шаг 3. Реализуйте задания +| Этап | Требования | Готовый пример рядом | +|---|---|---| +| DDS: `dim_airplanes`, `dim_passengers` | [Модели](analyst_spec.md#21-ddsdim_airplanes-scd1), [пассажиры](analyst_spec.md#22-ddsdim_passengers-scd1) | [dim_airports_load.sql](../../sql/dds/dim_airports_load.sql) | +| DDS: `dim_routes` | [Версии маршрута SCD2](analyst_spec.md#23-ddsdim_routes-scd2) | [Разбор сборки факта](../design/reading_the_pipeline.md#fact-flight-sales) показывает, как используется версия | +| DM: `airport_traffic` → `route_performance` → `monthly_overview` → `passenger_loyalty` | [Часть 3 ТЗ](analyst_spec.md#часть-3-dm-слой-data-marts--витрины) | [sales_report_load.sql](../../sql/dm/sales_report_load.sql) | -- **Файлы-заглушки уже на месте** — например, `sql/ods/airplanes_load.sql` - содержит `SELECT 1;`. Ваша задача: заменить `SELECT 1;` на реальную SQL-логику. -- **DDL уже создан** — таблицы существуют, создавать их не нужно. -- **Таски в DAG-ах уже подключены** — `PostgresOperator` ссылается на ваши SQL-файлы. - Менять Python-код DAG-ов не нужно. -- **Нейминг полей:** сверяйтесь с `docs/design/naming_conventions.md`. - -## Шаг 4. Проверьте себя валидационным DAG-ом - -DAG **`bookings_validate`** — автоматическая проверка вашей реализации. - -- Три группы проверок: `validate_ods`, `validate_dds`, `validate_dm` — запускаются - параллельно. Можно проверять ODS, пока DDS ещё не готов. -- Включает активный SCD2-тест для `dim_routes`: загружает изменённые данные - и проверяет, что версионирование работает корректно. -- Сообщения об ошибках подскажут, что именно не так и что делать дальше. - -**Цель: все проверки зелёные.** +Перед DDS разберите [загрузку факта](../design/reading_the_pipeline.md#fact-flight-sales). +После реализации измерений выполните [пересчет факта и витрин](analyst_spec.md#пересчёт-факта-после-реализации-измерений). +Проверяйте готовый слой своей группой в `bookings_validate`. +В DDS есть активный SCD2-тест: он временно меняет данные маршрута, +запускает вашу загрузку и затем восстанавливает тестируемые данные. ## Если застряли -- Изучите эталонные SQL-скрипты — в них те же паттерны, что нужны для заданий. -- Загляните в `docs/design/naming_conventions.md` — единый источник нейминга полей. -- Сверьтесь с **веткой `solution`** — там полная рабочая реализация. +Вернитесь к карточке объекта на карте: там есть DDL, загрузка, DQ и пункт ТЗ. +Для конкретного вопроса используйте [соглашения об именах](../design/naming_conventions.md), +[справочник DQ](../reference/dq_taxonomy.md) и руководства DAG: +[ODS](../bookings_to_gp_ods.md), [DDS](../bookings_to_gp_dds.md), [DM](../bookings_to_gp_dm.md). +Полную реализацию можно посмотреть в ветке `solution`. ## Для менторов -Дизайн заданий и педагогическая логика — в ветке `solution` -(`docs/design/assignment_design.md`). +Дизайн заданий и педагогическая логика находятся в ветке `solution`, +в `docs/design/assignment_design.md`. diff --git a/docs/assignment/analyst_spec.md b/docs/assignment/analyst_spec.md index e887bbb..992747e 100644 --- a/docs/assignment/analyst_spec.md +++ b/docs/assignment/analyst_spec.md @@ -13,6 +13,11 @@ > > SQL-скрипты эталона лежат в `sql/stg/`, `sql/ods/`, `sql/dds/`, `sql/dm/`. +Порядок запуска и проверки первой загрузки находится во +[входе в задания](README.md#первая-загрузка-ods). В каждом разделе ниже есть +ссылка на объект карты и близкий готовый пример. Карту открывайте +[локально в браузере](../design/db_schema.md#как-открыть-карту). + --- ## Рекомендуемый порядок выполнения @@ -36,7 +41,7 @@ ## Общие правила -- **Нейминг полей:** см. `docs/design/naming_conventions.md` — единый источник +- **Нейминг полей:** см. [соглашения об именах](../design/naming_conventions.md) — единый источник истины для служебных полей (`_load_id`, `_load_ts`, `created_at`, `updated_at`, `valid_from`, `valid_to`, `hashdiff`, суффиксы `_bk` / `_sk`). - **SQL-файлы:** располагайте в `sql/{слой}/{объект}_{роль}.sql` @@ -46,7 +51,10 @@ Вам нужно только заменить содержимое SQL-файлов — DAG менять не нужно. - **Идемпотентность:** каждый скрипт загрузки должен быть безопасен при повторном запуске (не создавать дубликатов). -- **Шаблон `{{ run_id }}`:** используйте Jinja-шаблон Airflow для `_load_id`. +- **Метки загрузки:** в ODS-снимках `airplanes` и `seats` сохраняйте в `_load_id` + идентификатор исходного STG-батча из `resolve_stg_batch_id`, как в `ods.airports`. + Так проверка сможет сопоставить ODS с тем снимком, из которого он загружен. + В заданиях DDS и DM используйте `'{{ run_id }}'` текущего запуска слоя. --- @@ -67,11 +75,11 @@ STG-слой (все 9 таблиц) и `ods.routes` полностью реал `ods.routes` тоже эталонный: факт `fact_flight_sales` использует его для резолвинга аэропортов вылета/прилёта, независимо от студенческого `dds.dim_routes`. -**Что вам делать:** Изучите эталонные скрипты как образец — именно так написан -«боевой» код загрузки: -- `sql/stg/airports_load.sql` — инкрементальная загрузка (HWM) -- `sql/stg/airplanes_load.sql` — full snapshot с проверкой `_load_id` -- `sql/ods/airports_load.sql` — TRUNCATE+INSERT из STG +Для первого задания разберите [загрузку `ods.airports`](../design/reading_the_pipeline.md#ods-airports). +При необходимости проследить получение снимка откройте +[sql/stg/airports_load.sql](../../sql/stg/airports_load.sql) или +[sql/stg/airplanes_load.sql](../../sql/stg/airplanes_load.sql): оба читают полный +снимок источника. `NOT EXISTS` защищает от повторной вставки ключа в том же `_load_id`. --- @@ -81,10 +89,14 @@ STG-слой (все 9 таблиц) и `ods.routes` полностью реал > Справочники (`airplanes`, `seats`) загружаются стратегией TRUNCATE + INSERT > из последнего согласованного батча STG. (`ods.routes` реализован в эталоне.) > -> **Аналог для изучения:** `sql/ods/airports_ddl.sql`, `sql/ods/airports_load.sql` +> **Аналог для изучения:** [DDL airports](../../sql/ods/airports_ddl.sql), +> [загрузка airports](../../sql/ods/airports_load.sql) и [ее разбор](../design/reading_the_pipeline.md#ods-airports). ### 1.1. ods.airplanes +[На карте: модель самолета и ее источник](../design/architecture-map.html#node=ods.airplanes). +Готовый пример полного снимка: [ods.airports](../design/reading_the_pipeline.md#ods-airports). + **Описание:** Очищенный справочник моделей воздушных судов с правильными типами. **Источник:** `stg.airplanes` @@ -95,7 +107,7 @@ STG-слой (все 9 таблиц) и `ods.routes` полностью реал | `model` | TEXT NOT NULL | Наименование модели | `model` (парсинг JSON: `model::JSON->>'ru'` — если JSON, иначе `model` как есть) | | `range_km` | INTEGER | Дальность полёта, км | `range::INTEGER` | | `speed_kmh` | INTEGER | Крейсерская скорость, км/ч | `speed::INTEGER` | -| `_load_id` | TEXT NOT NULL | Идентификатор батча | `'{{ run_id }}'` | +| `_load_id` | TEXT NOT NULL | Идентификатор исходного STG-батча | `'{{ ti.xcom_pull(task_ids="resolve_stg_batch_id") }}'` | | `_load_ts` | TIMESTAMP NOT NULL | Момент загрузки | `now()` | **Тип историзации:** Нет (текущее состояние справочника, TRUNCATE + INSERT). @@ -115,6 +127,9 @@ STG-слой (все 9 таблиц) и `ods.routes` полностью реал ### 1.2. ods.seats +[На карте: места и связь с моделью](../design/architecture-map.html#node=ods.seats). +Пример выбора снимка и дедупликации: [ods.airports](../design/reading_the_pipeline.md#ods-airports). + **Описание:** Карта посадочных мест с корректными типами. **Источник:** `stg.seats` @@ -124,7 +139,7 @@ STG-слой (все 9 таблиц) и `ods.routes` полностью реал | `airplane_code` | TEXT NOT NULL | Код модели (PK, часть 1) | `airplane_code` | | `seat_no` | TEXT NOT NULL | Номер места (PK, часть 2) | `seat_no` | | `fare_conditions` | TEXT NOT NULL | Класс обслуживания | `fare_conditions` | -| `_load_id` | TEXT NOT NULL | Идентификатор батча | `'{{ run_id }}'` | +| `_load_id` | TEXT NOT NULL | Идентификатор исходного STG-батча | `'{{ ti.xcom_pull(task_ids="resolve_stg_batch_id") }}'` | | `_load_ts` | TIMESTAMP NOT NULL | Момент загрузки | `now()` | **Тип историзации:** Нет (текущее состояние справочника, TRUNCATE + INSERT). @@ -149,10 +164,14 @@ STG-слой (все 9 таблиц) и `ods.routes` полностью реал > ключами. SCD1-измерения обновляют атрибуты «на месте». SCD2-измерение > хранит историю изменений через версионирование. > -> **Аналог для SCD1:** `sql/dds/dim_airports_ddl.sql`, `sql/dds/dim_airports_load.sql` +> **Аналог для SCD1:** [DDL dim_airports](../../sql/dds/dim_airports_ddl.sql), +> [загрузка dim_airports](../../sql/dds/dim_airports_load.sql). ### 2.1. dds.dim_airplanes (SCD1) +[На карте: характеристики и вместимость модели](../design/architecture-map.html#node=dds.dim_airplanes). +Пример SCD1: [dim_airports_load.sql](../../sql/dds/dim_airports_load.sql). + **Описание:** Измерение моделей самолётов. Содержит технические характеристики и рассчитанное общее количество мест (обогащение из `ods.seats`). @@ -184,7 +203,8 @@ STG-слой (все 9 таблиц) и `ods.routes` полностью реал в `ods.seats` для данного `airplane_code` (LEFT JOIN). **Генерация суррогатного ключа:** `MAX(airplane_sk) + ROW_NUMBER()`. -Безопасно при `concurrency=1` в DAG. +Загрузки одной таблицы должны выполняться последовательно. В DDS DAG +параллельные запуски ограничены параметром `max_active_runs=1`. **Distribution Key:** `airplane_sk` @@ -192,6 +212,9 @@ STG-слой (все 9 таблиц) и `ods.routes` полностью реал ### 2.2. dds.dim_passengers (SCD1) +[На карте: пассажир из билетов](../design/architecture-map.html#node=dds.dim_passengers). +Пример обновления атрибутов и вставки новых ключей: [dim_airports_load.sql](../../sql/dds/dim_airports_load.sql). + **Описание:** Измерение пассажиров. Извлекается из таблицы билетов — каждый уникальный `passenger_id` становится строкой измерения. @@ -230,6 +253,10 @@ STG-слой (все 9 таблиц) и `ods.routes` полностью реал ### 2.3. dds.dim_routes (SCD2) +[На карте: версии маршрута](../design/architecture-map.html#node=dds.dim_routes). +В [разборе готового факта](../design/reading_the_pipeline.md#fact-joins) видно, +как загрузчик выбирает версию на дату рейса. Алгоритм самой SCD2-загрузки дан ниже. + > **Это ключевой вызов курсовой.** Реализация SCD Type 2 — обязательный навык > для Data Engineer. Ниже — алгоритм текстом; SQL вы пишете самостоятельно. > Если застряли — сверьтесь с веткой `solution`. @@ -317,10 +344,15 @@ md5(concat_ws('|', > **Цель:** Построить аналитические витрины поверх DDS. > Каждая витрина отвечает на конкретный бизнес-вопрос. > -> **Аналог для изучения:** `sql/dm/sales_report_ddl.sql`, `sql/dm/sales_report_load.sql` +> **Аналог для изучения:** [DDL sales_report](../../sql/dm/sales_report_ddl.sql), +> [загрузка sales_report](../../sql/dm/sales_report_load.sql). ### 3.1. dm.airport_traffic +[На карте: аэропорт за день](../design/architecture-map.html#node=dm.airport_traffic). +Пример пересчета затронутых дат: [sales_report_load.sql](../../sql/dm/sales_report_load.sql). +Две роли аэропорта проследите в [готовом факте](../design/reading_the_pipeline.md#fact-joins). + **Бизнес-вопрос:** «Какой пассажиропоток и выручка у каждого аэропорта по дням?» **Описание:** Ежедневная статистика по каждому аэропорту: сколько рейсов @@ -359,6 +391,11 @@ md5(concat_ws('|', и `arrival_airport_sk`. Затем сгруппируйте по `(traffic_date, airport_sk)`. **Метрики:** + +Здесь `SUM(is_boarded)` означает число строк с признаком посадки. +Поле булево; в SQL используйте `SUM(CASE WHEN is_boarded THEN 1 ELSE 0 END)`, +как в `sales_report_load.sql`. + - `departures_flights` — `COUNT(DISTINCT flight_id)` для роли «вылет» - `departures_passengers` — `SUM(is_boarded)` для роли «вылет» - `departures_revenue` — `SUM(price)` для роли «вылет» @@ -377,6 +414,10 @@ md5(concat_ws('|', ### 3.2. dm.route_performance +[На карте: статистика маршрута](../design/architecture-map.html#node=dm.route_performance). +Соединение факта с измерениями и агрегация показаны в +[sales_report_load.sql](../../sql/dm/sales_report_load.sql); здесь по ТЗ нужен полный пересчет. + **Бизнес-вопрос:** «Какие маршруты самые эффективные? Где высокий load factor, а где теряем пассажиров?» @@ -427,9 +468,8 @@ md5(concat_ws('|', - `avg_boarding_rate` — `AVG(CASE WHEN is_boarded THEN 1 ELSE 0 END)` - `avg_load_factor` — `total_boarded / (total_flights * total_seats)` -**Тип хранения Greenplum:** AO Column Store (zstd). Идеален для аналитики: -отличное сжатие, чтение только нужных колонок. AO не поддерживает UPDATE — -поэтому используем Full Rebuild. +**Тип хранения Greenplum:** AO Column Store (zstd), сжатое колоночное хранение. +В этом задании небольшой результат полностью пересоздается при каждом запуске. **Distribution Key:** `route_bk` @@ -440,6 +480,10 @@ md5(concat_ws('|', ### 3.3. dm.monthly_overview +[На карте: месяц и модель самолета](../design/architecture-map.html#node=dm.monthly_overview). +Пример отбора затронутых периодов: [sales_report_load.sql](../../sql/dm/sales_report_load.sql). +В этом задании период и уровни агрегации определены ниже. + **Бизнес-вопрос:** «Какова помесячная динамика: рейсы, выручка, load factor в разрезе типов самолётов?» @@ -479,9 +523,11 @@ md5(concat_ws('|', **Ключевой приём — Двухуровневая агрегация:** -Чтобы честно посчитать среднюю заполняемость (`avg_load_factor`), нельзя просто -поделить `SUM(boarded)` на `SUM(seats)` — это даёт ошибку (парадокс Симпсона). -Правильный путь: +В этом задании `avg_load_factor` - среднее долей занятых мест по рейсам: +каждый рейс имеет одинаковый вес. Отношение `SUM(boarded) / SUM(seats)` +дает среднее с весом по вместимости и при разной вместимости может отличаться. +Нужный расчет: + 1. **Уровень 1 (рейс):** Для каждого `flight_id` посчитайте `load_factor = boarded / total_seats`. 2. **Уровень 2 (месяц):** Возьмите `AVG(load_factor)` по всем рейсам месяца. @@ -502,6 +548,10 @@ md5(concat_ws('|', ### 3.4. dm.passenger_loyalty +[На карте: история пассажира](../design/architecture-map.html#node=dm.passenger_loyalty). +В [sales_report_load.sql](../../sql/dm/sales_report_load.sql) посмотрите отделение +затронутых дат от полной истории за эти даты. Здесь единица пересчета - пассажир. + **Бизнес-вопрос:** «Кто наши самые лояльные пассажиры? Сколько они летают, тратят, какой класс предпочитают?» @@ -568,6 +618,10 @@ md5(concat_ws('|', ## Пересчёт факта после реализации измерений +[Факт на карте](../design/architecture-map.html#node=dds.fact_flight_sales) и +[разбор его INSERT и UPDATE](../design/reading_the_pipeline.md#найдите-запись-результата) +помогут проследить, когда заполняются ключи измерений. + После того, как вы реализуете все DDS-измерения (`dim_airplanes`, `dim_passengers`, `dim_routes`), нужно пересчитать факт — он загружался ещё без ваших SK: diff --git a/docs/design/architecture-map.html b/docs/design/architecture-map.html index 7072ae1..0235035 100644 --- a/docs/design/architecture-map.html +++ b/docs/design/architecture-map.html @@ -1268,6 +1268,10 @@ { "label": "DQ", "url": "../../sql/ods/airports_dq.sql" + }, + { + "label": "Разбор SQL загрузки", + "url": "reading_the_pipeline.md#ods-airports" } ], "parts": [] @@ -1836,6 +1840,10 @@ { "label": "Пересчет после реализации измерений", "url": "../assignment/analyst_spec.md#пересчёт-факта-после-реализации-измерений" + }, + { + "label": "Разбор SQL загрузки", + "url": "reading_the_pipeline.md#fact-flight-sales" } ], "parts": [] @@ -2633,6 +2641,10 @@ { "label": "SQL", "url": "../../sql/ods/airports_load.sql" + }, + { + "label": "Разбор SQL загрузки", + "url": "reading_the_pipeline.md#ods-airports" } ] }, @@ -3356,6 +3368,10 @@ { "label": "SQL", "url": "../../sql/dds/fact_flight_sales_load.sql" + }, + { + "label": "Разбор SQL загрузки", + "url": "reading_the_pipeline.md#fact-joins" } ] }, @@ -3397,6 +3413,10 @@ { "label": "SQL", "url": "../../sql/dds/fact_flight_sales_load.sql" + }, + { + "label": "Разбор SQL загрузки", + "url": "reading_the_pipeline.md#fact-joins" } ] }, @@ -3469,6 +3489,10 @@ { "label": "SQL", "url": "../../sql/dds/fact_flight_sales_load.sql" + }, + { + "label": "Разбор SQL загрузки", + "url": "reading_the_pipeline.md#fact-joins" } ] }, @@ -3505,6 +3529,10 @@ { "label": "SQL", "url": "../../sql/dds/fact_flight_sales_load.sql" + }, + { + "label": "Разбор SQL загрузки", + "url": "reading_the_pipeline.md#fact-joins" } ] }, diff --git a/docs/design/architecture-map/map.json b/docs/design/architecture-map/map.json index 0384c8e..ca0c341 100644 --- a/docs/design/architecture-map/map.json +++ b/docs/design/architecture-map/map.json @@ -1068,6 +1068,10 @@ { "label": "DQ", "url": "../../sql/ods/airports_dq.sql" + }, + { + "label": "Разбор SQL загрузки", + "url": "reading_the_pipeline.md#ods-airports" } ], "parts": [] @@ -1636,6 +1640,10 @@ { "label": "Пересчет после реализации измерений", "url": "../assignment/analyst_spec.md#пересчёт-факта-после-реализации-измерений" + }, + { + "label": "Разбор SQL загрузки", + "url": "reading_the_pipeline.md#fact-flight-sales" } ], "parts": [] @@ -2433,6 +2441,10 @@ { "label": "SQL", "url": "../../sql/ods/airports_load.sql" + }, + { + "label": "Разбор SQL загрузки", + "url": "reading_the_pipeline.md#ods-airports" } ] }, @@ -3156,6 +3168,10 @@ { "label": "SQL", "url": "../../sql/dds/fact_flight_sales_load.sql" + }, + { + "label": "Разбор SQL загрузки", + "url": "reading_the_pipeline.md#fact-joins" } ] }, @@ -3197,6 +3213,10 @@ { "label": "SQL", "url": "../../sql/dds/fact_flight_sales_load.sql" + }, + { + "label": "Разбор SQL загрузки", + "url": "reading_the_pipeline.md#fact-joins" } ] }, @@ -3269,6 +3289,10 @@ { "label": "SQL", "url": "../../sql/dds/fact_flight_sales_load.sql" + }, + { + "label": "Разбор SQL загрузки", + "url": "reading_the_pipeline.md#fact-joins" } ] }, @@ -3305,6 +3329,10 @@ { "label": "SQL", "url": "../../sql/dds/fact_flight_sales_load.sql" + }, + { + "label": "Разбор SQL загрузки", + "url": "reading_the_pipeline.md#fact-joins" } ] }, diff --git a/docs/design/db_schema.md b/docs/design/db_schema.md index d6ecbbf..737a716 100644 --- a/docs/design/db_schema.md +++ b/docs/design/db_schema.md @@ -204,8 +204,8 @@ SQL использует `LEFT JOIN`, поэтому строка с таким ## Куда идти дальше Для первого задания разберите готовую загрузку справочника: -[как читать SQL загрузки](reading_the_pipeline.md), затем откройте -[учебные задания](../assignment/README.md). Для точечного вопроса +[от карты к SQL загрузки](reading_the_pipeline.md#ods-airports), затем откройте +[первое задание ODS](../assignment/README.md#первая-загрузка-ods). Для точечного вопроса о таблице возвращайтесь на карту: там находится каталог объектов, их источников, потребителей, SQL и DQ. diff --git a/docs/design/reading_the_pipeline.md b/docs/design/reading_the_pipeline.md index 01db211..d5d7ea2 100644 --- a/docs/design/reading_the_pipeline.md +++ b/docs/design/reading_the_pipeline.md @@ -1,66 +1,132 @@ -# Как читать пайплайн по скрипту загрузки +# От карты к SQL загрузки -Большую схему не нужно запоминать. Связи любой таблицы уже записаны в блоке -`FROM ... JOIN ...` её файла `*_load.sql`. Обычно каждая прочитанная таблица -даёт одно входящее ребро. Если одна таблица читается под разными псевдонимами, -как `dim_airports`, её можно показать одним узлом. +На [карте](architecture-map.html) найдите таблицу, которую хотите заполнить, +и откройте ее SQL загрузки. Проследите, какие строки скрипт читает, +что меняет и по какому ключу записывает результат. +Смысл строк и связей разобран в [руководстве о структуре DWH](db_schema.md). -## Метод +Для первого задания достаточно примера `ods.airports` ниже. +К сборке факта вернитесь перед заданиями DDS. +Адресные ссылки на карту работают в локальном просмотре; +если читаете в Gitea, используйте [инструкцию открытия карты](db_schema.md#как-открыть-карту). -`INNER JOIN` показывает обязательную часть основного потока. Если входной строки -нет, целевая строка не появится. `LEFT JOIN` к измерениям решает две задачи. -Факт может прийти раньше измерения: это опаздывающее измерение -(late-arriving dimension). Эталонный факт также не должен зависеть от того, -успел ли студент реализовать свои измерения. Поэтому строка факта сохраняется, -а её суррогатный ключ пока может быть `NULL`. + -В этом CTE есть и другие `LEFT JOIN`. `ods.routes` даёт эталонный путь к -аэропортам, а `ods.boarding_passes` содержит необязательные `seat_no` и -признак посадки. +## Справочник: stg.airports → ods.airports -Если в загрузчике несколько SQL-операторов, смотрите на источник `INSERT`. -Именно он задаёт полный список входящих связей для новых строк. Например, в -`fact_flight_sales_load.sql` блок `FROM` у `UPDATE` читает только изменяемые -поля, а полный список находится в CTE `fact_src` перед `INSERT`. +Откройте [ods.airports на карте](architecture-map.html#node=ods.airports) +и связь [Типизация и загрузка ODS](architecture-map.html#edge=flow-stg.airports-ods.airports-13). +Здесь один источник: `stg.airports`. В STG накапливаются снимки разных запусков, +а в ODS нужна одна строка на аэропорт из выбранного снимка. -## Пример: `fact_flight_sales` +Рядом откройте [airports_load.sql](../../sql/ods/airports_load.sql). +Пройдите скрипт по порядку: -Откройте [`sql/dds/fact_flight_sales_load.sql`](../../sql/dds/fact_flight_sales_load.sql) -и пройдите `fact_src` сверху вниз: +1. `TRUNCATE TABLE ods.airports` очищает предыдущий снимок. + Следующий `INSERT` заменяет его целиком, поэтому исчезнувший из нового + снимка аэропорт исчезнет и из ODS. +2. В CTE `src` найдите `FROM stg.airports` и фильтр `_load_id`: -1. `FROM ods.segments` задаёт зерно `(ticket_no, flight_id)` и поле `price`. -2. `JOIN ods.tickets` обязателен для `book_ref` и `passenger_id`. -3. `JOIN ods.bookings` обязателен для `book_date`. -4. `JOIN ods.flights` обязателен для расписания и `route_no`. -5. `LEFT JOIN ods.routes` находит аэропорты по `route_no`. Этот эталонный путь - не зависит от студенческого `dim_routes`. -6. `LEFT JOIN dds.dim_routes` ищет point-in-time версию маршрута и `route_sk`. -7. `LEFT JOIN dds.dim_calendar` даёт `calendar_sk` по дате вылета. -8. Два `LEFT JOIN dds.dim_airports` дают ключи аэропортов вылета и прилёта. - На схеме их можно показать одним узлом `dim_airports`. -9. `LEFT JOIN dds.dim_airplanes` даёт `airplane_sk` через найденную версию маршрута. -10. `LEFT JOIN dds.dim_tariffs` даёт `tariff_sk`. -11. `LEFT JOIN dds.dim_passengers` даёт `passenger_sk`. -12. `LEFT JOIN ods.boarding_passes` даёт `seat_no` и признак `is_boarded`. + ```sql + WHERE s._load_id = '{{ ti.xcom_pull(task_ids="resolve_stg_batch_id") }}'::text + ``` -Этот путь разобран в разделе [«Сборка факта продаж»](db_schema.md#сборка-факта-продаж). -На карте сплошные стрелки показывают готовые загрузки, пунктирные — ожидаемые -по ТЗ. Тип соединения и поведение при отсутствии строки описаны в карточке связи. + Задача `resolve_stg_batch_id` в [ODS DAG](../../airflow/dags/bookings_to_gp_ods.py) + выбирает общий батч четырех справочников STG или берет `stg_batch_id` + из параметров запуска. Этот фильтр оставляет один снимок из истории STG. +3. Выражения `airport_name::json->>'ru'`, `city::json->>'ru'` + и `country::json->>'ru'` извлекают русские названия. Остальные бизнес-поля + переносятся без преобразования. +4. `ROW_NUMBER()` нумерует строки внутри `airport_code` по `_load_ts DESC`, + затем `event_ts DESC NULLS LAST`. Финальный `WHERE s.rn = 1` + оставляет одну строку на код. +5. Сопоставьте колонки `INSERT INTO ods.airports (...)` с финальным `SELECT`. + В `_load_id` сохраняется идентификатор выбранного STG-батча, + в `_load_ts` записывается время загрузки ODS. Поля и типы результата + заданы в [airports_ddl.sql](../../sql/ods/airports_ddl.sql). +6. `ANALYZE` обновляет статистику таблицы для планировщика запросов. + Проверки находятся отдельно: [airports_dq.sql](../../sql/ods/airports_dq.sql) + сравнивает множества ключей STG-батча и ODS, ищет дубли и пустые обязательные поля. + +Повторная загрузка того же снимка должна сохранить бизнес-строки без дублей; +время `_load_ts` при этом обновится. +Теперь переходите к [первой самостоятельной загрузке](../assignment/README.md#первая-загрузка-ods). + + + + +## Факт: сегмент билета и его измерения + +Откройте [dds.fact_flight_sales](architecture-map.html#node=dds.fact_flight_sales) +и [fact_flight_sales_load.sql](../../sql/dds/fact_flight_sales_load.sql). +Одна строка факта соответствует паре `(ticket_no, flight_id)`. +В [примере поездки](db_schema.md#сборка-факта-продаж) два билета с тремя +сегментами дают три строки факта. + + + + +### Найдите источник новых строк + +В файле сначала идет `UPDATE`, а затем CTE `fact_src` и `INSERT`. +Начните с `fact_src`: здесь виден полный набор источников новых строк. +`FROM ods.segments AS seg` задает зерно, а `seg.amount AS price` +переносит цену сегмента в факт. + +| Участок `fact_src` | Что добавляется к сегменту | Связь на карте | +|---|---|---| +| `JOIN ods.tickets`, затем `JOIN ods.bookings` | Бронирование, пассажир и дата покупки | [Билет и пассажир](architecture-map.html#edge=fact-1), [дата бронирования](architecture-map.html#edge=fact-2) | +| `JOIN ods.flights` | Дата вылета и номер маршрута | [Рейс, дата и маршрут](architecture-map.html#edge=fact-3) | +| `LEFT JOIN` подзапроса из `ods.routes` | Коды аэропортов из строки с наибольшим `validity` на `route_no` | [Коды двух аэропортов](architecture-map.html#edge=fact-4) | +| Два `LEFT JOIN dds.dim_airports` как `dep` и `arr` | `departure_airport_sk` и `arrival_airport_sk` по двум кодам | [Вылет](architecture-map.html#edge=fact-7), [прилет](architecture-map.html#edge=fact-8) | +| `LEFT JOIN dds.dim_routes`, затем `dds.dim_airplanes` | Версия маршрута на дату рейса и SK ее модели самолета | [Версия маршрута](architecture-map.html#edge=fact-5), [модель](architecture-map.html#edge=fact-9) | +| `LEFT JOIN dds.dim_calendar`, `dds.dim_tariffs`, `dds.dim_passengers` | SK по дате вылета, классу обслуживания и `passenger_id` | [Дата](architecture-map.html#edge=fact-6), [тариф](architecture-map.html#edge=fact-10), [пассажир](architecture-map.html#edge=fact-11) | +| `LEFT JOIN ods.boarding_passes` по обоим полям ключа | `seat_no`; наличие талона задает `is_boarded` | [Посадка и место](architecture-map.html#edge=fact-12) | + +Если билет, бронирование или рейс не найдены, `INNER JOIN` исключит сегмент +из вставки. `LEFT JOIN` сохранит его при отсутствии совпадения: например, +без талона получатся `seat_no = NULL` и `is_boarded = false`. + +Обратите внимание на два разных пути от маршрута: + +- Аэропорты берутся через готовый `ods.routes`. Отбор `ORDER BY validity DESC` + работает по полю TEXT и не использует дату рейса. +- `route_sk` ищется в студенческом `dim_routes` по дате вылета + в интервале `[valid_from, valid_to)`. Из этой версии берется `airplane_code` + для поиска `airplane_sk`. + +До выполнения заданий `route_sk`, `airplane_sk` и `passenger_sk` могут быть +`NULL`. Путь аэропортов от студенческих измерений не зависит. +Объяснение этой части модели: [аэропорты и версия маршрута](db_schema.md#аэропорты-вылета-и-прилета). + +### Найдите запись результата + +Финальный `INSERT` берет поля из `fact_src`. Условие `NOT EXISTS` +проверяет пару `(ticket_no, flight_id)` и пропускает уже записанные строки. +Вернитесь к `UPDATE` в начале файла: он меняет только цену, место, +признак посадки и метки загрузки у существующих строк. +Ключи измерений он не пересчитывает. + +Поэтому после реализации измерений нужен +[пересчет факта по ТЗ](../assignment/analyst_spec.md#пересчёт-факта-после-реализации-измерений). +Проверки результата находятся в [fact_flight_sales_dq.sql](../../sql/dds/fact_flight_sales_dq.sql). ## Как проверить свой слой -- Выпишите все таблицы из `FROM` и `JOIN` своего `*_load.sql`. -- Сравните список с эталонной схемой или дизайн-документом этого слоя. -- Проверьте служебные и SCD-поля по [`naming_conventions.md`](naming_conventions.md). -- Убедитесь, что рядом есть `*_dq.sql` и он покрывает нужные - [классы DQ-проверок](../reference/dq_taxonomy.md). +Перед записью SQL найдите свой объект на карте и откройте его пункт ТЗ. +Сформулируйте, что означает одна строка результата, по каким полям соединяются +источники и что произойдет при отсутствии совпадения. Затем сверяйте +`INSERT` с DDL, а преобразования и `*_dq.sql` с требованиями задания. -## Шаблон схемы одной таблицы +Близкие готовые примеры для следующих этапов: -Скопируйте шаблон и замените имена таблиц и полей: +- SCD1: [dim_airports_load.sql](../../sql/dds/dim_airports_load.sql) обновляет + изменившиеся атрибуты через `IS DISTINCT FROM` и вставляет новые бизнес-ключи. +- DM: [sales_report_load.sql](../../sql/dm/sales_report_load.sql) находит + затронутые даты по `_load_ts` факта, пересчитывает за них полные агрегаты + во временную таблицу и выполняет `UPDATE` и `INSERT`. -```mermaid -graph LR - MAIN["schema.main_input"] -->|"grain, metric"| TARGET["schema.target"] - LOOKUP["schema.lookup"] -.->|"lookup_key"| TARGET -``` +[Порядок заданий](../assignment/README.md#следующие-этапы) ведет к требованиям +каждого этапа. Имена служебных полей проверяйте по +[соглашениям](naming_conventions.md), состав DQ - по +[справочнику проверок](../reference/dq_taxonomy.md). -- 2.54.0 From 20eb7f7b841bba2443bb7eae16dc64783f7ee4b6 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sun, 20 Sep 2026 15:13:20 -0400 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20=D1=83=D1=82=D0=BE=D1=87=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BF=D0=BE=D1=80=D1=8F=D0=B4=D0=BE=D0=BA=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BE=D0=BA=20=D0=BF=D0=BE?= =?UTF-8?q?=D1=81=D0=BB=D0=B5=20SCD2-=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/assignment/README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/assignment/README.md b/docs/assignment/README.md index 3880083..1479c9c 100644 --- a/docs/assignment/README.md +++ b/docs/assignment/README.md @@ -64,6 +64,14 @@ В `bookings_validate` автоматически запускаются все три группы. Пока готовы только задания ODS, оценивайте `validate_ods`: ошибки DDS и DM на этом этапе ожидаемы. После выполнения всех заданий должны пройти все группы. + +Перед следующей загрузкой дождитесь завершения всего `bookings_validate` +и убедитесь, что `validate_dds.scd2_restore` завершился успешно. +SCD2-тест временно меняет данные маршрута, запускает вашу загрузку +и восстанавливает таблицы из копии. Параллельная загрузка может потерять +свои изменения при восстановлении. Если `scd2_restore` упал, сначала +разберите его лог. + `*_dq.sql` тоже нужно реализовать: успешный `SELECT 1;` ничего не проверяет. ## Следующие этапы @@ -77,8 +85,6 @@ Перед DDS разберите [загрузку факта](../design/reading_the_pipeline.md#fact-flight-sales). После реализации измерений выполните [пересчет факта и витрин](analyst_spec.md#пересчёт-факта-после-реализации-измерений). Проверяйте готовый слой своей группой в `bookings_validate`. -В DDS есть активный SCD2-тест: он временно меняет данные маршрута, -запускает вашу загрузку и затем восстанавливает тестируемые данные. ## Если застряли -- 2.54.0