From af2731487869f1017ceaca06f1006a39e254ea2c Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sat, 14 Mar 2026 10:49:40 +0300 Subject: [PATCH] =?UTF-8?q?docs(main):=20=D0=BE=D0=BD=D0=B1=D0=BE=D1=80?= =?UTF-8?q?=D0=B4=D0=B8=D0=BD=D0=B3=20=D1=81=D1=82=D1=83=D0=B4=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=20=E2=80=94=20=D0=B3=D0=B8=D0=B4,=20=D0=BC=D0=B0?= =?UTF-8?q?=D1=80=D0=BA=D0=B5=D1=82=D0=B8=D0=BD=D0=B3,=20=D0=B0=D0=B4?= =?UTF-8?q?=D0=B0=D0=BF=D1=82=D0=B0=D1=86=D0=B8=D1=8F=20docs=20=D0=B8=20do?= =?UTF-8?q?cstrings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - на main docs и DAG-docstrings утверждали «все реализовано», хотя 18 заглушек; студент застревал после шага 6 без явного «что дальше». - Что: - README: маркетинг-баннер, шаг 6 (только эталонные таблицы), шаг 7 → задания. - docs/assignment/README: полный гид студента (эталон → ТЗ → заглушки → validate). - analyst_spec: DAG-интеграция — «таски уже подключены, менять DAG не нужно». - docs ODS/DDS/DM: пометки заглушек, адаптация секций проверки результата. - 4 DAG docstrings: эталон vs задания (заглушки). - Проверка: - make test (4 passed, 14 skipped), make lint (clean). - grep «все 5 витрин|все реализован» — ложных утверждений без оговорок нет. Co-Authored-By: Claude Opus 4.6 --- README.md | 17 +++++-- airflow/dags/bookings_dm_ddl.py | 2 +- airflow/dags/bookings_to_gp_dds.py | 2 + airflow/dags/bookings_to_gp_dm.py | 2 +- airflow/dags/bookings_to_gp_ods.py | 2 + docs/assignment/README.md | 77 ++++++++++++++++++++++++++---- docs/assignment/analyst_spec.md | 5 +- docs/bookings_to_gp_dds.md | 23 +++++++-- docs/bookings_to_gp_dm.md | 40 ++++++++++------ docs/bookings_to_gp_ods.md | 10 ++-- 10 files changed, 139 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index fd8ebf9..5dc3d80 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # airflow-dwh-gp-lab +> Этот стенд можно использовать самостоятельно, но он также является курсовой работой +> плана обучения [Data Engineering Roadmap](https://github.com/dementev-dev/de-roadmap). +> Нужна помощь? Обратитесь к автору материалов — ментору [@dementev_dev](https://t.me/dementev_dev). + Учебный стенд для лабораторных по Data Engineering: **Airflow** оркестрирует загрузку данных из демо‑БД **bookings** (Postgres) в **Greenplum**. @@ -83,18 +87,21 @@ make bookings-init # быстрое восстановление из s make gp-psql -- внутри psql: SELECT COUNT(*) FROM stg.bookings; -SELECT COUNT(*) FROM stg.tickets; -SELECT * FROM stg.bookings ORDER BY event_ts DESC LIMIT 10; SELECT COUNT(*) FROM ods.bookings; -SELECT COUNT(*) FROM ods.tickets; -SELECT COUNT(*) FROM dds.dim_routes; +SELECT COUNT(*) FROM dds.dim_airports; SELECT COUNT(*) FROM dds.fact_flight_sales; SELECT COUNT(*) FROM dm.sales_report; -SELECT COUNT(*) FROM dm.route_performance; +-- Остальные DDS/DM-таблицы пока пусты — вы реализуете их в задании. ``` Подробнее про логику DAG и проверки — `docs/bookings_to_gp_stage.md`. +7) Приступайте к заданию: + +Стенд работает, данные загружены. Переходите к +[учебным заданиям](docs/assignment/README.md) — там описан путь +от изучения эталона до полной реализации DWH. + ## DAG-и в стенде Основные (для потока bookings → DWH): diff --git a/airflow/dags/bookings_dm_ddl.py b/airflow/dags/bookings_dm_ddl.py index 6af5223..21158d2 100644 --- a/airflow/dags/bookings_dm_ddl.py +++ b/airflow/dags/bookings_dm_ddl.py @@ -7,7 +7,7 @@ from __future__ import annotations Создаёт 5 DM-витрин: sales_report, route_performance, passenger_loyalty, airport_traffic, monthly_overview. -На данном этапе реализованы все 5 витрин: sales_report, route_performance, passenger_loyalty, airport_traffic, monthly_overview. +DDL создаёт все 5 витрин. Эталон: sales_report. Остальные — задания. """ from datetime import timedelta diff --git a/airflow/dags/bookings_to_gp_dds.py b/airflow/dags/bookings_to_gp_dds.py index ee44efa..2a0bb65 100644 --- a/airflow/dags/bookings_to_gp_dds.py +++ b/airflow/dags/bookings_to_gp_dds.py @@ -8,6 +8,8 @@ from __future__ import annotations - для каждой сущности выполняем пару задач load -> dq; - для dim_routes применяем SCD2, для остальных измерений — SCD1 UPSERT; - факт грузим инкрементальным UPSERT по зерну (ticket_no, flight_id). + +Эталон: dim_airports, dim_tariffs, dim_calendar, fact_flight_sales. dim_routes, dim_passengers, dim_airplanes — задания (заглушки). """ from datetime import timedelta diff --git a/airflow/dags/bookings_to_gp_dm.py b/airflow/dags/bookings_to_gp_dm.py index c2cfb57..63e5d18 100644 --- a/airflow/dags/bookings_to_gp_dm.py +++ b/airflow/dags/bookings_to_gp_dm.py @@ -9,7 +9,7 @@ from __future__ import annotations - все витрины загружаются параллельно (не зависят друг от друга); - sales_report использует UPSERT (heap-таблица), route_performance — Full Rebuild (AO Column). -На данном этапе реализованы все 5 витрин: sales_report, route_performance, passenger_loyalty, airport_traffic, monthly_overview. +Эталон: sales_report. Остальные 4 витрины — задания (заглушки SELECT 1;). """ from datetime import timedelta diff --git a/airflow/dags/bookings_to_gp_ods.py b/airflow/dags/bookings_to_gp_ods.py index c63f4b4..82dd2e2 100644 --- a/airflow/dags/bookings_to_gp_ods.py +++ b/airflow/dags/bookings_to_gp_ods.py @@ -7,6 +7,8 @@ from __future__ import annotations - весь запуск ODS работает с одним stg_batch_id; - для каждой сущности выполняем пару задач load -> dq; - загрузка реализована как SCD1 UPSERT (UPDATE изменившихся + INSERT новых). + +Эталон: airports, routes и 5 инкрементальных сущностей. airplanes, seats — задания (заглушки). """ from datetime import timedelta diff --git a/docs/assignment/README.md b/docs/assignment/README.md index 9ab1b02..7bebcdd 100644 --- a/docs/assignment/README.md +++ b/docs/assignment/README.md @@ -1,18 +1,77 @@ # Учебные задания -## Техническое задание +## О чём задание -Основной документ: **[analyst_spec.md](analyst_spec.md)** — ТЗ от аналитика -с описанием всех таблиц, маппингами, бизнес-правилами и подсказками. +В стенде уже работает **эталонный вертикальный срез** — полная цепочка +`STG → ODS → DDS → DM` для витрины `dm.sales_report`. Ваша задача — +реализовать недостающие загрузки по аналогии с эталоном. -## Эталон для изучения +На месте ваших заданий сейчас стоят **заглушки** (`SELECT 1;`). Вы замените +их на реальную SQL-логику, и после этого весь DWH будет заполнен. -Перед началом работы изучите эталонный срез (витрина `dm.sales_report` -и вся её цепочка STG → ODS → DDS → DM): +## Перед началом -- SQL-скрипты: `sql/stg/`, `sql/ods/`, `sql/dds/`, `sql/dm/` -- DAG-файлы: `airflow/dags/` -- [Порядок запуска DAG-ов](../dag_execution_order.md) +Убедитесь, что стенд запущен и данные загружены — +пройдите шаги 1–6 из [README (Быстрый старт)](../../README.md#быстрый-старт-основной-сценарий-bookings--stg--ods--dds--dm). + +После этого в Greenplum будут данные в эталонных таблицах (`stg.*`, `ods.bookings`, +`dds.dim_airports`, `dds.fact_flight_sales`, `dm.sales_report` и др.). + +## Шаг 1. Изучите эталон + +Прежде чем писать код, разберите, как устроены эталонные загрузки. +По одному файлу на каждый ключевой паттерн: + +| Паттерн | Файл для изучения | Что посмотреть | +|---------|-------------------|----------------| +| 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-инкремент, агрегация фактов по измерениям | + +Описания каждого 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) + +## Шаг 2. Откройте ТЗ + +Техническое задание от аналитика: **[analyst_spec.md](analyst_spec.md)**. + +Там описаны все таблицы, которые нужно реализовать, с маппингами, бизнес-правилами +и подсказками. Рекомендуемый порядок (от простого к сложному): + +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. Реализуйте задания + +- **Файлы-заглушки уже на месте** — например, `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`: загружает изменённые данные + и проверяет, что версионирование работает корректно. +- Сообщения об ошибках подскажут, что именно не так и что делать дальше. + +**Цель: все проверки зелёные.** + +## Если застряли + +- Изучите эталонные SQL-скрипты — в них те же паттерны, что нужны для заданий. +- Загляните в `docs/design/naming_conventions.md` — единый источник нейминга полей. +- Сверьтесь с **веткой `solution`** — там полная рабочая реализация. ## Для менторов diff --git a/docs/assignment/analyst_spec.md b/docs/assignment/analyst_spec.md index 42769db..e887bbb 100644 --- a/docs/assignment/analyst_spec.md +++ b/docs/assignment/analyst_spec.md @@ -41,8 +41,9 @@ `valid_from`, `valid_to`, `hashdiff`, суффиксы `_bk` / `_sk`). - **SQL-файлы:** располагайте в `sql/{слой}/{объект}_{роль}.sql` (например, `sql/stg/airplanes_ddl.sql`, `sql/stg/airplanes_load.sql`). -- **DAG-интеграция:** используйте `PostgresOperator` + путь к SQL-файлу. - Добавьте таски в существующие DAG-файлы соответствующего слоя. +- **DAG-интеграция:** таски для всех заданий уже подключены в DAG-файлах + соответствующего слоя (`PostgresOperator` + путь к SQL-файлу). + Вам нужно только заменить содержимое SQL-файлов — DAG менять не нужно. - **Идемпотентность:** каждый скрипт загрузки должен быть безопасен при повторном запуске (не создавать дубликатов). - **Шаблон `{{ run_id }}`:** используйте Jinja-шаблон Airflow для `_load_id`. diff --git a/docs/bookings_to_gp_dds.md b/docs/bookings_to_gp_dds.md index 5312103..1166bd1 100644 --- a/docs/bookings_to_gp_dds.md +++ b/docs/bookings_to_gp_dds.md @@ -11,6 +11,9 @@ point-in-time join, защитные LEFT JOIN для устойчивости - `dds.dim_airports`, `dds.dim_airplanes`, `dds.dim_tariffs`, `dds.dim_passengers` — SCD1 UPSERT; - `dds.dim_routes` — **SCD2** с `hashdiff`, `valid_from`, `valid_to` + денормализация. - Загружает факт `dds.fact_flight_sales` — инкрементальный UPSERT по зерну `(ticket_no, flight_id)`. + +> На ветке `main` измерения `dim_routes`, `dim_passengers`, `dim_airplanes` — +> заглушки. Эталон: `dim_calendar`, `dim_airports`, `dim_tariffs`, `fact_flight_sales`. - Для каждой таблицы выполняет пару задач `load → dq`. - Использует `_load_id = {{ run_id }}`. DDS не требует `stg_batch_id`, потому что читает текущее состояние ODS. @@ -68,12 +71,15 @@ load_dds_dim_calendar → dq_dds_dim_calendar | # | Задача | SQL-файлы | Что загружает | |---|--------|-----------|---------------| | 2 | `load_dds_dim_airports` → `dq_dds_dim_airports` | `sql/dds/dim_airports_load.sql`, `sql/dds/dim_airports_dq.sql` | Аэропорты (код, город, координаты) | -| 3 | `load_dds_dim_airplanes` → `dq_dds_dim_airplanes` | `sql/dds/dim_airplanes_load.sql`, `sql/dds/dim_airplanes_dq.sql` | Самолёты (код, модель, кол-во мест) | +| 3 | `load_dds_dim_airplanes` → `dq_dds_dim_airplanes` | `sql/dds/dim_airplanes_load.sql`, `sql/dds/dim_airplanes_dq.sql` | Самолёты (код, модель, кол-во мест) ⚠️ заглушка на main | | 4 | `load_dds_dim_tariffs` → `dq_dds_dim_tariffs` | `sql/dds/dim_tariffs_load.sql`, `sql/dds/dim_tariffs_dq.sql` | Тарифы (класс обслуживания) | -| 5 | `load_dds_dim_passengers` → `dq_dds_dim_passengers` | `sql/dds/dim_passengers_load.sql`, `sql/dds/dim_passengers_dq.sql` | Пассажиры (ID, имя, контакты) | +| 5 | `load_dds_dim_passengers` → `dq_dds_dim_passengers` | `sql/dds/dim_passengers_load.sql`, `sql/dds/dim_passengers_dq.sql` | Пассажиры (ID, имя, контакты) ⚠️ заглушка на main | Паттерн загрузки — SCD1 UPSERT: TEMP TABLE → UPDATE (IS DISTINCT FROM) → INSERT. +> **Задание.** На ветке `main` этот скрипт — заглушка (`SELECT 1;`). +> Описание ниже — спецификация того, что нужно реализовать. Образец SCD2 — в ветке `solution`. + ### 6) `load_dds_dim_routes` → `dq_dds_dim_routes` (SCD2) - **SQL:** `sql/dds/dim_routes_load.sql`, `sql/dds/dim_routes_dq.sql` @@ -156,7 +162,8 @@ make gp-psql ```sql SELECT COUNT(*) FROM dds.dim_calendar; -SELECT COUNT(*) FROM dds.dim_routes; +SELECT COUNT(*) FROM dds.dim_airports; +SELECT COUNT(*) FROM dds.dim_tariffs; SELECT COUNT(*) FROM dds.fact_flight_sales; -- Проверка: кол-во строк факта ≈ кол-во строк ODS segments @@ -164,14 +171,20 @@ SELECT (SELECT COUNT(*) FROM dds.fact_flight_sales) AS fact_rows, (SELECT COUNT(*) FROM ods.segments) AS ods_rows; --- Проверка SCD2: текущие версии маршрутов (valid_to IS NULL) +-- dim_routes, dim_passengers, dim_airplanes — заглушки на main. +-- Проверки ниже станут осмысленны после реализации задания. +SELECT COUNT(*) FROM dds.dim_routes; + +-- Проверка SCD2 (после реализации dim_routes): +-- текущие версии маршрутов (valid_to IS NULL) SELECT COUNT(*) AS current_versions, (SELECT COUNT(*) FROM dds.dim_routes) AS total_versions FROM dds.dim_routes WHERE valid_to IS NULL; ``` -Ожидаемо: `fact_rows = ods_rows`, `current_versions ≤ total_versions`. +Ожидаемо: `fact_rows ≈ ods_rows`, `dim_calendar` и `dim_airports` непусты. +`dim_routes` — после реализации задания: `current_versions ≤ total_versions`. ## Типичные ошибки diff --git a/docs/bookings_to_gp_dm.md b/docs/bookings_to_gp_dm.md index d635709..4f3b02f 100644 --- a/docs/bookings_to_gp_dm.md +++ b/docs/bookings_to_gp_dm.md @@ -1,6 +1,7 @@ # DAG `bookings_to_gp_dm`: `dds` → `dm` в Greenplum Этот DAG — учебный пример загрузки слоя **DM** (Data Mart / витрины) из текущего состояния **DDS**. +Эталон: витрина `sales_report`. Остальные 4 витрины — задания (заглушки `SELECT 1;` на ветке `main`). Все 5 витрин загружаются **параллельно** и демонстрируют разные стратегии загрузки — это ключевая учебная ценность данного DAG. @@ -8,13 +9,13 @@ Загружает 5 витрин параллельно, для каждой — пара `load → dq`: -| Витрина | Зерно (grain) | Паттерн загрузки | -|---------|---------------|------------------| -| `dm.sales_report` | (flight_date, departure_airport_sk, arrival_airport_sk, tariff_sk) | Инкрементальный UPSERT (HWM по датам) | -| `dm.route_performance` | route_bk | Full Rebuild (TRUNCATE + INSERT) | -| `dm.passenger_loyalty` | passenger_sk | Инкрементальный UPSERT (HWM по затронутым ключам) | -| `dm.airport_traffic` | (traffic_date, airport_sk) | Инкрементальный UPSERT (HWM по датам) | -| `dm.monthly_overview` | (year_actual, month_actual, airplane_sk) | Инкрементальный UPSERT (HWM по месяцам) | +| Витрина | Зерно (grain) | Паттерн загрузки | Статус на main | +|---------|---------------|------------------|----------------| +| `dm.sales_report` | (flight_date, departure_airport_sk, arrival_airport_sk, tariff_sk) | Инкрементальный UPSERT (HWM по датам) | Эталон | +| `dm.route_performance` | route_bk | Full Rebuild (TRUNCATE + INSERT) | Задание (заглушка) | +| `dm.passenger_loyalty` | passenger_sk | Инкрементальный UPSERT (HWM по затронутым ключам) | Задание (заглушка) | +| `dm.airport_traffic` | (traffic_date, airport_sk) | Инкрементальный UPSERT (HWM по датам) | Задание (заглушка) | +| `dm.monthly_overview` | (year_actual, month_actual, airplane_sk) | Инкрементальный UPSERT (HWM по месяцам) | Задание (заглушка) | ## Что должно быть готово перед запуском @@ -68,6 +69,9 @@ start_dm - **NULLIF** для защиты от деления на ноль (`boarding_rate = boarded / NULLIF(sold, 0)`); - **Денормализация**: города и коды аэропортов тянутся в витрину из измерений. +> **Задание.** На ветке `main` этот скрипт — заглушка (`SELECT 1;`). +> Реализуйте по описанию ниже и ТЗ в `analyst_spec.md`. + ### 2) `load_dm_route_performance` → `dq_dm_route_performance` - **SQL:** `sql/dm/route_performance_load.sql`, `sql/dm/route_performance_dq.sql` @@ -89,6 +93,9 @@ start_dm Метрики: `avg_load_factor = total_boarded / (total_flights * total_seats)`, `avg_ticket_price = total_revenue / total_tickets`. +> **Задание.** На ветке `main` этот скрипт — заглушка (`SELECT 1;`). +> Реализуйте по описанию ниже и ТЗ в `analyst_spec.md`. + ### 3) `load_dm_passenger_loyalty` → `dq_dm_passenger_loyalty` - **SQL:** `sql/dm/passenger_loyalty_load.sql`, `sql/dm/passenger_loyalty_dq.sql` @@ -105,6 +112,9 @@ start_dm - Фильтрация `passenger_sk IS NOT NULL` — защита от неконсистентных фактов (NULL SK при data quality аномалиях в измерениях). +> **Задание.** На ветке `main` этот скрипт — заглушка (`SELECT 1;`). +> Реализуйте по описанию ниже и ТЗ в `analyst_spec.md`. + ### 4) `load_dm_airport_traffic` → `dq_dm_airport_traffic` - **SQL:** `sql/dm/airport_traffic_load.sql`, `sql/dm/airport_traffic_dq.sql` @@ -116,6 +126,9 @@ start_dm в два «события» (вылет из одного аэропорта и прилёт в другой). Это позволяет собрать единую статистику аэропорта (departures + arrivals) в одном проходе. +> **Задание.** На ветке `main` этот скрипт — заглушка (`SELECT 1;`). +> Реализуйте по описанию ниже и ТЗ в `analyst_spec.md`. + ### 5) `load_dm_monthly_overview` → `dq_dm_monthly_overview` - **SQL:** `sql/dm/monthly_overview_load.sql`, `sql/dm/monthly_overview_dq.sql` @@ -145,19 +158,16 @@ make gp-psql ```sql SELECT COUNT(*) FROM dm.sales_report; -SELECT COUNT(*) FROM dm.route_performance; -SELECT COUNT(*) FROM dm.passenger_loyalty; -SELECT COUNT(*) FROM dm.airport_traffic; -SELECT COUNT(*) FROM dm.monthly_overview; +SELECT COUNT(*) FROM dm.route_performance; -- будет непусто после реализации задания +SELECT COUNT(*) FROM dm.passenger_loyalty; -- будет непусто после реализации задания +SELECT COUNT(*) FROM dm.airport_traffic; -- будет непусто после реализации задания +SELECT COUNT(*) FROM dm.monthly_overview; -- будет непусто после реализации задания -- Инвариант sales_report: посаженных не больше, чем продано SELECT COUNT(*) FROM dm.sales_report WHERE tickets_sold < passengers_boarded; - --- Нет дублей по бизнес-ключу route_performance -SELECT route_bk, COUNT(*) FROM dm.route_performance GROUP BY route_bk HAVING COUNT(*) > 1; ``` -Ожидаемо: все витрины непусты, инварианты соблюдены, дублей нет. +Ожидаемо: `dm.sales_report` непуста. Остальные — после реализации заданий. ## Типичные ошибки diff --git a/docs/bookings_to_gp_ods.md b/docs/bookings_to_gp_ods.md index 3ecc718..f0f6b30 100644 --- a/docs/bookings_to_gp_ods.md +++ b/docs/bookings_to_gp_ods.md @@ -9,7 +9,8 @@ - Определяет `stg_batch_id` — последний согласованный батч, по которому все 4 snapshot-справочника (`airports`, `airplanes`, `routes`, `seats`) уже приехали в STG. - Загружает 9 таблиц ODS: `bookings`, `tickets`, `airports`, `airplanes`, `routes`, `seats`, - `flights`, `segments`, `boarding_passes`. + `flights`, `segments`, `boarding_passes` (на ветке `main` загрузка `airplanes` и `seats` — + заглушки; реализуйте их по ТЗ в `docs/assignment/analyst_spec.md`). - Для каждой таблицы выполняет пару задач `load → dq`. - Snapshot-справочники фильтруются по `stg_batch_id`, транзакционные таблицы — по HWM (`_load_ts`). - Для snapshot-справочников дополнительно синхронизирует ключи (удаляет из ODS записи, @@ -97,9 +98,9 @@ resolve_stg_batch_id | 2 | `load_ods_bookings` → `dq_ods_bookings` | `sql/ods/bookings_load.sql`, `sql/ods/bookings_dq.sql` | HWM (инкремент) | | 3 | `load_ods_tickets` → `dq_ods_tickets` | `sql/ods/tickets_load.sql`, `sql/ods/tickets_dq.sql` | HWM (инкремент) | | 4 | `load_ods_airports` → `dq_ods_airports` | `sql/ods/airports_load.sql`, `sql/ods/airports_dq.sql` | snapshot по `stg_batch_id` | -| 5 | `load_ods_airplanes` → `dq_ods_airplanes` | `sql/ods/airplanes_load.sql`, `sql/ods/airplanes_dq.sql` | snapshot по `stg_batch_id` | +| 5 | `load_ods_airplanes` → `dq_ods_airplanes` | `sql/ods/airplanes_load.sql`, `sql/ods/airplanes_dq.sql` | snapshot по `stg_batch_id` ⚠️ заглушка на main | | 6 | `load_ods_routes` → `dq_ods_routes` | `sql/ods/routes_load.sql`, `sql/ods/routes_dq.sql` | snapshot по `stg_batch_id` | -| 7 | `load_ods_seats` → `dq_ods_seats` | `sql/ods/seats_load.sql`, `sql/ods/seats_dq.sql` | snapshot по `stg_batch_id` | +| 7 | `load_ods_seats` → `dq_ods_seats` | `sql/ods/seats_load.sql`, `sql/ods/seats_dq.sql` | snapshot по `stg_batch_id` ⚠️ заглушка на main | | 8 | `load_ods_flights` → `dq_ods_flights` | `sql/ods/flights_load.sql`, `sql/ods/flights_dq.sql` | HWM (инкремент) | | 9 | `load_ods_segments` → `dq_ods_segments` | `sql/ods/segments_load.sql`, `sql/ods/segments_dq.sql` | HWM (инкремент) | | 10 | `load_ods_boarding_passes` → `dq_ods_boarding_passes` | `sql/ods/boarding_passes_load.sql`, `sql/ods/boarding_passes_dq.sql` | HWM (инкремент) | @@ -118,6 +119,9 @@ resolve_stg_batch_id > который не поддерживает эффективный row-level UPDATE (вызывает bloat). > Для маленьких справочников (~100–300 строк) полная перезагрузка быстрее и чище. +> На ветке `main` загрузка `airplanes` и `seats` — заглушки. +> Паттерн TRUNCATE + INSERT описан выше; используйте `airports_load.sql` как образец. + **Транзакционные таблицы** (`bookings`, `tickets`, `flights`, `segments`, `boarding_passes`) — **SCD1 UPSERT**: