Files
airflow-greenplum/docs/archive/2026-03-11_routes-to-reference.md
T
ddadminandClaude Opus 4.6 23cde47fe7 docs(plans): добавлен план валидационного DAG, архивирован routes-to-reference
- Зачем:
  - план routes-to-reference выполнен, нужен план следующего этапа — валидационный DAG
- Что:
  - перемещён docs/plans/2026-03-11_routes-to-reference.md → docs/archive/
  - создан docs/plans/2026-03-12_validation-dag.md: структура DAG (17 SQL + DAG + smoke-тест),
    активный тест SCD2 (цепочка тасков с безопасным откатом), прошёл 5 раундов ревью ChatGPT
- Проверка:
  - make test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:50:03 +03:00

29 KiB
Raw Blame History

Plan: Перенос STG в эталон, ODS airplanes+seats — студенту

Версия: 4 (после третьего ревью ChatGPT 5.4) Дата: 2026-03-11

Контекст и мотивация

Обнаруженная проблема

При подготовке к Этапу 4 (раскладка по веткам main / solution) обнаружен конфликт между текущим дизайном БД (docs/design/db_schema.md, docs/design/bookings_dds_design.md) и планом курсового задания (docs/design/assignment_design.md).

Суть конфликта: эталонный пайплайн (который должен работать «из коробки» на main-ветке для студента) неработоспособен, если студент ещё не реализовал ни одного задания. Три DDS-измерения (dim_routes, dim_passengers, dim_airplanes) отнесены к заданию студента, но эталонная фактовая таблица fact_flight_sales зависит от них через цепочку LEFT JOIN. Без этих измерений факт загружается с массовыми NULL в ключевых FK, а DQ-проверки блокируют pipeline. Эталонные витрины (sales_report, airport_traffic) становятся бесполезными.

Проблема усугубляется архитектурой batch resolver ODS DAG и тестовыми контрактами, которые требуют данных во всех snapshot-таблицах STG.

Ниже — детальный разбор и выбранное решение.

Фактовая таблица: зависимость от студенческих измерений

fact_flight_sales зависит от всех 7 измерений DDS, включая 3 студенческих: dim_routes, dim_passengers, dim_airplanes.

Критическая цепочка: аэропорты в факте разрешаются через dim_routes:

flt.route_no → dim_routes.route_bk → rte.departure_airport → dim_airports.airport_bk → airport_sk

Если dim_routes пуст (студент ещё не реализовал), то NULL получают не только route_sk, но и оба airport_sk и airplane_sk. Эталонная витрина sales_report становится бесполезной.

Причина: ods.flights содержит только route_no, но не departure_airport / arrival_airport / aircraft_code. Эти поля доступны только через таблицу routes.

Почему нельзя просто использовать DDL-заглушки

Если dim_routes — пустая заглушка, все LEFT JOIN через неё возвращают NULL. Факт загружается, но:

  • departure_airport_sk = NULLsales_report не может группировать по аэропортам
  • arrival_airport_sk = NULLairport_traffic не может считать трафик
  • UPDATE факта не перезаписывает SK (by design, строка 4-5 load-скрипта) → NULL-и остаются навсегда до TRUNCATE + перезагрузки

Дополнительные архитектурные ограничения (из ревью)

  1. Fact DQ блокирует загрузку (fact_flight_sales_dq.sql):

    • Строка 71: passenger_sk IS NULLhard fail (100% NULL → exception)
    • Строка 98: route_sk / airplane_sk IS NULL → при >1% строк → exception
    • На main с пустыми студенческими dims — 100% NULL → pipeline падает
  2. Batch resolver ODS DAG (bookings_to_gp_ods.py:59-77):

    • INTERSECT по stg.airports, stg.airplanes, stg.routes, stg.seats
    • Если любая STG-таблица пуста — общего batch нет → весь ODS pipeline падает
    • Контракт зафиксирован тестом test_ods_sql_contract.py:30

Выбранное решение

Весь STG-слой — эталонный (все 9 таблиц загружаются reference-кодом). Это гарантирует:

  • Batch resolver всегда находит согласованный batch (все 4 snapshot-таблицы заполнены)
  • DAG-зависимости и smoke-тесты STG не требуют изменений
  • Никакого расхождения batch-контракта между main и solution

ODS: airplanes + seats остаются заданием студента (TRUNCATE+INSERT практика).

DDS: dim_airplanes, dim_passengers, dim_routes — задание студента (DDL-заглушки).

Fact lookup: аэропорты через ods.routes (эталон); airplane_sk остаётся point-in-time через dim_routes (NULL на main — допустимо).

Fact DQ: на main ослабить проверки студенческих SK.

Что теряет студент (и почему это приемлемо)

Потеря Компенсация
STG-практика (PXF external tables) Изучает эталонный STG-код; отдельный PXF-практикум в бэклоге (assignment_design.md, раздел 6)
ods.routes (TRUNCATE+INSERT) Остаются ods.airplanes + ods.seats — тот же паттерн

Ключевые элементы задания сохранены:

  • ODS: 2 таблицы (airplanes, seats) — практика TRUNCATE+INSERT
  • DDS: dim_airplanes (SCD1), dim_passengers (SCD1), dim_routes (SCD2) — ключевой вызов
  • DM: 4 витрины разной сложности (от простой к сложной)

Пересчёт факта после реализации студентом

После реализации всех измерений студенту нужно:

  1. Запустить загрузку DDS-измерений (dim_airplanes, dim_passengers, dim_routes)
  2. TRUNCATE dds.fact_flight_sales;
  3. Перезапустить загрузку факта → теперь все SK заполнены
  4. Перезапустить DM-витрины

Это стандартная практика при late-arriving dimensions и хорошая обучающая точка.

airport_traffic — студенческое задание

airport_traffic архитектурно не зависит от студенческих dims (использует только calendar_sk, departure_airport_sk, arrival_airport_sk). Но это не значит, что витрина готова: SQL витрины пишет студент. Данные в факте есть (эталон), студент пишет агрегирующий SQL. Статус: задание студента (как и остальные 3 DM).


Матрица зависимостей DM-витрин

DM витрина calendar airports tariffs routes passengers airplanes Работает без студ. dims?
sales_report (эталон) + + + ДА
airport_traffic (студент) + + Данные есть, SQL пишет студент
route_performance (студент) + + Нет
monthly_overview (студент) + + + + Нет
passenger_loyalty (студент) + + + + Нет

Изменения (код)

1. sql/dds/fact_flight_sales_load.sql — lookup аэропортов через ods.routes

Ветка: chore/bookings-etl (сейчас) + main (Этап 4)

Текущее состояние (строки 51-62):

LEFT JOIN dds.dim_routes AS rte
    ON rte.route_bk = flt.route_no
    AND flt.scheduled_departure::DATE >= rte.valid_from
    AND (rte.valid_to IS NULL OR flt.scheduled_departure::DATE < rte.valid_to)
...
LEFT JOIN dds.dim_airports AS dep
    ON dep.airport_bk = rte.departure_airport       -- через dim_routes!
LEFT JOIN dds.dim_airports AS arr
    ON arr.airport_bk = rte.arrival_airport          -- через dim_routes!
LEFT JOIN dds.dim_airplanes AS ap
    ON ap.airplane_bk = rte.airplane_code            -- через dim_routes!

После изменения:

-- Учебный комментарий: Airport lookup через ods.routes (эталонный справочник),
-- а не через dds.dim_routes (студенческое задание SCD2).
-- Это архитектурное решение: эталонный пайплайн работает независимо от студенческого кода.
-- ROW_NUMBER по validity DESC: выбираем актуальную версию расписания маршрута.
-- Аэропорты вылета/прилёта одинаковы во всех версиях одного route_no.
LEFT JOIN (
    SELECT route_no, departure_airport, arrival_airport
    FROM (
        SELECT route_no, departure_airport, arrival_airport,
               ROW_NUMBER() OVER (PARTITION BY route_no ORDER BY validity DESC) AS rn
        FROM ods.routes
    ) ranked
    WHERE rn = 1
) AS ods_rte ON ods_rte.route_no = flt.route_no
LEFT JOIN dds.dim_routes AS rte
    ON rte.route_bk = flt.route_no
    AND flt.scheduled_departure::DATE >= rte.valid_from
    AND (rte.valid_to IS NULL OR flt.scheduled_departure::DATE < rte.valid_to)
...
LEFT JOIN dds.dim_airports AS dep
    ON dep.airport_bk = ods_rte.departure_airport    -- через ods.routes (эталон)
LEFT JOIN dds.dim_airports AS arr
    ON arr.airport_bk = ods_rte.arrival_airport      -- через ods.routes (эталон)
LEFT JOIN dds.dim_airplanes AS ap
    ON ap.airplane_bk = rte.airplane_code            -- через dim_routes (point-in-time, как прежде)

Почему аэропорты через ods.routes, а airplane через dim_routes:

  • Аэропорты: departure_airport и arrival_airport одинаковы во всех версиях одного route_no (маршрут SVO→LED всегда SVO→LED). Безопасно брать из ODS. Это даёт эталонным витринам (sales_report, airport_traffic) корректные airport_sk даже без студенческого dim_routes.

  • Самолёт: airplane_code теоретически может отличаться между версиями маршрута. Текущий контракт DDS (bookings_dds_design.md:499) фиксирует, что airplane_sk приходит из той же point-in-time версии маршрута, что и route_sk. Менять эту семантику — изменение модели данных, а не стабилизация. На main airplane_sk будет NULL (dim_routes — заглушка). Это допустимо: ни sales_report, ни airport_traffic не используют airplane_sk.

2. sql/dds/fact_flight_sales_dq.sql — ослабить проверки студенческих SK (только main)

Ветка: только main (Этап 4). На solution-ветке полные проверки сохраняются.

Изменения:

  • Строки 65-75 (passenger_sk IS NULL): заменить RAISE EXCEPTION на RAISE NOTICE. На main dim_passengers — заглушка, 100% строк будут с passenger_sk IS NULL. Любой порог (даже >1%) даст exception. Только логирование, без блокировки.

  • Строки 89-108 (route-related FK): разделить на два блока:

    Блок A — эталонные FK (проверка с порогом 1%, как calendar_sk):

    -- departure_airport_sk и arrival_airport_sk заполняются через ods.routes (эталон).
    -- NULL здесь — аномалия данных (пропущен маршрут в ODS), а не отсутствие студенческого кода.
    -- Порог 1% — защита от единичных аномалий источника (аналогично calendar_sk).
    SELECT COUNT(*) INTO v_null_airport
    FROM dds.fact_flight_sales
    WHERE departure_airport_sk IS NULL OR arrival_airport_sk IS NULL;
    
    IF v_null_airport > 0 THEN
        IF v_null_airport * 100.0 / NULLIF(v_row_count, 0) > 1.0 THEN
            RAISE EXCEPTION 'DQ FAILED: NULL airport_sk: % (>1%%)', v_null_airport;
        ELSE
            RAISE NOTICE 'DQ WARNING: NULL airport_sk: % (<=1%%, допустимо)', v_null_airport;
        END IF;
    END IF;
    

    Блок B — студенческие FK (только логирование, RAISE NOTICE):

    -- route_sk и airplane_sk будут NULL, пока студент не реализует dim_routes.
    -- Не блокируем pipeline.
    SELECT COUNT(*) INTO v_null_student
    FROM dds.fact_flight_sales
    WHERE route_sk IS NULL OR airplane_sk IS NULL;
    
    IF v_null_student > 0 THEN
        RAISE NOTICE 'DQ INFO: студенческие SK (route/airplane) NULL: %. '
            'После реализации dim_routes: TRUNCATE fact → перезагрузка.',
            v_null_student;
    END IF;
    

Итоговое правило на main:

  • departure_airport_sk, arrival_airport_skпроверка с порогом 1% (эталон, через ods.routes; >1% → EXCEPTION, <=1% → NOTICE)
  • passenger_sk, route_sk, airplane_skтолько RAISE NOTICE (студенческие заглушки, 100% NULL допустимо)
-- Учебный комментарий: route_sk, airplane_sk и passenger_sk будут NULL,
-- пока вы не реализуете соответствующие DDS-измерения.
-- После реализации: TRUNCATE dds.fact_flight_sales → перезагрузка → все SK заполнены.
-- Полную версию DQ (с блокировкой) см. в ветке solution.

3. ODS Routes DQ: убрать RI-проверку airplane_code (только main)

Ветка: только main (Этап 4). На solution-ветке полные проверки сохраняются.

Почему только ODS, не STG? Весь STG — эталон, stg.airplanes всегда заполнен, stg/routes_dq.sql RI-проверка к stg.airplanes проходит штатно. Менять STG не нужно. На уровне ODS ods.airplanes — студенческая заглушка (пустая), поэтому ods/routes_dq.sql RI-проверка к ods.airplanes упадёт.

Файл:

  • sql/ods/routes_dq.sql — удалить блок строк 143-156 (RI к ods.airplanes)

Добавить комментарий:

-- Учебный комментарий: проверка RI airplane_code → ods.airplanes не выполняется,
-- т.к. таблица ods.airplanes реализуется студентом. После реализации — раскомментируйте
-- (см. ветку solution для полной версии).

4. ODS DAG-зависимость: убрать airplanes → routes (только main)

Ветка: только main (Этап 4)

Почему только ODS, не STG? STG DAG не меняется: stg.airplanes — эталон, барьер check_airplanes_dq >> load_routes_to_stg работает штатно. На уровне ODS dq_ods_airplanes — заглушка (SELECT 1;), формально проходит, но зависимость вводит в заблуждение. Убираем для ясности.

airflow/dags/bookings_to_gp_ods.py (строка 266):

# Было:
[dq_ods_airports, dq_ods_airplanes] >> load_ods_routes
# Стало:
dq_ods_airports >> load_ods_routes

5. Заглушки для студенческих файлов (только main)

Ветка: только main (Этап 4)

STG-слой: весь код остаётся (эталон), заглушки не нужны

Все 9 STG-таблиц загружаются эталонным кодом. Batch resolver работает без изменений.

ODS-слой: заглушки для airplanes + seats

Файл main (студент) solution
sql/ods/airplanes_ddl.sql Полный DDL (таблица нужна) Тот же
sql/ods/airplanes_load.sql Заглушка: SELECT 1; -- TODO Полная реализация
sql/ods/airplanes_dq.sql Заглушка: SELECT 1; Полная реализация
sql/ods/seats_ddl.sql Полный DDL Тот же
sql/ods/seats_load.sql Заглушка Полная реализация
sql/ods/seats_dq.sql Заглушка Полная реализация

DDS-слой: заглушки для 3 измерений

Файл main (студент) solution
sql/dds/dim_routes_ddl.sql Полный DDL (нужен для LEFT JOIN) Тот же
sql/dds/dim_routes_load.sql Заглушка: SELECT 1; -- TODO: реализуйте SCD2 Полная реализация
sql/dds/dim_routes_dq.sql Заглушка: SELECT 1; Полная реализация
sql/dds/dim_passengers_ddl.sql Полный DDL Тот же
sql/dds/dim_passengers_load.sql Заглушка Полная реализация
sql/dds/dim_passengers_dq.sql Заглушка Полная реализация
sql/dds/dim_airplanes_ddl.sql Полный DDL Тот же
sql/dds/dim_airplanes_load.sql Заглушка Полная реализация
sql/dds/dim_airplanes_dq.sql Заглушка Полная реализация

DM-слой: заглушки для 4 витрин

Аналогично: DDL остаётся, load/dq заменяются заглушками. Файлы: airport_traffic, route_performance, monthly_overview, passenger_loyalty.


Изменения (документация)

6. docs/assignment/analyst_spec.md

  • Удалить разделы 1.1-1.3 (все STG-задания) — весь STG теперь эталон
  • Удалить разделы 2.3 (ods.routes) — routes теперь эталон
  • Обновить рекомендуемый порядок: начинается с ODS (airplanes, seats)
  • Добавить мотивационную секцию «Почему STG уже реализован»:
    • Эталонный пайплайн требует согласованного batch по всем snapshot-таблицам
    • Без данных в STG невозможна загрузка ODS и далее по цепочке
    • Студент изучает эталонный STG-код как образец
  • Добавить секцию «Пересчёт факта после реализации измерений»:
    • Инструкция: TRUNCATE fact + re-run после реализации всех DDS-измерений
    • Педагогическая ценность: late-arriving dimensions, dependency management

7. docs/design/assignment_design.md

  • Обновить таблицу «Эталонные таблицы»: добавить весь STG + ods.routes
  • Обновить таблицу «Задание студенту»: убрать STG целиком, ODS оставить airplanes+seats
  • Обновить рекомендуемый порядок: начинается с ODS
  • Обновить структуру валидационного DAG (раздел 4): убрать validate_stg секцию

8. docs/design/bookings_dds_design.md

  • Строки 463, 468 (SQL-скелет факта): обновить JOIN-блок — dep.airport_bk и arr.airport_bk теперь через ods_rte, а не через rte; ap.airplane_bk остаётся через rte (point-in-time)
  • Строка 499-501 (текстовое пояснение): обновить — departure_airport_sk и arrival_airport_sk берутся из ods.routes (эталон); airplane_sk остаётся point-in-time через dim_routes
  • Строка 558 (passenger_sk = 0): убрать утверждение о нулевой толерантности — на main passenger_sk будет 100% NULL (dim_passengers — заглушка)
  • Строка 559 (DQ текстовое описание): обновить — на main student SK (passenger_sk, route_sk, airplane_sk) не блокируют pipeline; airport_sk проверяются с порогом 1%
  • Строка 741 (DQ SQL-пример): обновить встроенный SQL — разделить route-related блок на эталонный (airport_sk, порог 1%) и студенческий (route_sk, airplane_sk, NOTICE)

9. docs/design/db_schema.md

  • Пометить весь STG и ods.routes как эталонные (не студенческие)
  • Строка 3: «Все слои реализованы» — уточнить, что на main ODS airplanes/seats, DDS student dims и DM student vitrines — заглушки
  • Строка 253: DQ-контракт «SQL-скрипты с RAISE EXCEPTION» — добавить оговорку, что на main студенческие DQ-скрипты являются заглушками

10. Тесты

  • tests/test_dags_smoke.py строки 242-243: На main убрать assert барьера dq_ods_airplanes → dq_ods_routes (ODS routes не зависит от ODS airplanes на main). STG-барьеры (строки 123-124) не трогаем: STG целиком эталонный, барьер check_airplanes_dq → check_routes_dq работает.
  • tests/test_ods_sql_contract.py строка 6: На main изменить SNAPSHOT_ENTITIES — исключить студенческие ODS:
    # Было:
    SNAPSHOT_ENTITIES = ("airports", "airplanes", "routes", "seats")
    # Стало (main):
    SNAPSHOT_ENTITIES = ("airports", "routes")
    
    Тесты test_snapshot_load_scripts_use_truncate (строка 13) и test_snapshot_dq_checks_extra_keys (строка 22) проверяют паттерны TRUNCATE+INSERT и v_extra_keys_count в SQL-файлах. Заглушки SELECT 1; не содержат этих паттернов → тесты упадут для airplanes/seats. Исключение из SNAPSHOT_ENTITIES решает проблему. Batch resolver тест (строка 30) проверяет DAG-код (не SQL-файлы) — без изменений.

11. Прочие документы

  • docs/design/PRD.md — обновить чеклист (раздел 10)
  • docs/design/bookings_ods_design.md — комплексное обновление:
    • Строка 29, 515, 534: пометить airplanes и seats как студенческие (на main — заглушки; STG-уровень — эталон, ODS load/dq — студент)
    • Строки 521, 523: скорректировать общий DQ-контракт — на main airplanes_dq.sql и seats_dq.sql являются заглушками, а не полноценными RAISE EXCEPTION скриптами
    • Строка 620: обновить DAG-граф — routes больше не зависит от airplanes на ODS
    • Строка 634: убрать зависимость routes от airplanes в описании FK
  • docs/bookings_to_gp_ods.md — обновить описание
  • docs/bookings_to_gp_dds.md:
    • Строки 111-113: убрать гарантию отсутствия NULL SK в штатном режиме — на main student SK (passenger_sk, route_sk, airplane_sk) будут NULL
    • Строка 116: обновить DQ-семантику — student SK не блокируют (NOTICE); airport_sk — порог 1%
    • Строка 126: обновить описание lookup (airports через ods.routes)
  • docs/reference/qa-plan.md:
    • Строка 80: «ODS: все 9 таблиц не пустые» — на main ods.airplanes и ods.seats будут пустыми by design (студенческие заглушки). Уточнить формулировку.
  • docs/e2e-etl-test-protocol.md — обновить контекст

Порядок выполнения

Сейчас (на chore/bookings-etl)

  1. Код: изменить fact_flight_sales_load.sql (lookup через ods.routes) — п.1
  2. Документация: обновить analyst_spec.md — п.6
  3. Документация: обновить assignment_design.md — п.7
  4. Документация: обновить db_schema.md — п.9
  5. Тестирование: make test + ручная проверка SQL
  6. Коммит

Этап 3 (валидационный DAG)

  • Проектировать с учётом DDL-заглушек
  • validate_stg секцию не делать (STG — эталон)
  • check_dim_routes_scd2 — активный тест (мутация ODS → проверка → откат)

Этап 4 (подготовка main + solution)

  • Fact DQ: ослабить проверки student SK (main only) — п.2
  • Routes DQ: убрать airplane RI (main only) — п.3
  • DAG-зависимости: убрать airplanes → routes (main only) — п.4
  • Заглушки: ODS (airplanes, seats) + DDS (3 dims) + DM (4 vitrines) — п.5
  • Тесты: обновить smoke-тесты (main only) — п.10
  • Документация: bookings_dds_design, PRD, прочее — п.8, п.11

Верификация

После п.1 (fact_flight_sales fix)

make test                         # smoke-тесты DAG

Ручная проверка (при поднятом стенде):

Важно: UPDATE факта не перезаписывает dimension SK (by design, строка 4-5). Уже загруженные строки сохранят старые значения airport_sk (через dim_routes). Для полной проверки новой логики нужен TRUNCATE + перезагрузка:

TRUNCATE dds.fact_flight_sales;
-- Перезапустить DDS DAG (или только task load_dds_fact_flight_sales)

-- Проверка: airport_sk заполнены (через ods.routes), route_sk тоже (dim_routes заполнен на этой ветке)
SELECT
    COUNT(*) AS total,
    COUNT(departure_airport_sk) AS has_dep_sk,
    COUNT(arrival_airport_sk) AS has_arr_sk,
    COUNT(route_sk) AS has_route_sk
FROM dds.fact_flight_sales;

После Этапа 4 (main branch)

  1. make upmake ddl-gp → запустить STG+ODS+DDS DAG-и
  2. Проверить: sales_report содержит данные с корректными аэропортами
  3. Проверить: студенческие ODS/DDS-таблицы пусты (заглушки)
  4. Fact DQ проходит (student SK = NULL, но DQ не блокирует)
  5. Реализовать один студенческий dim → TRUNCATE fact → re-run → проверить SK

Ключевые файлы

Файл Роль Ветка
sql/dds/fact_flight_sales_load.sql Главное изменение: airport lookup через ods.routes обе
sql/dds/fact_flight_sales_dq.sql Student SK: EXCEPTION → NOTICE main only
sql/ods/routes_dq.sql Убрать RI airplane_code → ods.airplanes main only
airflow/dags/bookings_to_gp_ods.py DAG: убрать airplanes → routes main only
tests/test_ods_sql_contract.py SNAPSHOT_ENTITIES: убрать airplanes, seats main only
tests/test_dags_smoke.py ODS-барьер airplanes → routes main only
docs/assignment/analyst_spec.md Убрать STG-задания, убрать ods.routes обе
docs/design/assignment_design.md Обновить таблицы эталон/задание обе
docs/design/bookings_dds_design.md Обновить описание fact lookup обе
docs/design/bookings_ods_design.md Убрать зависимость routes от airplanes main only
docs/design/db_schema.md Пометить STG + ods.routes как эталон обе

STG не трогаем: sql/stg/routes_dq.sql, bookings_to_gp_stage.py, STG smoke-тесты — без изменений. Весь STG эталонный, зависимости работают штатно.