From 35144e49f6a6d89a53915d7e4f1c712bbd56da47 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sun, 20 Sep 2026 15:53:39 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20=D1=81=D0=BE=D0=B3=D0=BB=D0=B0=D1=81?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D1=8B=20=D1=80=D1=83=D0=BA=D0=BE=D0=B2?= =?UTF-8?q?=D0=BE=D0=B4=D1=81=D1=82=D0=B2=D0=B0=20=D0=B7=D0=B0=D0=B3=D1=80?= =?UTF-8?q?=D1=83=D0=B7=D0=BE=D0=BA=20=D0=B8=20=D1=83=D1=87=D0=B5=D0=B1?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D0=BC=D0=B0=D1=80=D1=88=D1=80=D1=83=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Руководства STG/ODS/DDS/DM сверены с действующими SQL и DAG, ожидания учебных заглушек связаны с ТЗ. Самопроверка заданий отделена от проверок разработки и исторических планов. Для dim_routes закреплено обновление описательных полей только у открытых версий. SQL, DAG, DDL и настройки стенда не менялись. Проверки: make lint; make test (4 passed, 14 skipped); карта; 292 локальные ссылки; маршрут чтения и карта в Chromium. Живой ETL не запускался. --- README.md | 8 +- TESTING.md | 170 +++++++++++------- docs/README.md | 53 +++--- docs/assignment/analyst_spec.md | 24 +-- docs/bookings_to_gp_dds.md | 270 ++++++++++++----------------- docs/bookings_to_gp_dm.md | 240 +++++++++---------------- docs/bookings_to_gp_ods.md | 245 +++++++++++--------------- docs/bookings_to_gp_stage.md | 206 +++++++++------------- docs/dag_execution_order.md | 73 +++++--- docs/design/bookings_dds_design.md | 18 +- docs/design/bookings_dm_design.md | 18 +- docs/design/bookings_ods_design.md | 18 +- docs/design/bookings_stg_design.md | 18 +- docs/design/db_schema.md | 12 +- docs/design/naming_conventions.md | 45 +++-- docs/reference/dq_taxonomy.md | 5 + docs/reference/qa-plan.md | 31 ++-- 17 files changed, 699 insertions(+), 755 deletions(-) diff --git a/README.md b/README.md index bdfde28..eacee59 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,8 @@ SELECT COUNT(*) FROM dm.sales_report; -- Остальные DDS/DM-таблицы пока пусты — вы реализуете их в задании. ``` -Подробнее про логику DAG и проверки — `docs/bookings_to_gp_stage.md`. +Порядок повторных запусков и переходов между слоями описан в +[руководстве по запуску DAG](docs/dag_execution_order.md). 7) Разберитесь в данных и приступайте к заданию: @@ -115,7 +116,7 @@ SELECT COUNT(*) FROM dm.sales_report; routes, seats, flights, segments, boarding_passes; включая внешние `*_ext` через PXF); - `bookings_to_gp_stage` — генерирует учебный день в `bookings-db`, затем загружает данные в STG и выполняет DQ‑проверки. - `bookings_ods_ddl` — создаёт/обновляет ODS-таблицы по домену bookings. -- `bookings_to_gp_ods` — загружает данные из STG в ODS (SCD1 UPSERT) и выполняет DQ‑проверки. +- `bookings_to_gp_ods` — заменяет снимки справочников и обновляет транзакционные таблицы ODS из STG; выполняет DQ-проверки. - `bookings_dds_ddl` — создаёт/обновляет DDS-таблицы (`dim_*`, `fact_flight_sales`) по домену bookings. - `bookings_to_gp_dds` — загружает данные из ODS в DDS (SCD1/SCD2 + факт) и выполняет DQ‑проверки. - `bookings_dm_ddl` — создаёт/обновляет DM-витрины (sales_report, route_performance, passenger_loyalty, airport_traffic, monthly_overview). @@ -159,13 +160,14 @@ make clean # полный reset: удалить контейнер - [От карты к SQL загрузки](docs/design/reading_the_pipeline.md) - [Учебные задания: порядок работы и проверка](docs/assignment/README.md) - [Интерактивная карта таблиц и связей](docs/design/architecture-map.html) -- [План тестирования/проверок и негативные кейсы](TESTING.md) - [Дополнительные заметки и технические детали](docs/README.md) - [Детали по STG DAG](docs/bookings_to_gp_stage.md) - [Детали по ODS DAG](docs/bookings_to_gp_ods.md) - [Детали по DDS DAG](docs/bookings_to_gp_dds.md) - [Детали по DM DAG](docs/bookings_to_gp_dm.md) +Для работы над репозиторием: [проверки разработки и стенда](TESTING.md). + ## Типичные проблемы и решения | Проблема | Решение | diff --git a/TESTING.md b/TESTING.md index e2d3c70..646b121 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1,80 +1,118 @@ -# План тестирования (для студентов) +# Проверка изменений репозитория -Этот документ — пошаговый чек‑лист, как проверить, что всё работает: от «быстрых локальных проверок» до запуска стенда в Docker и просмотра данных в Greenplum. Подходит начинающим: просто выполняйте шаги по порядку. +Этот документ предназначен для работы над кодом, документацией и стендом. +Если вы выполняете учебное задание, начните с +[его самопроверки](docs/assignment/README.md#запуск-и-проверка): там объяснены +`*_dq.sql`, группы `bookings_validate` и порядок восстановления после SCD2-теста. -Если что‑то пошло не так, смотрите раздел «Быстрый reset» ниже. +## Локальные проверки -## 1. Быстрая проверка окружения -- `uv sync` — подтягиваем Python и зависимости из `pyproject.toml`/`uv.lock`. -- Проверяем версию uv: `uv --version` (ожидаем ≥ 0.9). -- Убедитесь, что `docker compose version` доступна и Docker запущен. +Из корня репозитория: -## 2. Локальные автоматические проверки (без Docker) -- `make test` — короткие unit-тесты (`tests/test_greenplum_helpers.py`, `tests/test_dags_smoke.py`). - - Smoke-тесты DAG автоматически `skip`, если Airflow не установлен в venv, поэтому прогонится за миллисекунды. -- `make lint` — black/isort в режиме проверки (после `make fmt` должен проходить без ошибок). -- `make fmt` — автоисправление форматирования; полезно запускать перед пушем. -- (опционально) `uv run pytest -q -k dags_smoke` — только DAG smoke. +```bash +uv sync +make lint +make test +``` -## 3. Подготовка Docker-стенда -- `cp .env.example .env` (если файла ещё нет) и проверьте переменные: - - `GP_PORT` — внутренний порт Greenplum в Docker-сети (по умолчанию 5432, менять не нужно); внешний порт для подключения с хоста фиксирован на `5435`, поэтому локальный PostgreSQL на 5432 не помешает. - - `GP_USE_AIRFLOW_CONN=true` при желании использовать Airflow Connection; `false` — fallback на ENV. -- Если меняли `airflow/requirements.txt` или `Dockerfile.airflow`, выполните `make build` перед `make up`. -- `make up` — поднимаем все сервисы. Важно дождаться статуса `healthy` у `pgmeta` и `greenplum` (`docker compose ps`); `greenplum` считается `healthy` только когда поднят и Greenplum, и PXF. -- `make logs` — следим, пока webserver и scheduler не перейдут в рабочее состояние (`Listening at: http://0.0.0.0:8080`). +`make lint` проверяет Black и isort для `airflow/` и `tests/`. +`make fmt` исправляет форматирование в этих каталогах. +Тесты находятся в трех файлах: -## 4. Smoke тесты DAG в Airflow UI -1. Открыть http://localhost:8080 (admin/admin). -2. (опционально) Зайти в Admin → Connections и убедиться, что DAG’и видят подключения: - - `greenplum_conn` и `bookings_db` задаются через переменные `AIRFLOW_CONN_...` в docker-compose и могут не отображаться в списке, но `airflow connections get greenplum_conn` / `bookings_db` внутри контейнера должны отрабатывать без ошибок. +- [test_ods_sql_contract.py](tests/test_ods_sql_contract.py) проверяет + контракты SQL справочников ODS по исходным файлам. +- [test_dags_smoke.py](tests/test_dags_smoke.py) проверяет структуру DAG. + Если импорт `airflow` не предоставляет `DAG`, проверки пропускаются. + Смотрите причины пропусков: `uv run pytest -q -rs`. +- [test_ods_snapshot_integration.py](tests/test_ods_snapshot_integration.py) + по умолчанию пропускается; отдельный запуск описан ниже. -- DAG `bookings_to_gp_stage` (полная проверка цепочки bookings → Greenplum STG): - - предварительно выполнить один раз: `make bookings-init` (быстрое восстановление демобазы `demo` из seed-дампа, ~18 сек) и `make ddl-gp` (создаёт STG/ODS/DDS слои в Greenplum, включая внешние `*_ext` через PXF); - - перед Trigger проверить, что в source реально есть данные (все значения должны быть `> 0`): - - `docker compose exec bookings-db psql -U bookings -d demo -At -c "SELECT COUNT(*) FROM bookings.bookings;"` - - `docker compose exec bookings-db psql -U bookings -d demo -At -c "SELECT COUNT(*) FROM bookings.airports_data;"` - - `docker compose exec bookings-db psql -U bookings -d demo -At -c "SELECT COUNT(*) FROM bookings.airplanes_data;"` - - если хотя бы один `COUNT(*) = 0`, не запускать DAG: повторить `make bookings-init`; если после этого `bookings.bookings` всё ещё пустая, выполнить `make bookings-generate-day` и снова проверить `COUNT(*)`; - - важно: DAG `bookings_stg_ddl` **не** создаёт базу `demo` в `bookings-db`; если вы делали `docker compose down -v` / `make clean`, `make bookings-init` обязателен (быстрое восстановление из seed-дампа); - - включить DAG `bookings_to_gp_stage` и запустить `Trigger DAG`; - - убедиться, что все задачи завершились со статусом Success (включая загрузки справочников/транзакций и DQ); - - при желании проверить данные: в `bookings-db` появился новый день, а в Greenplum в `stg.bookings` — строки с актуальным `_load_id` (см. пример запросов в разделе 5). +Успешный pytest с пропущенными smoke-тестами не подтверждает работоспособность +DAG в Airflow. Число выполненных и пропущенных тестов указывайте в отчете. -- (опционально, для менторов/разработчиков) Smoke-тест DAG через Airflow CLI без UI: - - `docker compose -f docker-compose.yml exec airflow-webserver airflow dags test bookings_to_gp_stage 2024-01-01` — прогоняет `bookings_to_gp_stage` целиком в «off-line» режиме; - - `docker compose -f docker-compose.yml exec airflow-webserver airflow dags trigger bookings_to_gp_stage` — создаёт реальный запуск DAG (логи и статус можно смотреть либо через UI, либо командой `airflow tasks list`/`airflow tasks logs` внутри контейнера). +Для документации проверьте относительные ссылки и якоря, имена файлов, +таблиц и задач. Для карты выполните: -## 5. Проверка данных в Greenplum -- `make gp-psql` — запустить psql в контейнере от имени `gpadmin`. -- (опционально) Проверить, что PXF действительно запущен: - - `docker compose exec greenplum bash -lc "su - gpadmin -c '/usr/local/pxf/bin/pxf cluster status'"` -- Команды внутри psql: - - `\dt public.*` — таблицы схему public. - - (после настройки PXF) `SELECT COUNT(*) FROM public.ext_bookings_bookings;` — проверка чтения из демо-БД bookings через PXF. - - (после настройки PXF) `SELECT * FROM public.ext_bookings_bookings LIMIT 5;` — визуальное сравнение с таблицей `bookings.bookings` в исходной БД. -- Завершить `\q`. +```bash +uv run docs/design/architecture-map/render.py --check +git diff --check +``` -## 6. Негативные сценарии и fallback -- **Проблемы с подключением**: временно изменить `GP_HOST` или `GP_PORT` на несуществующий, перезапустить `make up`, убедиться, что DAG падает с понятной ошибкой (`psycopg2.OperationalError`). -- **Fallback без Airflow Connection**: установить `GP_USE_AIRFLOW_CONN=false`, перезапустить стек (`make down && make up`), удостовериться, что загрузка и DQ работают через ENV. -- **PXF и демобаза bookings** (после настройки PXF и выполнения `make ddl-gp`): временно остановить `bookings-db` (`docker compose stop bookings-db`) и попробовать выполнить `SELECT COUNT(*) FROM public.ext_bookings_bookings;` в `make gp-psql` — ожидается ошибка подключения. Затем запустить `bookings-db` (`docker compose start bookings-db`) и убедиться, что запрос снова работает. +Сборщик сверяет карту с DDL, загрузками и ссылками. Действия в браузере +проверяются отдельно по [чек-листу карты](docs/design/architecture-map/README.md#проверка-в-браузере). -## 7. Быстрый reset (если «что-то сломалось») -- Перезапустить стенд: - - Мягкий вариант (сохранить данные): `make stop`, затем `make up`. - - Полный reset (очистить данные в Docker-томах): `make clean`, затем `make up` (Greenplum/Airflow/bookings будут подняты и инициализированы с нуля). -- Иногда Greenplum не стартует после «грязных» остановок (из‑за старых внутренних файлов). Лечение: всегда делайте `make down` перед повторным `make up`. +## Проверка на стенде -## 8. Снятие метрик и мониторинг -- Контейнеры: `docker compose ps`, `docker stats` (по желанию). -- Логи задач: в Airflow UI → конкретный таск → Log. +Подготовьте стенд по [README](README.md) и проверьте его состояние: -## 9. Завершение работы -- `make down` — выключает сервисы и удаляет контейнеры/сети (volumes сохраняются). -- Полный сброс данных (удаляет volumes): `make clean`. +```bash +make up +docker compose ps +``` -## Текущий статус (пример успешного прогона) -- `uv run pytest -q` — 14 passed, 9 smoke-тестов DAG пропущены (Airflow не установлен в venv). -- `make lint` — проходит (DAG‑файлы отформатированы black/isort). -- Полный ETL-цикл (STG→ODS→DDS→DM) проверен на стенде 2026-03-09: все DAG-и завершились с Success. +Порты берутся из `.env`: `AIRFLOW_WEB_PORT`, `GP_HOST_PORT`, `BOOKINGS_DB_PORT`. +Для локальных подключений используйте их, а для Greenplum внутри Docker-сети +применяется `GP_PORT` (по умолчанию 5432). +Загрузки используют Airflow Connections из Compose; +подробности находятся в [справочнике стенда](docs/stack.md). + +Если изменились `Dockerfile.airflow` или `airflow/requirements.txt`, перед +`make up` выполните `make build`. Инициализация Bookings нужна при создании +стенда: `make bookings-init` пересоздает исходную базу из seed-дампа. +Не используйте ее для обычной проверки SQL на существующих данных. + +Выполните DDL и загрузки по [порядку DAG](docs/dag_execution_order.md). +В Airflow откройте Graph и Log задач измененного слоя; в Greenplum +(`make gp-psql`) выполните проверки из его руководства: +[STG](docs/bookings_to_gp_stage.md#проверка-результата), +[ODS](docs/bookings_to_gp_ods.md#проверка-результата), +[DDS](docs/bookings_to_gp_dds.md#проверка-результата), +[DM](docs/bookings_to_gp_dm.md#проверка-результата). + +Для проверки чтения через PXF используйте действующую внешнюю таблицу: + +```sql +SELECT COUNT(*) FROM stg.bookings_ext; +SELECT * FROM stg.bookings_ext LIMIT 5; +``` + +Сравнить источник можно через `make bookings-psql`. +При ошибке соединения начните с `docker compose ps` и логов нужного сервиса, +затем проверьте настройки из `docs/stack.md`. + +## Дополнительные прогоны + +Интеграционный тест снимка ODS требует работающего Greenplum и базы `gp_dwh`: + +```bash +RUN_ODS_INTEGRATION=1 uv run pytest -q tests/test_ods_snapshot_integration.py +``` + +Он создает, затем удаляет служебные таблицы +`public.it_stg_airports_ods` и `public.it_ods_airports_ods`. +Используйте тестовый стенд, где эти имена не заняты нужными данными. +Тест проверяет замену снимка, преобразование названий и DQ готового `airports`; +задания `airplanes` и `seats` он не проверяет. + +`make e2e-etl` запускает DDL и два прохода STG → ODS → DDS → DM через API Airflow. +Скрипт использует `localhost:8080`; при другом `AIRFLOW_WEB_PORT` проходите +цепочку вручную. Каждый STG добавляет учебный день. Финальные SQL выводят +счетчики, но не превращают все неверные значения в ошибку завершения скрипта: +результаты нужно прочитать с учетом заглушек `main`. + +`make e2e-smoke` начинает с `make clean` и **удаляет все Docker-тома стенда**, +затем инициализирует Bookings и выполняет E2E. Запускайте его только там, +где данные можно потерять. Обычный `make down` сохраняет тома; +`make stop`, затем `make up` подходят для перезапуска с сохранением данных. + +[Исторический QA-план](docs/reference/qa-plan.md) сохраняет многодневные, +негативные и граничные сценарии полной реализации. Это материал для +подготовки отдельной диагностики: перед выполнением сверяйте его SQL +с текущей веткой и учитывайте операции очистки данных. + +## Что указать в отчете + +Укажите коммит, команды и результаты, включая пропуски тестов. +Разделите чтение исходников, проверки ссылок, действия в браузере и реальные +загрузки. Для запуска на стенде сохраните Run ID и логи проверенных задач. +Если ETL или интерфейс не проверялись, укажите это прямо. diff --git a/docs/README.md b/docs/README.md index 14b0c1a..91f0906 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,40 +8,45 @@ 2. [От карты к SQL загрузки](design/reading_the_pipeline.md#ods-airports): разбор готового справочника ODS. Сборка факта понадобится позже, перед DDS. 3. [Учебные задания](assignment/README.md): файлы для работы, порядок запуска - и проверка результата. Поля и бизнес-правила находятся в [ТЗ](assignment/analyst_spec.md). + и [самопроверка](assignment/README.md#запуск-и-проверка). + Поля и бизнес-правила находятся в [ТЗ](assignment/analyst_spec.md). ## Найти деталь во время работы [Интерактивная карта](design/architecture-map.html) ведет от таблицы к ее источникам, потребителям, SQL и пункту задания. - -Карту открывайте из локальной копии: `docs/design/architecture-map.html` в браузере. -Сервер не нужен. Просмотр исходного HTML в Gitea не показывает карту. +Откройте локальный файл `docs/design/architecture-map.html` в браузере; +просмотр исходного HTML в Gitea не показывает карту. [Руководство](design/db_schema.md#как-открыть-карту) объясняет открытие и адресные ссылки. -Если вопрос о запуске или проверке, откройте нужный материал: +| Вопрос | Основной документ | +|---|---| +| В каком порядке запускать слои? | [Порядок запуска DAG](dag_execution_order.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) | +| Какие поля и преобразования нужны в моем задании? | [ТЗ](assignment/analyst_spec.md) | +| Как называются служебные поля и что означают метки времени? | [Соглашения об именах](design/naming_conventions.md) | +| Какие проверки включить в DQ? | [Классы DQ-проверок](reference/dq_taxonomy.md) | +| Как устроены контейнеры, Connections и настройки? | [Справочник стенда](stack.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/`) +[TESTING.md](../TESTING.md) описывает проверки изменений репозитория: +pytest, линтеры, проверку карты и прогоны стенда. Для выполнения первого +задания достаточно самопроверки из его инструкции. -- [Единые конвенции нейминга DWH (служебные поля и SCD)](design/naming_conventions.md) -- [Структура DWH и маршрут по карте](design/db_schema.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) -- [Дизайн-документ DM](design/bookings_dm_design.md) +Для углубленного разбора доступны +[исторический QA-план](reference/qa-plan.md) с многодневными и негативными +сценариями полной реализации и [заметки о генераторе Bookings](reference/bookings_db_issues.md). +QA-план требует отдельного тестового стенда и сверки с текущими SQL. -> Полные дизайн-документы (PRD, assignment_design, архитектурные решения) — в ветке `solution`. +## История разработки -## Справочники (`reference/`) +Планы слоев сохранены по прежним адресам: +[STG](design/bookings_stg_design.md), [ODS](design/bookings_ods_design.md), +[DDS](design/bookings_dds_design.md), [DM](design/bookings_dm_design.md). +В них остаются исходные обоснования, варианты SQL и последовательность +создания файлов. В начале каждого плана есть переход к действующему руководству. +Для реализации учебных загрузок используйте ТЗ и готовые SQL-примеры. -- [Как устроен Docker-стенд (образы, Connections, переменные окружения)](stack.md) -- [Известные проблемы bookings-db](reference/bookings_db_issues.md) -- [QA-план отладки пайплайна](reference/qa-plan.md) -- [Классы DQ-проверок](reference/dq_taxonomy.md) -- [Порядок запуска DAG-ов](dag_execution_order.md) +Полные материалы проектирования (PRD, дизайн заданий, архитектурные решения) +находятся в ветке `solution`. diff --git a/docs/assignment/analyst_spec.md b/docs/assignment/analyst_spec.md index 992747e..cd0daba 100644 --- a/docs/assignment/analyst_spec.md +++ b/docs/assignment/analyst_spec.md @@ -329,8 +329,9 @@ md5(concat_ws('|', фактов на исторические рейсы находил корректный `route_sk`. - **Последующие версии** (маршрут уже был): `valid_from = CURRENT_DATE`. 6. **SCD1-обновление денормализованных полей:** Обновите `departure_city`, - `arrival_city`, `airplane_model`, `total_seats` для ВСЕХ открытых версий - (даже если `hashdiff` не менялся). + `arrival_city`, `airplane_model`, `total_seats` только у открытых версий + (`valid_to IS NULL`), даже если `hashdiff` не менялся. Новая версия при этом + не создается. Закрытые версии сохраняют значения на момент закрытия. **Интервалы версий:** полуоткрытые `[valid_from, valid_to)`. Текущая (актуальная) версия: `valid_to IS NULL`. @@ -650,16 +651,19 @@ FROM dds.fact_flight_sales; ## Валидация -После реализации каждого слоя запустите валидационный DAG `bookings_validate` -в Airflow UI. Он проверит: +После реализации слоя следуйте [порядку запуска и проверки](README.md#запуск-и-проверка). +В `bookings_validate` смотрите группу своего этапа: -- Таблицы существуют и содержат данные -- PK не содержат NULL -- SCD2: корректность `valid_from`/`valid_to`, отсутствие «дыр» в версиях -- Кросс-слойная консистентность (ODS vs STG по кол-ву записей) -- DM-витрины содержат данные за загруженные дни +- `validate_ods` проверяет данные `airplanes` и `seats`, их ключи + и согласованность с выбранным снимком STG. +- `validate_dds` проверяет наличие данных в учебных измерениях, уникальность + пассажиров и сценарий изменения маршрута SCD2 с восстановлением таблиц. +- `validate_dm` проверяет, что четыре учебные витрины содержат строки. -Сообщения об ошибках укажут, что именно не так и что делать дальше. +Этот DAG дополняет ваши `*_dq.sql`. В частности, непустота витрины еще +не подтверждает формулы метрик: проверяйте их по требованиям выше и +[классам DQ](../reference/dq_taxonomy.md). Перед следующей загрузкой дождитесь +всего DAG и успешного `validate_dds.scd2_restore`. --- diff --git a/docs/bookings_to_gp_dds.md b/docs/bookings_to_gp_dds.md index 1166bd1..c452187 100644 --- a/docs/bookings_to_gp_dds.md +++ b/docs/bookings_to_gp_dds.md @@ -1,196 +1,144 @@ -# DAG `bookings_to_gp_dds`: `ods` → `dds` в Greenplum +# Загрузка ODS → DDS: `bookings_to_gp_dds` -Этот DAG — учебный пример загрузки аналитического слоя **DDS** (Star Schema) из текущего состояния **ODS**. -Здесь сосредоточены ключевые паттерны аналитического хранилища: SCD1, SCD2 с hashdiff, -point-in-time join, защитные LEFT JOIN для устойчивости к data quality аномалиям. +DAG наполняет измерения и собирает `dds.fact_flight_sales`. +Одна строка факта соответствует сегменту билета: `(ticket_no, flight_id)`. +У одного билета может быть несколько таких строк. +Сборка показана [на карте](design/architecture-map.html#node=dds.fact_flight_sales) +и в [разборе SQL](design/reading_the_pipeline.md#fact-flight-sales). +Карту открывайте [локально](design/db_schema.md#как-открыть-карту). -## Что делает DAG +## Перед запуском -- Загружает 6 измерений DDS: - - `dds.dim_calendar` — статическое измерение дат (Full Rebuild); - - `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)`. +Дождитесь успешных STG и ODS. Таблицы DDS должны быть созданы через +`bookings_dds_ddl` или общий `make ddl-gp`. +Затем в Airflow UI запустите `bookings_to_gp_dds` целиком. +DDS читает состояние ODS; передавать `stg_batch_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. +| Объект | Что уже выполняется на `main` | +|---|---| +| `dim_calendar` | Заполнение датами с 2016-01-01 по 2030-12-31, только если таблица пуста | +| `dim_airports` | Обновление изменившихся атрибутов и вставка новых аэропортов с постоянными SK | +| `dim_tariffs` | Добавление новых классов обслуживания из `ods.segments` | +| `fact_flight_sales` | Обновление существующих сегментов и вставка новых пар `(ticket_no, flight_id)` | +| `dim_airplanes`, `dim_passengers`, `dim_routes` | Загрузки и DQ пока содержат `SELECT 1;`; требования находятся в [части 2 ТЗ](assignment/analyst_spec.md#часть-2-dds-слой-detailed-data-store--измерения) | -## Что должно быть готово перед запуском +До выполнения заданий три студенческих измерения остаются пустыми. +Готовый факт при этом загружается: отсутствующие ключи измерений сохраняются +как `NULL`, чтобы можно было изучить цепочку до написания собственного SQL. -1) Стенд поднят: +## Порядок задач -```bash -make up -``` +В [DAG](../airflow/dags/bookings_to_gp_dds.py) сначала выполняется +`load_dds_dim_calendar → dq_dds_dim_calendar`. Затем: -2) STG и ODS уже загружены: +1. Параллельно могут загружаться `dim_airports`, `dim_airplanes`, `dim_tariffs` + и `dim_passengers`; у каждого измерения своя следующая DQ-задача. +2. `load_dds_dim_routes` ждет `dq_dds_dim_airports` и `dq_dds_dim_airplanes`, + затем выполняется `dq_dds_dim_routes`. +3. `load_dds_fact_flight_sales` ждет DQ всех пяти измерений после календаря. +4. `dq_dds_fact_flight_sales → finish_dds_summary` завершают запуск. -- выполнены DAG-и `bookings_to_gp_stage` и `bookings_to_gp_ods`; -- DDL-объекты созданы (`bookings_dds_ddl` или `make ddl-gp`). +Это порядок задач Airflow. Источники строк и поля соединений показаны +на карте отдельно от него. -## Как запустить +## Как работают готовые загрузки -1) Откройте Airflow UI: http://localhost:8080. -2) Если запускаете DDS впервые — выполните `bookings_dds_ddl`. -3) Запустите `bookings_to_gp_dds`. +[dim_calendar_load.sql](../sql/dds/dim_calendar_load.sql) проверяет +`NOT EXISTS (SELECT 1 FROM dds.dim_calendar LIMIT 1)`. Если есть хотя бы одна +строка, скрипт не вставляет даты. Повторный запуск не пересоздает календарь +и не дополняет частично заполненную таблицу. +[Его DQ](../sql/dds/dim_calendar_dq.sql) проверяет также покрытие дат рейсов ODS. -## Граф зависимостей +[dim_airports_load.sql](../sql/dds/dim_airports_load.sql) сохраняет SK +существующего аэропорта, обновляет изменившиеся атрибуты через +`IS DISTINCT FROM` и вставляет новые бизнес-ключи. +[dim_tariffs_load.sql](../sql/dds/dim_tariffs_load.sql) только добавляет +недостающие значения `fare_conditions`. +Метки измененных строк относятся к текущему запуску DDS; +календарь не содержит `_load_id` и `_load_ts`. -``` -load_dds_dim_calendar → dq_dds_dim_calendar - ├─ load_dds_dim_airports → dq_dds_dim_airports ─┐ - │ ├─ load_dds_dim_routes - ├─ load_dds_dim_airplanes → dq_dds_dim_airplanes ─┘ └─ dq_dds_dim_routes - │ │ - ├─ load_dds_dim_tariffs → dq_dds_dim_tariffs │ - │ │ - └─ load_dds_dim_passengers → dq_dds_dim_passengers │ - │ - все 5 dq_dds_dim_* ─────────────────────────────────────────┘ - └─ load_dds_fact_flight_sales - └─ dq_dds_fact_flight_sales - └─ finish_dds_summary -``` +В [fact_flight_sales_load.sql](../sql/dds/fact_flight_sales_load.sql) +есть две операции: -Ключевой момент: `dim_routes` зависит от `dim_airports` и `dim_airplanes` (денормализация), -а факт ждёт завершения **всех** пяти измерений. +- `UPDATE` меняет цену, место, признак посадки и метки загрузки, если эти + бизнес-поля изменились. SK измерений остаются прежними. +- `INSERT` собирает данные из ODS и измерений для пар + `(ticket_no, flight_id)`, которых еще нет в факте. -## Как это работает внутри (по шагам) +Факт не отбирает дельту по HWM. Скрипт читает ODS и сопоставляет бизнес-ключи +с уже записанными строками. -### 1) `load_dds_dim_calendar` → `dq_dds_dim_calendar` +Аэропорты вылета и прилета находятся через `ods.routes → dim_airports`. +Из ODS выбирается строка с наибольшим `validity` для `route_no`; сортировка +идет по TEXT, дата рейса в этом отборе не участвует. +`route_sk` определяется отдельно: по дате вылета в полуоткрытом интервале +`[valid_from, valid_to)` измерения `dim_routes`. Из найденной версии берется +код модели для поиска `airplane_sk`. +[Разбор соединений](design/reading_the_pipeline.md#fact-joins) показывает, +какие строки сохраняет `LEFT JOIN` и что происходит при отсутствии измерения. -- **SQL:** `sql/dds/dim_calendar_load.sql`, `sql/dds/dim_calendar_dq.sql` -- **Паттерн:** Full Rebuild — каждый запуск пересоздаёт календарь целиком. - Измерение маленькое и детерминированное, дельту считать нет смысла. +## Учебные измерения -### 2–5) SCD1-измерения (параллельно после calendar) +Начните с [моделей самолетов](assignment/analyst_spec.md#21-ddsdim_airplanes-scd1) +и [пассажиров](assignment/analyst_spec.md#22-ddsdim_passengers-scd1), затем +переходите к [версиям маршрута](assignment/analyst_spec.md#23-ddsdim_routes-scd2). +Поля, hashdiff и алгоритм SCD2 определены в ТЗ. -| # | Задача | 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` | Самолёты (код, модель, кол-во мест) ⚠️ заглушка на 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, имя, контакты) ⚠️ заглушка на main | +У маршрута изменение версионируемых атрибутов создает новую строку. +Описательные поля из других измерений обновляются только у открытых версий +(`valid_to IS NULL`), без новой версии; закрытые версии сохраняют свои значения. +Это правило также записано в [алгоритме ТЗ](assignment/analyst_spec.md#алгоритм-scd2-пошагово). -Паттерн загрузки — SCD1 UPSERT: TEMP TABLE → UPDATE (IS DISTINCT FROM) → INSERT. +После реализации измерений выполните +[пересчет факта и витрин](assignment/analyst_spec.md#пересчёт-факта-после-реализации-измерений). +Обычный повторный запуск DDS не заполнит SK в старых строках факта, +поскольку его `UPDATE` эти поля не меняет. -> **Задание.** На ветке `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` -- **Паттерн:** SCD2 — самый нетривиальный паттерн в проекте. Работает в 3 фазы: - -**Фаза 1. Hashdiff и закрытие старых версий.** -Скрипт считает MD5-хеш от шести бизнес-атрибутов маршрута (`departure_airport`, `arrival_airport`, -`airplane_code`, `days_of_week`, `departure_time`, `duration`). -Если хеш текущей версии в DDS не совпадает с хешем из ODS — старая версия закрывается -(`valid_to = CURRENT_DATE`). Также закрываются маршруты, исчезнувшие из ODS. - -**Фаза 2. Вставка новых версий.** -Для изменённых и совершенно новых маршрутов создаётся новая строка. -`valid_from` выставляется в `1900-01-01` для первой версии маршрута и `CURRENT_DATE` для версии 2+. -Суррогатный ключ (`route_sk`) генерируется через `MAX(route_sk) + ROW_NUMBER()`. - -> **Важно:** такая генерация SK безопасна только при `max_active_runs=1` (Airflow гарантирует -> последовательный запуск). В боевых системах используют sequence. - -**Фаза 3. Обновление денормализованных атрибутов.** -`dim_routes` хранит денормализованные SCD1-атрибуты из `dim_airports` (города) -и `dim_airplanes` (модель, кол-во мест). Если, например, город переименовали — -фаза 3 обновляет **все** версии маршрута (и текущие, и исторические), -при этом `_load_id` и `_load_ts` не перезаписываются (lineage версий сохраняется). - -### 7) `load_dds_fact_flight_sales` → `dq_dds_fact_flight_sales` - -- **SQL:** `sql/dds/fact_flight_sales_load.sql`, `sql/dds/fact_flight_sales_dq.sql` -- **Зерно:** `(ticket_no, flight_id)` — один билет на один рейс. -- **Паттерн:** инкрементальный UPSERT. - -Три учебных приёма в этом скрипте: - -**Защитные LEFT JOIN (defensive coding).** -Все JOIN-ы с измерениями — `LEFT JOIN`. На ветке `solution` все измерения заполнены, -и NULL SK не возникают в штатном режиме. На ветке `main` студенческие измерения -(`dim_passengers`, `dim_routes`, `dim_airplanes`) — заглушки, поэтому соответствующие -SK будут NULL до реализации студентом. - -DQ-проверки факта на main: -- `tariff_sk` — **запрещён** NULL (EXCEPTION); -- `departure_airport_sk`, `arrival_airport_sk` (через `ods.routes`, эталон) — порог **1%** NULL (EXCEPTION); -- `passenger_sk`, `route_sk`, `airplane_sk` (студенческие) — только **NOTICE** (100% NULL допустимо); -- `calendar_sk` — порог **1%** NULL. - -> После реализации всех измерений: `TRUNCATE dds.fact_flight_sales` → перезагрузка → -> все SK заполнены. Полную версию DQ см. в ветке `solution`. - -**Два пути lookup для аэропортов и маршрутов.** -Аэропорты (`departure_airport_sk`, `arrival_airport_sk`) разрешаются через `ods.routes` → -`dim_airports`. Аэропорты вылета/прилёта одинаковы во всех версиях маршрута, поэтому -point-in-time логика не нужна — безопасно брать актуальную версию из ODS. - -`route_sk` и `airplane_sk` разрешаются через point-in-time join с SCD2 `dim_routes`: - -```sql -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) -``` - -Это гарантирует, что факт привязывается к той версии маршрута, которая была актуальна -на дату рейса. - -**UPDATE мутабельных полей.** -UPDATE обновляет только `seat_no`, `price`, `is_boarded` (данные, которые реально -могут измениться — посадка пассажира, корректировка цены). SK измерений не перезаписываются — -они зафиксированы на момент вставки. - -### 8) `finish_dds_summary` - -Ждёт завершения DQ факта и логирует сводку. - -## Как проверить результат - -```bash -make gp-psql -``` +В `make gp-psql` выполните: ```sql SELECT COUNT(*) FROM dds.dim_calendar; SELECT COUNT(*) FROM dds.dim_airports; SELECT COUNT(*) FROM dds.dim_tariffs; -SELECT COUNT(*) FROM dds.fact_flight_sales; --- Проверка: кол-во строк факта ≈ кол-во строк ODS segments SELECT (SELECT COUNT(*) FROM dds.fact_flight_sales) AS fact_rows, - (SELECT COUNT(*) FROM ods.segments) AS ods_rows; + (SELECT COUNT(*) FROM ods.segments) AS segment_rows; --- 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; +SELECT ticket_no, flight_id, departure_airport_sk, arrival_airport_sk, + route_sk, airplane_sk, passenger_sk, price, is_boarded +FROM dds.fact_flight_sales +ORDER BY ticket_no, flight_id +LIMIT 10; ``` -Ожидаемо: `fact_rows ≈ ods_rows`, `dim_calendar` и `dim_airports` непусты. -`dim_routes` — после реализации задания: `current_versions ≤ total_versions`. +На подготовленном стенде готовые измерения и факт непусты. +`fact_rows` должен точно совпасть с `segment_rows`. +[Готовая DQ факта](../sql/dds/fact_flight_sales_dq.sql) также ищет дубли +по паре ключей и проверяет обязательные поля. -## Типичные ошибки +На учебной ветке `NULL` в `passenger_sk`, `route_sk` и `airplane_sk` +вызывает только `NOTICE`. `NULL` в `tariff_sk` вызывает ошибку; +для строк с отсутствующим аэропортом и для `calendar_sk` допустима доля +не выше 1%. Эти послабления не заменяют проверку заполнения ключей после задания. -- `relation "dds..." does not exist`: - - не применён DDS DDL (`bookings_dds_ddl` или `make ddl-gp`). -- DQ падает на `dim_routes`: - - проверьте согласованность `ods.routes` (дубли/аномальные версии) и перезапустите DAG. -- DQ падает на `fact_flight_sales` по coverage: - - проверьте, что ODS DAG завершился успешно без пропуска задач. +Свои `*_dq.sql` реализуйте по ТЗ и +[справочнику DQ](reference/dq_taxonomy.md). Для дополнительной проверки DDS +используйте `validate_dds` в `bookings_validate`, соблюдая +[порядок запуска и восстановления](assignment/README.md#запуск-и-проверка). +Далее переходите к [DM](bookings_to_gp_dm.md). + +## Если загрузка не прошла + +- Нет `dds.*`: выполните `bookings_dds_ddl`. +- DQ календаря сообщает о непокрытых датах: сравните даты вылета ODS + с диапазоном и содержимым `dim_calendar`. Повторный запуск загрузки + непустого календаря не заполнит пропуски. +- Число строк факта отличается от ODS: проверьте ключи билета, бронирования + и рейса, затем соединения в `fact_src`. Потеря строки на `INNER JOIN` + и размножение строк из-за нескольких подходящих версий дают разные причины + одного расхождения. +- Ошибка в вашей DQ маршрутов: проверьте открытые версии, интервалы и hashdiff + по ТЗ. Успешная заглушка `SELECT 1;` этих свойств не проверяет. diff --git a/docs/bookings_to_gp_dm.md b/docs/bookings_to_gp_dm.md index 4f3b02f..a598360 100644 --- a/docs/bookings_to_gp_dm.md +++ b/docs/bookings_to_gp_dm.md @@ -1,181 +1,113 @@ -# DAG `bookings_to_gp_dm`: `dds` → `dm` в Greenplum +# Загрузка DDS → DM: `bookings_to_gp_dm` -Этот DAG — учебный пример загрузки слоя **DM** (Data Mart / витрины) из текущего состояния **DDS**. -Эталон: витрина `sales_report`. Остальные 4 витрины — задания (заглушки `SELECT 1;` на ветке `main`). -Все 5 витрин загружаются **параллельно** и демонстрируют разные стратегии загрузки — -это ключевая учебная ценность данного DAG. +DAG собирает витрины из факта и измерений DDS. На `main` полностью работает +`dm.sales_report`; остальные четыре загрузки и их DQ содержат `SELECT 1;`. +Успешный запуск с заглушками не означает, что все витрины наполнены. -## Что делает DAG +На [карте sales_report](design/architecture-map.html#node=dm.sales_report) +можно проследить источники полей. Как сегменты билетов превращаются +в строки витрины, объясняет [руководство о структуре](design/db_schema.md#агрегация-фактов-в-витрине-продаж). +Карту открывайте [локально](design/db_schema.md#как-открыть-карту). -Загружает 5 витрин параллельно, для каждой — пара `load → dq`: +## Перед запуском -| Витрина | Зерно (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 по месяцам) | Задание (заглушка) | +Дождитесь успешного `bookings_to_gp_dds`. Создайте таблицы через +`bookings_dm_ddl`, если еще не запускали его или общий `make ddl-gp`. +Затем запустите `bookings_to_gp_dm` в Airflow UI. +Общий порядок находится в [инструкции запуска DAG](dag_execution_order.md). -## Что должно быть готово перед запуском +Для готовой `sales_report` достаточно готового факта, календаря, аэропортов +и тарифов. Для своих витрин сначала заполните нужные измерения и выполните +[пересчет факта после заданий DDS](assignment/analyst_spec.md#пересчёт-факта-после-реализации-измерений). -1) Стенд поднят: +## Что делает sales_report -```bash -make up -``` +Зерно витрины: `(flight_date, departure_airport_sk, arrival_airport_sk, tariff_sk)`. +Одна строка содержит продажи сегментов за дату вылета, пару аэропортов и тариф. +`tickets_sold = COUNT(*)` считает строки факта в этой группе. -2) STG, ODS и DDS уже загружены: +[sales_report_load.sql](../sql/dm/sales_report_load.sql) работает в три шага: -- выполнены DAG-и `bookings_to_gp_stage`, `bookings_to_gp_ods`, `bookings_to_gp_dds`; -- DDL-объекты созданы (`bookings_dm_ddl` или `make ddl-gp`). +1. Находит даты вылета фактов с `_load_ts > MAX(_load_ts)` витрины. + Для пустой витрины нижняя граница равна `1900-01-01`. +2. Для этих дат пересчитывает агрегаты по всем их фактам во временную таблицу. + В расчет входят и строки, загруженные ранее: например, новая посадка + меняет итог дня, сохраняя остальные продажи этого дня. +3. Обновляет существующие группы при изменении проверяемых полей и вставляет + новые. `_load_id` записанных строк равен текущему `run_id` DM. -## Как запустить +Если после пропуска запуска в DDS накопились факты с более новой `_load_ts`, +следующий запуск включит их даты в пересчет. Изменение только описания +измерения само по себе не выбирает дату: отбор привязан к метке факта. -1) Откройте Airflow UI: http://localhost:8080. -2) Если запускаете DM впервые — выполните `bookings_dm_ddl`. -3) Запустите `bookings_to_gp_dm`. +[sales_report_dq.sql](../sql/dm/sales_report_dq.sql) берет даты из строк витрины, +записанных текущим запуском. Для них проверяет непустоту при наличии фактов, +дубли ключа, обязательные поля, неотрицательную выручку и долю посадок от 0 до 1. +Если витрина в этом запуске не менялась, DQ выводит `NOTICE` и завершает работу. -## Граф зависимостей +## Учебные витрины -``` -start_dm -├── load_dm_sales_report → dq_dm_sales_report ─┐ -├── load_dm_route_performance → dq_dm_route_performance ─┤ -├── load_dm_passenger_loyalty → dq_dm_passenger_loyalty ─┼─ finish_dm_summary -├── load_dm_airport_traffic → dq_dm_airport_traffic ─┤ -└── load_dm_monthly_overview → dq_dm_monthly_overview ─┘ -``` +Ниже указано ожидаемое поведение заданий. Поля и формулы находятся в ТЗ; +загрузку и DQ для каждого объекта нужно написать самостоятельно. -Все 5 веток полностью независимы и работают параллельно. +| Задание | Одна строка | Что нужно пересчитывать | +|---|---|---| +| [airport_traffic](assignment/analyst_spec.md#31-dmairport_traffic) | Дата и аэропорт | Полные итоги затронутых дат, с учетом ролей вылета и прилета | +| [route_performance](assignment/analyst_spec.md#32-dmroute_performance) | Маршрут по `route_bk` | Всю историю при каждом запуске, объединяя версии маршрута | +| [monthly_overview](assignment/analyst_spec.md#33-dmmonthly_overview) | Год, месяц и модель по `airplane_sk` | Затронутые месяцы; сначала метрики рейса, затем месяца | +| [passenger_loyalty](assignment/analyst_spec.md#34-dmpassenger_loyalty) | Пассажир по `passenger_sk` | Всю историю пассажиров, чьи факты изменились | -## Как это работает внутри (по шагам) +Готовый пример отбора затронутых дат находится в `sales_report_load.sql`. +В заданиях меняется единица пересчета: дата, месяц, пассажир или вся таблица. +Следуйте [порядку заданий](assignment/README.md#следующие-этапы), чтобы каждый +следующий расчет опирался на уже освоенный прием. -### 1) `load_dm_sales_report` → `dq_dm_sales_report` +## Порядок задач -- **SQL:** `sql/dm/sales_report_load.sql`, `sql/dm/sales_report_dq.sql` -- **Паттерн:** инкрементальный UPSERT (HWM по датам). +В [DAG](../airflow/dags/bookings_to_gp_dm.py) `start_dm` запускает пять +независимых веток. Каждая имеет вид +`load_dm_<витрина> → dq_dm_<витрина> → finish_dm_summary`. +Финальная задача ждет все пять DQ. Витрины читают DDS и не читают друг друга, +поэтому между их задачами загрузки нет зависимостей. -Витрина агрегирует продажи билетов по дате, аэропортам вылета/прилёта и тарифу. -Инкрементальность работает через HWM: витрина сравнивает свой `MAX(_load_ts)` с `_load_ts` -фактов в DDS и пересчитывает агрегаты только для **затронутых дат**. +## Проверка результата -> Если витрина пуста — `1900-01-01` заберёт всю историю (первичная загрузка). -> Если DAG не запускался несколько дней — при следующем запуске витрина автоматически -> «догонит» всю накопленную дельту. - -Учебные приёмы: -- **TEMP TABLE** для однократной агрегации (канон для MPP); -- **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` -- **Паттерн:** Full Rebuild (TRUNCATE + INSERT). - -Витрина агрегирует эффективность маршрутов за всю историю. Таблица маленькая (~1000 строк), -поэтому пересоздать её с нуля дешевле, чем вычислять дельту. Дополнительная причина: -таблица хранится в формате **AO Column Store**, который не поддерживает эффективный UPDATE/DELETE. - -Трёхшаговый паттерн: -1. **TRUNCATE** — очистка (единственный эффективный способ для AO). -2. **Агрегация** по `route_bk` — факты суммируются через **все исторические версии** маршрута - (route_sk из SCD2), чтобы не терять данные при версионировании. -3. **JOIN** с текущей (актуальной, `valid_to IS NULL`) версией `dim_routes` для денормализации. - -> Благодаря денормализации `dim_routes` — один JOIN вместо четырёх -> (аэропорты вылета/прилёта, самолёт уже хранятся в `dim_routes`). - -Метрики: `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` -- **Паттерн:** инкрементальный UPSERT по «затронутым ключам». - -В отличие от `sales_report` (где инкремент по датам), здесь HWM находит **конкретных пассажиров** -с новыми фактами, а затем пересчитывает для них всю историю. Это гарантирует точность -накопительных агрегатов (`total_spent`, `first/last_flight_date`). - -Учебные приёмы: -- **DISTINCT ON** (PostgreSQL-специфика) для нахождения моды (самый частый тариф пассажира); -- **Агрегация SCD2 по BK**: при подсчёте уникальных маршрутов используем `route_bk`, - а не `route_sk`, т.к. один маршрут может иметь несколько версий; -- Фильтрация `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` -- **Паттерн:** инкрементальный UPSERT по датам. - -Витрина показывает пассажиропоток аэропортов по дням (вылеты + прилёты). - -Учебный приём — **Dual-role dimension через UNION ALL**: один билет превращается -в два «события» (вылет из одного аэропорта и прилёт в другой). -Это позволяет собрать единую статистику аэропорта (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` -- **Паттерн:** инкрементальный UPSERT по месяцам. - -Витрина показывает помесячную статистику по типам самолётов. - -Учебный приём — **двухуровневая агрегация**: чтобы честно посчитать `avg_load_factor`, -сначала считаем load factor для каждого рейса (`boarded / total_seats`), -затем берём среднее по месяцу. Прямая агрегация `SUM(boarded) / SUM(seats)` дала бы -искажённый результат (взвешенный по числу билетов, а не рейсов). - -> **Ограничение SCD1:** `total_seats` берётся из текущего состояния `dim_airplanes`. -> Если самолёт переоборудовали в прошлом, для точного исторического расчёта -> потребовалось бы SCD2-измерение. - -### 6) `start_dm` / `finish_dm_summary` - -- `start_dm` — стартовый sentinel, от которого расходятся все 5 параллельных веток. -- `finish_dm_summary` — ждёт завершения всех DQ-задач и логирует сводку. - -## Как проверить результат - -```bash -make gp-psql -``` +В `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; -- будет непусто после реализации задания --- Инвариант sales_report: посаженных не больше, чем продано -SELECT COUNT(*) FROM dm.sales_report WHERE tickets_sold < passengers_boarded; +SELECT flight_date, departure_airport_bk, arrival_airport_bk, + fare_conditions, tickets_sold, passengers_boarded, total_revenue +FROM dm.sales_report +ORDER BY flight_date DESC, departure_airport_bk, arrival_airport_bk, + fare_conditions +LIMIT 10; + +SELECT COUNT(*) AS invalid_boarding_rows +FROM dm.sales_report +WHERE tickets_sold < passengers_boarded; ``` -Ожидаемо: `dm.sales_report` непуста. Остальные — после реализации заданий. +После первого успешного запуска `sales_report` непуста, +`invalid_boarding_rows = 0`. Возьмите одну строку и найдите на карте, +откуда пришли ее дата, два аэропорта и тариф. -## Типичные ошибки +После реализации своих витрин запустите `bookings_validate` и посмотрите +`validate_dm`. Проверки этой группы требуют непустые таблицы; +правильность метрик должна проверять ваша DQ по ТЗ. +Соблюдайте [правила запуска валидации](assignment/README.md#запуск-и-проверка): +в том же DAG выполняется активный SCD2-тест с восстановлением данных. -- `relation "dm..." does not exist`: - - не применён DM DDL (`bookings_dm_ddl` или `make ddl-gp`). -- DQ падает на `sales_report` по `boarding_rate`: - - проверьте, что DDS загрузился корректно (`dds.fact_flight_sales` непуста). -- DQ падает на `passenger_loyalty` с ошибкой FK: - - проверьте, что `dds.dim_passengers` содержит всех пассажиров из факта. -- `finish_dm_summary` не выполняется: - - одна из DQ-задач упала; найдите в логах Airflow задачу с ошибкой и исправьте. +## Если загрузка не прошла + +- Нет `dm.*`: выполните `bookings_dm_ddl`. +- `sales_report` пуста: проверьте наличие факта и соответствий его SK + календарю, аэропортам и тарифам. Витрина соединяет их через `INNER JOIN`. +- Учебная витрина пуста: проверьте замену загрузочной заглушки и наличие + необходимых SK факта после заданий DDS. +- Ошибка метрики: разберите одну группу и сравните результат со строками факта + и формулой в ТЗ. Дополнительные примеры проверок есть в + [справочнике DQ](reference/dq_taxonomy.md). +- `finish_dm_summary` не выполняется: найдите первую упавшую задачу, + откройте ее Log и исправьте причину до следующего запуска слоя. diff --git a/docs/bookings_to_gp_ods.md b/docs/bookings_to_gp_ods.md index f0f6b30..321a54a 100644 --- a/docs/bookings_to_gp_ods.md +++ b/docs/bookings_to_gp_ods.md @@ -1,180 +1,139 @@ -# DAG `bookings_to_gp_ods`: `stg` → `ods` в Greenplum +# Загрузка STG → ODS: `bookings_to_gp_ods` -Этот DAG — учебный пример загрузки типизированного слоя **ODS** из уже подготовленного слоя **STG**. -Логика каноничная: **TRUNCATE + INSERT** для snapshot-справочников, **SCD1 UPSERT** для транзакционных -таблиц + DQ-проверки. +DAG приводит данные STG к целевым типам и оставляет одну строку на бизнес-ключ. +Снимки справочников заменяют предыдущее состояние; транзакционные таблицы +получают новые и измененные строки. Смысл ключей показан на +[карте ODS](design/architecture-map.html#node=ods.airports), +а готовый снимок разобран в [stg.airports → ods.airports](design/reading_the_pipeline.md#ods-airports). +Карту открывайте [локально](design/db_schema.md#как-открыть-карту). -## Что делает DAG +## Перед запуском -- Определяет `stg_batch_id` — последний согласованный батч, по которому все 4 snapshot-справочника - (`airports`, `airplanes`, `routes`, `seats`) уже приехали в STG. -- Загружает 9 таблиц ODS: `bookings`, `tickets`, `airports`, `airplanes`, `routes`, `seats`, - `flights`, `segments`, `boarding_passes` (на ветке `main` загрузка `airplanes` и `seats` — - заглушки; реализуйте их по ТЗ в `docs/assignment/analyst_spec.md`). -- Для каждой таблицы выполняет пару задач `load → dq`. -- Snapshot-справочники фильтруются по `stg_batch_id`, транзакционные таблицы — по HWM (`_load_ts`). -- Для snapshot-справочников дополнительно синхронизирует ключи (удаляет из ODS записи, - отсутствующие в выбранном STG-батче). -- Для `flights` дополнительно добирает рейсы из истории `stg.flights`, если на них - ссылаются `stg.segments` (чтобы сохранить ссылочную целостность `segments.flight_id → flights.flight_id`). +Дождитесь успешного `bookings_to_gp_stage`. Создайте ODS через +`bookings_ods_ddl`, если еще не запускали его или общий `make ddl-gp`. +Подготовка стенда описана в [README](../README.md), последовательность слоев +в [порядке запуска DAG](dag_execution_order.md). -## Что должно быть готово перед запуском +На `main` работают `airports`, `routes`, `bookings`, `tickets`, `flights`, +`segments` и `boarding_passes`. Загрузки и DQ для `airplanes` и `seats` +содержат `SELECT 1;`. Успешный DAG с этими заглушками оставит две таблицы пустыми. +Их нужно заполнить в [первом задании](assignment/README.md#первая-загрузка-ods). -1) Стек поднят: +## Запуск и выбор снимка -```bash -make up -``` +В Airflow UI запустите `bookings_to_gp_ods` целиком. +Первая задача, `resolve_stg_batch_id`, выбирает последний общий `_load_id` +четырех справочников STG: `airports`, `airplanes`, `routes`, `seats`. +Общие батчи находятся через `INTERSECT`, последний выбирается по времени +загрузки их строк. Результат передается в SQL через XCom. -2) STG-слой создан и заполнен: - -- запущен `bookings_stg_ddl` (или `make ddl-gp`); -- хотя бы один раз выполнен DAG `bookings_to_gp_stage`. - -3) ODS-таблицы созданы (один из вариантов): - -- учебный: запустить DAG `bookings_ods_ddl`; -- шорткат: `make ddl-gp` (создаёт и STG, и ODS). - -## Как запустить - -1) Откройте Airflow UI: http://localhost:8080. -2) Запустите DAG `bookings_to_gp_ods`. -3) (Опционально) передайте `stg_batch_id` в конфиге запуска: +Resolver проверяет наличие батча в таблицах, а не статус запуска STG в Airflow. +Поэтому сначала дождитесь всех STG DQ. Если нужен определенный снимок, +передайте в Trigger DAG JSON с его реальным `_load_id`: ```json -{"stg_batch_id": "manual__2026-02-22T12:00:00+00:00"} +{"stg_batch_id": ""} ``` -Если конфиг не передан, DAG автоматически возьмёт последний согласованный snapshot-батч. +Явно переданный идентификатор resolver принимает без проверки согласованности. +Убедитесь, что он есть во всех четырех справочниках; произвольная строка +или Run ID запуска ODS здесь не подойдут. -## Граф зависимостей +## Что выполняет загрузка -``` -resolve_stg_batch_id - ├─ load_ods_bookings → dq_ods_bookings - │ └─ load_ods_tickets → dq_ods_tickets ──────────────────┐ - │ │ - ├─ load_ods_airports → dq_ods_airports ─┐ │ - │ ├─ load_ods_routes │ - ├─ load_ods_airplanes → dq_ods_airplanes ─┤ └─ dq_ods_routes - │ │ └─ load_ods_flights - │ │ └─ dq_ods_flights ─┐ - │ │ │ - │ │ dq_ods_flights + dq_ods_tickets - │ │ └─ load_ods_segments - │ │ └─ dq_ods_segments - │ │ └─ load_ods_boarding_passes - │ │ └─ dq_ods_boarding_passes ─┐ - │ │ │ - │ └─ load_ods_seats │ - │ └─ dq_ods_seats ─────────────────────────────────┤ - │ │ - └────────────────────────────────────────────────────────────────── finish_ods_summary ◀──────────┘ -``` +| Объекты | Реализация на `main` | Как отбираются данные | +|---|---|---| +| `airports`, `routes` | Готовые `TRUNCATE + INSERT` | Один снимок по `stg_batch_id`, дедупликация по бизнес-ключу | +| `airplanes`, `seats` | Заглушки; требуемый полный снимок описан в [части 1 ТЗ](assignment/analyst_spec.md#часть-1-ods-слой-operational-data-store) | По выбранному `stg_batch_id` после реализации задания | +| `bookings`, `tickets`, `flights`, `segments`, `boarding_passes` | Готовые `UPDATE + INSERT` через временную таблицу | Новые строки STG по `_load_ts` относительно `MAX(_load_ts)` соответствующей ODS-таблицы | -Ветка `seats` работает параллельно с веткой `routes → flights → segments → boarding_passes`. -Обе ветки сходятся на `finish_ods_summary`. +В [airports_load.sql](../sql/ods/airports_load.sql) и +[routes_load.sql](../sql/ods/routes_load.sql) сначала очищается таблица, +затем вставляется выбранный снимок. Вместе с заменой исчезают ключи, +которых больше нет в снимке. Это один полный пересчет небольшой AO-таблицы; +отдельного `DELETE` для синхронизации ключей нет. -## Как это работает внутри (по шагам) +Для транзакционных таблиц, например [bookings](../sql/ods/bookings_load.sql), +дельта фиксируется во временной таблице до `UPDATE` и `INSERT`. +Поэтому обе операции читают один набор строк, даже когда обновление меняет +максимальную `_load_ts` в ODS. Дельта может охватывать несколько STG-батчей; +параметр `stg_batch_id` ее не ограничивает. -### 1) `resolve_stg_batch_id` (Python) +[flights_load.sql](../sql/ods/flights_load.sql) дополнительно читает историю +STG для рейсов, на которые ссылаются новые сегменты. Так сегмент может получить +свой рейс, даже если тот пришел раньше текущего инкремента. -Определяет, какой STG-батч использовать для snapshot-справочников. -Если `stg_batch_id` не передан через `dag_run.conf`, ищет последний **согласованный** батч — -`_load_id`, который есть одновременно во всех четырёх snapshot-таблицах -(`stg.airports`, `stg.airplanes`, `stg.routes`, `stg.seats`). -Для этого используется **INTERSECT** по `_load_id`. +Метки загрузки имеют два варианта: -> **Зачем согласованность?** Чтобы ODS загружал только те данные, для которых приехали -> ВСЕ связанные справочники. Иначе возможна потеря ссылочной целостности при сборке витрин. +- У снимков `_load_id` сохраняет выбранный STG-батч, `_load_ts = now()`. +- У транзакционных строк обе метки переносятся из STG. Сохраненная `_load_ts` + служит границей следующего инкремента. -### 2–10) Загрузка 9 таблиц: `load_ods_*` → `dq_ods_*` +Названия и смысл служебных полей собраны в +[соглашениях об именах](design/naming_conventions.md). -Каждая пара задач работает одинаково: +## Порядок задач на main -| # | Задача | SQL-файл | Тип загрузки | -|---|--------|----------|--------------| -| 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` ⚠️ заглушка на 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` ⚠️ заглушка на 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 (инкремент) | +В [DAG](../airflow/dags/bookings_to_gp_ods.py) после resolver одновременно +могут стартовать три ветки. В таблице указаны зависимости задач Airflow: -### Два паттерна загрузки +| Загрузка | Что должно завершиться перед ней | +|---|---| +| `load_ods_bookings`, `load_ods_airports`, `load_ods_airplanes` | `resolve_stg_batch_id` | +| `load_ods_tickets` | `dq_ods_bookings` | +| `load_ods_routes` | `dq_ods_airports` | +| `load_ods_seats` | `dq_ods_airplanes` | +| `load_ods_flights` | `dq_ods_routes` | +| `load_ods_segments` | `dq_ods_flights` и `dq_ods_tickets` | +| `load_ods_boarding_passes` | `dq_ods_segments` | -В ODS используются **два разных паттерна** — выбор зависит от типа данных и формата хранения: +Каждый `load_ods_*` ведет к своему `dq_ods_*`. +`finish_ods_summary` ждет `dq_ods_boarding_passes` и `dq_ods_seats`. +На `main` маршруты не ждут студенческий `airplanes`; проверка ссылки +`routes.airplane_code → airplanes` в их DQ отключена. -**Snapshot-справочники** (`airports`, `airplanes`, `routes`, `seats`) — **TRUNCATE + INSERT**: +## Проверка результата -1. **TRUNCATE** — полная очистка таблицы. -2. **INSERT** — вставка всех строк из STG-батча (`_load_id = stg_batch_id`) с дедупликацией - через `ROW_NUMBER()`. - -> Почему не UPSERT? Эти таблицы хранятся в формате **AO Row** (`appendonly=true`), -> который не поддерживает эффективный row-level UPDATE (вызывает bloat). -> Для маленьких справочников (~100–300 строк) полная перезагрузка быстрее и чище. - -> На ветке `main` загрузка `airplanes` и `seats` — заглушки. -> Паттерн TRUNCATE + INSERT описан выше; используйте `airports_load.sql` как образец. - -**Транзакционные таблицы** (`bookings`, `tickets`, `flights`, `segments`, `boarding_passes`) — -**SCD1 UPSERT**: - -1. **TEMP TABLE** — собирает дельту (новые/изменённые строки) с дедупликацией внутри батча - через `ROW_NUMBER()`. Временная таблица автоматически удаляется (`ON COMMIT DROP`). -2. **UPDATE** — обновляет существующие строки. Использует `IS DISTINCT FROM` для корректного - сравнения `NULL`-значений (обычный `<>` не обнаружит изменение `NULL → значение`). -3. **INSERT** — добавляет новые строки (которых нет в ODS по бизнес-ключу). - -Транзакционные таблицы фильтруются по **HWM** — `WHERE _load_ts > (SELECT MAX(_load_ts) FROM ods.table)`. -Это сделано, чтобы не потерять инкременты, если STG-DAG запускался несколько раз -до запуска ODS-DAG'а. - -### DQ-проверки (одинаковый паттерн) - -Каждый `*_dq.sql` — PL/pgSQL-блок (`DO $$...$$`), который проверяет: -- нет дублей по бизнес-ключу в ODS; -- все ключи из STG текущего батча присутствуют в ODS; -- обязательные поля не содержат NULL. - -При ошибке — `RAISE EXCEPTION` с понятным текстом. Для инкрементальных таблиц пустой батч допустим. - -### 11) `finish_ods_summary` - -Ждёт завершения обеих параллельных веток (`dq_ods_boarding_passes` и `dq_ods_seats`) -и логирует краткую сводку. - -## Как проверить результат - -```bash -make gp-psql -``` +В `make gp-psql` посмотрите строки готовых таблиц: ```sql +SELECT airport_code, airport_name, city, _load_id, _load_ts +FROM ods.airports +ORDER BY airport_code +LIMIT 10; + SELECT COUNT(*) FROM ods.bookings; SELECT COUNT(*) FROM ods.tickets; -SELECT COUNT(*) FROM ods.flights; +SELECT COUNT(*) FROM ods.segments; --- Проверка: в ODS не должно быть дублей по бизнес-ключу SELECT book_ref, COUNT(*) FROM ods.bookings -GROUP BY 1 +GROUP BY book_ref HAVING COUNT(*) > 1; ``` -Ожидаемо: в последнем запросе `0` строк. +После первого полного запуска готовые таблицы непусты; последний запрос +не должен вернуть строк. У `ods.airports` видны русские названия и выбранный +STG-батч. Сравните его с XCom задачи `resolve_stg_batch_id`. -## Типичные ошибки +Готовые DQ проверяют ключи, обязательные поля и связи. Для снимков +[airports_dq.sql](../sql/ods/airports_dq.sql) и +[routes_dq.sql](../sql/ods/routes_dq.sql) ищут и пропущенные, и лишние ключи +относительно выбранного батча. DQ транзакционных таблиц проверяют покрытие +ключей выбранного батча, хотя загрузка читает всю дельту по HWM. +Пустой транзакционный батч допустим, пустой снимок справочника считается ошибкой. -- `stg_batch_id не найден`: - - передайте `stg_batch_id` в `dag_run.conf`, или - - сначала загрузите STG через `bookings_to_gp_stage`. -- Ошибки `relation "ods...." does not exist`: - - не применён ODS DDL (`bookings_ods_ddl` / `make ddl-gp`). -- Ошибки DQ по ссылочной целостности: - - проверьте, что ODS DAG выполнялся с корректным `stg_batch_id` и без пропуска upstream задач. +После реализации `airplanes` и `seats` пройдите +[самопроверку первого задания](assignment/README.md#запуск-и-проверка). +Дальше можно переходить к [DDS](bookings_to_gp_dds.md). + +## Если загрузка не прошла + +- `stg_batch_id не найден`: проверьте, что STG загрузил все четыре справочника. + Одна заполненная `stg.bookings` для выбора снимка недостаточна. +- Нет `ods.*`: выполните `bookings_ods_ddl`. +- Ошибка приведения типа: в логе найдите поле, затем сравните значение + в STG с преобразованием в `*_load.sql` и типом в DDL. +- DQ сообщает о пропущенных ключах: сопоставьте выбранный батч, SQL загрузки + и конкретную связь на карте. Для учебных таблиц проверьте, что заменены + обе заглушки: загрузка и DQ. diff --git a/docs/bookings_to_gp_stage.md b/docs/bookings_to_gp_stage.md index 6d1e5a1..eb3f8e3 100644 --- a/docs/bookings_to_gp_stage.md +++ b/docs/bookings_to_gp_stage.md @@ -1,155 +1,107 @@ -# DAG `bookings_to_gp_stage`: `bookings-db` → `stg` в Greenplum +# Загрузка Bookings → STG: `bookings_to_gp_stage` -Этот DAG — основной учебный пример в стенде. Он показывает путь данных из источника **Postgres** -(`bookings-db`, демо‑БД `demo`) в сырой слой **STG** в **Greenplum** с инкрементальной загрузкой -и простыми проверками качества данных. +DAG читает девять таблиц Bookings через внешние таблицы PXF и сохраняет +строки в Greenplum. Весь STG, включая DQ, уже реализован на `main`. +Смысл бронирования, билета и сегмента разобран в +[руководстве о структуре](design/db_schema.md); на карте начните со +[stg.bookings](design/architecture-map.html#node=stg.bookings). +Карту открывайте [локально в браузере](design/db_schema.md#как-открыть-карту). -## Что делает DAG +## Перед запуском -- В источнике (`bookings-db`) генерирует следующий учебный день данных в `bookings.bookings` - (генератор всегда “шагает” вперёд от `max(book_date)`). -- В Greenplum загружает инкремент в `stg.bookings` через внешнюю таблицу `stg.bookings_ext`, используя PXF. -- Сверяет количество строк между источником (за окно инкремента) и загруженным батчем. -- Загружает инкремент в `stg.tickets` через внешнюю таблицу `stg.tickets_ext`, используя PXF. -- Запускает DQ‑проверки для `stg.tickets` (количество, ссылочная целостность, обязательные поля). -- Загружает справочники (full load): `stg.airports`, `stg.airplanes`, `stg.routes`, `stg.seats` + DQ. -- Загружает транзакции: `stg.flights` (инкремент), `stg.segments` (инкремент), `stg.boarding_passes` (инкремент через tickets/bookings) + DQ. +Выполните подготовку из [быстрого старта](../README.md#быстрый-старт-основной-сценарий-bookings--stg--ods--dds--dm): +стенд должен работать, база `demo` содержать исходные данные, +а DAG `bookings_stg_ddl` создать внутренние таблицы `stg.*` и внешние `stg.*_ext`. +Альтернатива DDL DAG: `make ddl-gp`, который создает все четыре слоя. -## Что должно быть готово перед запуском +Подключения `bookings_db` и `greenplum_conn` заданы через `AIRFLOW_CONN_...` +в Compose и могут не отображаться в списке Connections. +Порты, учетные данные и настройка PXF описаны в [справочнике стенда](stack.md). -1) Стек поднят: +В Airflow UI (по умолчанию http://localhost:8080) запустите +`bookings_to_gp_stage` через Trigger DAG и дождитесь завершения всех задач. +Каждый полный запуск добавляет следующий учебный день в источник. +Дата запуска Airflow не задает день генерируемых данных. -```bash -make up -``` +## Какие строки загружаются -2) Демо‑БД bookings установлена и содержит данные: +[generate_bookings_day](../sql/src/bookings_generate_day_if_missing.sql) +вызывает `continue(...)` до конца следующего дня после `MAX(book_date)`. +Для пустой установленной базы он вызывает `generate(...)`, используя настройки +`bookings.start_date`, `bookings.init_days` и `bookings.jobs`. +Эти настройки задаются при инициализации Bookings. -```bash -make bookings-init -``` +Внешняя таблица `*_ext` читает источник; внутренний STG хранит полученные строки. +Бизнес-поля сохраняются как TEXT. `_load_id` равен `run_id` запуска STG, +`_load_ts` фиксирует время записи. Опорное `event_ts` зависит от таблицы: -3) В Greenplum созданы STG‑объекты (внешние `*_ext` через PXF и внутренние таблицы слоя `stg`) -для всех таблиц потока: `bookings`, `tickets`, `airports`, `airplanes`, `routes`, `seats`, `flights`, -`segments`, `boarding_passes` (выберите один вариант): +| Таблицы | Отбор строк | Готовый SQL | +|---|---|---| +| `bookings` | Новые `book_date` | [bookings_load.sql](../sql/stg/bookings_load.sql) | +| `tickets`, `segments`, `boarding_passes` | По `book_date` связанного бронирования через билеты | [tickets](../sql/stg/tickets_load.sql), [segments](../sql/stg/segments_load.sql), [boarding_passes](../sql/stg/boarding_passes_load.sql) | +| `flights` | Новые `scheduled_departure` | [flights_load.sql](../sql/stg/flights_load.sql) | +| `airports`, `airplanes`, `routes`, `seats` | Полный снимок при каждом запуске; `event_ts = now()` | [airports](../sql/stg/airports_load.sql), [airplanes](../sql/stg/airplanes_load.sql), [routes](../sql/stg/routes_load.sql), [seats](../sql/stg/seats_load.sql) | -- учебный вариант: запустить DAG `bookings_stg_ddl` в Airflow UI; -- технический шорткат: `make ddl-gp`. +У каждой инкрементальной таблицы своя нижняя граница: +`MAX(event_ts)` по предыдущим батчам этой таблицы. Верхней границы нет. +`NOT EXISTS` не дает повторно вставить тот же ключ в текущий `_load_id`. +У справочников сохраняются снимки разных запусков, поэтому повторение +бизнес-ключа между батчами ожидаемо. -4) Airflow Connections: +Такой инкремент не перечитывает все старые строки источника: например, +изменение рейса с датой ниже границы само по себе в новый STG-батч не попадет. +Для проверки повторяемости загрузки учитывайте и генератор: +повторный запуск `generate_bookings_day` снова продвинет источник на день. -- `bookings_db` — подключение к источнику `bookings-db`; -- `greenplum_conn` — подключение к Greenplum. +## Порядок задач -По умолчанию они задаются через переменные окружения `AIRFLOW_CONN_...` в `docker-compose.yml`, -поэтому могут не отображаться в UI — для `PostgresOperator` это нормально. +В [DAG](../airflow/dags/bookings_to_gp_stage.py) после каждой загрузки стоит DQ. +Ниже стрелки показывают порядок выполнения задач Airflow. +Связи строк по ключам смотрите на карте. -## Как запустить +1. `generate_bookings_day → load_bookings_to_stg → check_row_counts`. +2. `load_tickets_to_stg → check_tickets_dq`. +3. После билетов параллельно могут выполняться загрузки аэропортов и моделей. + `load_routes_to_stg` ждет `check_airports_dq` и `check_airplanes_dq`; + `load_seats_to_stg` ждет только `check_airplanes_dq`. +4. После проверки маршрутов идет цепочка + `flights → segments → boarding_passes`, у каждой таблицы своя DQ-задача. +5. `finish_summary` ждет `check_boarding_passes_dq` и `check_seats_dq`. -1) Откройте Airflow UI: http://localhost:8080 (admin/admin). -2) Запустите DAG `bookings_to_gp_stage` вручную (Trigger DAG). -3) Дождитесь статуса Success у всех задач. +Инкрементальные DQ допускают пустое окно: выводят `NOTICE` и завершаются успешно. +Пустой источник справочника считается ошибкой. Сверки выполняются для текущего +батча; примеры: [bookings_dq.sql](../sql/stg/bookings_dq.sql) и +[airports_dq.sql](../sql/stg/airports_dq.sql). -## Как это работает внутри (по шагам) +## Проверка результата -1) `generate_bookings_day` - -- выполняет `sql/src/bookings_generate_day_if_missing.sql` в `bookings-db`; -- если `bookings.bookings` пустая — вызывает `generate(...)` на `BOOKINGS_INIT_DAYS`; -- иначе — вызывает `continue(...)`, добавляя ровно один следующий день. - -2) `load_bookings_to_stg` - -- выполняет `sql/stg/bookings_load.sql` в Greenplum; -- берёт строки из `stg.bookings_ext`, которые попадают в новое окно инкремента; -- вставляет их в `stg.bookings`, добавляя тех.колонки: - - `event_ts` (опорная метка времени для инкремента), - - `_load_ts`, - - `_load_id={{ run_id }}`. - -3) `check_row_counts` - -- выполняет `sql/stg/bookings_dq.sql` в Greenplum; -- считает количество строк в источнике за то же окно инкремента и сравнивает с количеством строк, - вставленных в `stg.bookings` для текущего `_load_id`; -- при расхождении делает `RAISE EXCEPTION` с понятным текстом. - -4) `load_tickets_to_stg` - -- выполняет `sql/stg/tickets_load.sql` в Greenplum; -- так как в `bookings.tickets` нет явной временной колонки, окно инкремента берётся по `book_date` - из связанной внешней таблицы `stg.bookings_ext` (JOIN по `book_ref`); -- вставляет строки в `stg.tickets`, добавляя `event_ts`, `_load_ts` и `_load_id={{ run_id }}`. - -5) `check_tickets_dq` - -- выполняет `sql/stg/tickets_dq.sql` в Greenplum; -- проверяет количество строк в том же окне инкремента, а также ссылочную целостность и обязательные поля; -- при проблемах делает `RAISE EXCEPTION`, чтобы DAG падал “красным”. - -6) Справочники (full load, параллельно где возможно) - -Справочники загружаются “снэпшотом” (все строки) и затем проверяются DQ-скриптом. -Порядок определяется зависимостями данных — **airports** и **airplanes** грузятся **параллельно**, -потому что не зависят друг от друга: - -``` -check_tickets_dq - ├─ load_airports → check_airports_dq ─┐ - │ ├─ load_routes → check_routes_dq - └─ load_airplanes → check_airplanes_dq ─┤ - └─ load_seats → check_seats_dq -``` - -- `load_airports_to_stg` → `check_airports_dq` (`sql/stg/airports_load.sql`, `sql/stg/airports_dq.sql`) -- `load_airplanes_to_stg` → `check_airplanes_dq` (`sql/stg/airplanes_load.sql`, `sql/stg/airplanes_dq.sql`) -- `load_routes_to_stg` → `check_routes_dq` (`sql/stg/routes_load.sql`, `sql/stg/routes_dq.sql`) — зависит от **airports** и **airplanes** (DQ проверяет ссылочную целостность) -- `load_seats_to_stg` → `check_seats_dq` (`sql/stg/seats_load.sql`, `sql/stg/seats_dq.sql`) — зависит от **airplanes** (DQ проверяет `airplane_code → airplanes`) - -7) Транзакции - -- `load_flights_to_stg` → `check_flights_dq` (инкремент по `scheduled_departure`) — зависит от **routes** -- `load_segments_to_stg` → `check_segments_dq` (инкремент по `book_date` через tickets/bookings) — зависит от **flights** -- `load_boarding_passes_to_stg` → `check_boarding_passes_dq` (инкремент по `book_date` через tickets/bookings) — зависит от **segments** - -Ветка `seats` работает параллельно с веткой `routes → flights → segments → boarding_passes`. -Обе ветки сходятся на `finish_summary`. - -Важно: для инкрементальных таблиц “пустое окно инкремента” допустимо — загрузка и DQ логируют `NOTICE` и завершаются успешно. -Для snapshot-справочников (airports/airplanes/routes/seats) пустой источник считается ошибкой (DQ делает `RAISE EXCEPTION`). - -8) `finish_summary` - -- ждёт завершения **обеих** параллельных веток (`check_boarding_passes_dq` и `check_seats_dq`); -- логирует краткую сводку в конце запуска. - -## Как проверить результат - -```bash -make gp-psql -``` - -Примеры запросов: +Откройте Greenplum командой `make gp-psql`: ```sql SELECT COUNT(*) FROM stg.bookings; -SELECT - event_ts, - _load_ts, - _load_id +SELECT book_ref, book_date, event_ts, _load_ts, _load_id FROM stg.bookings -ORDER BY event_ts DESC +ORDER BY _load_ts DESC, book_ref LIMIT 10; ``` -## Типичные ошибки +После первого успешного запуска `stg.bookings` непуста. Сопоставьте `_load_id` +с Run ID в Airflow и объясните, чем `book_date` отличается от `_load_ts`. +Для просмотра первоисточника используйте `make bookings-psql`. -- `database "demo" does not exist`: демо‑БД не установлена → выполните `make bookings-init`. -- Ошибки про `stg.*`/`stg.*_ext`: не применён DDL → запустите `bookings_stg_ddl` или `make ddl-gp`. -- Ошибки PXF (`protocol "pxf" does not exist`, connection refused): перезапустите `greenplum` и повторите DDL. - Для технических деталей PXF см. ветку `solution` (`docs/reference/pxf_bookings.md`). +Далее запускайте [ODS](bookings_to_gp_ods.md). +Общий порядок слоев находится в [порядке запуска DAG](dag_execution_order.md). -## Рекомендации по качеству решения +## Если загрузка не прошла -Ревью решения и список улучшений — в ветке `solution` (`docs/archive/`). +Откройте Log первой упавшей задачи в Airflow. Если упала DQ, начните с ключа +и батча, названных в сообщении, затем откройте ее SQL из карточки таблицы. + +- Нет базы `demo`: пройдите инициализацию из README. `make bookings-init` + пересоздает исходную базу из seed-дампа, поэтому не используйте его для + обычного повторного запуска уже заполненного стенда. +- Нет `stg.*` или `stg.*_ext`: выполните `bookings_stg_ddl`. +- Ошибка PXF или соединения: проверьте `docker compose ps`, логи `greenplum` + и `bookings-db`, затем настройки из [справочника стенда](stack.md). + Повторное применение DDL само по себе не исправляет недоступное подключение. diff --git a/docs/dag_execution_order.md b/docs/dag_execution_order.md index 575a8a5..8ec3890 100644 --- a/docs/dag_execution_order.md +++ b/docs/dag_execution_order.md @@ -1,35 +1,56 @@ -# Порядок запуска DAG (Cross-DAG Dependencies) +# Порядок запуска DAG -В этом стенде пайплайны разделены на несколько DAG-ов по слоям DWH (STG, ODS, DDS, DM). -Они настроены с `schedule=None`, так как это учебный проект. +Все загрузочные DAG в стенде запускаются вручную (`schedule=None`). +Завершение одного слоя не запускает следующий автоматически. +Подготовка Docker и источника находится в [быстром старте](../README.md#быстрый-старт-основной-сценарий-bookings--stg--ods--dds--dm). -Чтобы данные корректно прошли от источника до витрин, запускать DAG-и нужно в определённом порядке. +## Создание таблиц -## 1. DDL-скрипты (выполняются один раз) +Перед первой загрузкой выполните по порядку: -Для создания структуры таблиц в аналитических слоях: -1. Запустите `bookings_stg_ddl` — создаст STG-таблицы и внешние `*_ext` через PXF. -2. Запустите `bookings_ods_ddl` — создаст таблицы ODS (типизированные, SCD1). -3. Запустите `bookings_dds_ddl` — создаст таблицы для измерений и фактов в слое DDS. -4. Запустите `bookings_dm_ddl` — создаст таблицы витрин в слое DM. +1. `bookings_stg_ddl` создает STG и внешние таблицы `*_ext` для чтения через PXF. +2. `bookings_ods_ddl` создает типизированные таблицы ODS. +3. `bookings_dds_ddl` создает измерения и факт DDS. +4. `bookings_dm_ddl` создает витрины DM. -*(Технический шорткат: `make ddl-gp` применяет DDL для всех 4 слоёв сразу).* +Альтернатива четырем запускам: `make ddl-gp`. +DDL создает структуру; для наполнения нужны отдельные загрузки. +В заданиях таблицы и задачи уже подготовлены, поэтому перед каждым +повторным запуском загрузки применять DDL не требуется. -## 2. Ежедневная загрузка (ETL) +## Проход данных по слоям -Для прогрузки новой порции данных (или полного перерасчёта) соблюдайте следующую цепочку: +Запускайте следующий DAG после успешного завершения всех задач предыдущего. +Эта последовательность показывает зависимости загрузок; связи строк и зерно +объектов смотрите на [карте](design/architecture-map.html), открытой +[локально](design/db_schema.md#как-открыть-карту). -1. **`bookings_to_gp_stage`** - - Извлекает новые данные из демо-БД PostgreSQL и сохраняет их в `stg`-схему в Greenplum. - - Генерирует `stg_batch_id` для текущей загрузки. -2. **`bookings_to_gp_ods`** - - Берёт последний согласованный `stg_batch_id` из STG-слоя. - - Выполняет нормализацию и SCD1-UPSERT в слой ODS. -3. **`bookings_to_gp_dds`** - - Читает очищенные данные из ODS. - - Обновляет измерения (SCD1, SCD2) и инкрементально догружает новые рейсы в таблицу фактов `dds.fact_flight_sales`. -4. **`bookings_to_gp_dm`** - - Читает новые факты из DDS. - - Обновляет агрегированные витрины (использует HWM-инкрементальность по `_load_ts` или полный перерасчёт). +| Порядок | DAG и руководство | Что меняется в данных | +|---|---|---| +| 1 | [bookings_to_gp_stage](bookings_to_gp_stage.md) | Генератор добавляет учебный день в Bookings; STG получает инкременты транзакций и полные снимки справочников с `_load_id = run_id` STG | +| 2 | [bookings_to_gp_ods](bookings_to_gp_ods.md) | Resolver выбирает общий STG-батч справочников; их снимки заменяются целиком. Транзакционные загрузки читают дельту STG по `_load_ts` и обновляют строки по бизнес-ключу | +| 3 | [bookings_to_gp_dds](bookings_to_gp_dds.md) | Сначала выполняются загрузки измерений, затем факт: вставляются новые пары `(ticket_no, flight_id)`, у существующих обновляются цена, место и признак посадки | +| 4 | [bookings_to_gp_dm](bookings_to_gp_dm.md) | `sales_report` пересчитывает полные агрегаты дат, затронутых изменениями факта; поведение остальных витрин задано в ТЗ | -> **💡 Архитектурная заметка:** В реальном production-окружении (Airflow) эти связи между DAG-ами обычно настраиваются автоматически через `TriggerDagRunOperator`, `ExternalTaskSensor` или механизмы Data-Aware Scheduling (Datasets/Data Assets). В учебных целях мы оставили их ручными, чтобы вы могли проинспектировать каждый слой после его загрузки. +На исходной `main` готовы весь STG, семь таблиц ODS, календарь, аэропорты, +тарифы, факт и `sales_report`. Загрузки и DQ остальных объектов являются +[заданиями](assignment/README.md). Успех этих задач с `SELECT 1;` оставляет +таблицы пустыми. У факта до реализации измерений могут быть `NULL` в +`passenger_sk`, `route_sk` и `airplane_sk`. + +## Повторный запуск и проверка задания + +Чтобы получить следующий учебный день, повторите цепочку STG → ODS → DDS → DM. +Когда проверяете правку SQL своего слоя на уже загруженных источниках, +начните с этого слоя; новый STG запустит генератор еще раз. +Подробности выбора батча и повторного запуска находятся в руководстве слоя. + +После реализации измерений обычный повтор DDS не заполнит старые SK факта. +Выполните [пересчет по ТЗ](assignment/analyst_spec.md#пересчёт-факта-после-реализации-измерений), +затем обновите DM. + +Для самопроверки задания откройте +[«Запуск и проверка»](assignment/README.md#запуск-и-проверка). +`bookings_validate` запускайте отдельно от загрузок: дождитесь всего DAG +и успешного `validate_dds.scd2_restore` перед следующей записью в хранилище. +Тест SCD2 временно меняет данные и восстанавливает их из копии. diff --git a/docs/design/bookings_dds_design.md b/docs/design/bookings_dds_design.md index 04d25ed..af787c0 100644 --- a/docs/design/bookings_dds_design.md +++ b/docs/design/bookings_dds_design.md @@ -1,4 +1,20 @@ -# DDS Layer: план реализации Star Schema для bookings +# История проектирования DDS для Bookings + +> Архивный материал: исходный план создания слоя. Он сохраняет обоснования, +> варианты SQL и этапы разработки; приведенные фрагменты отражают состояние +> проекта во время проектирования. + +Для работы с текущей учебной веткой откройте +[руководство загрузки DDS](../bookings_to_gp_dds.md). +Смысл строк и связи показаны на [карте](architecture-map.html#node=dds.fact_flight_sales); +поля и правила самостоятельных загрузок определяет [ТЗ](../assignment/analyst_spec.md). +Готовые примеры доступны из [разбора SQL](reading_the_pipeline.md). + +Ниже сохранен план "DDS Layer: план реализации Star Schema для bookings". +Задачи по созданию файлов относятся к истории разработки, а примеры SQL +требуют сверки с действующими файлами перед использованием. + +--- ## Контекст diff --git a/docs/design/bookings_dm_design.md b/docs/design/bookings_dm_design.md index 4a224a3..04cb9ae 100644 --- a/docs/design/bookings_dm_design.md +++ b/docs/design/bookings_dm_design.md @@ -1,4 +1,20 @@ -# План: DM-слой (Data Mart) для учебного стенда Bookings +# История проектирования DM для Bookings + +> Архивный материал: исходный план создания слоя. Он сохраняет обоснования, +> варианты SQL и этапы разработки; приведенные фрагменты отражают состояние +> проекта во время проектирования. + +Для работы с текущей учебной веткой откройте +[руководство загрузки DM](../bookings_to_gp_dm.md). +Смысл строк и связи показаны на [карте](architecture-map.html#node=dm.sales_report); +поля и правила самостоятельных загрузок определяет [ТЗ](../assignment/analyst_spec.md). +Готовые примеры доступны из [разбора SQL](reading_the_pipeline.md). + +Ниже сохранен план "План: DM-слой (Data Mart) для учебного стенда Bookings". +Задачи по созданию файлов относятся к истории разработки, а примеры SQL +требуют сверки с действующими файлами перед использованием. + +--- ## Context diff --git a/docs/design/bookings_ods_design.md b/docs/design/bookings_ods_design.md index f77df92..e33cd6d 100644 --- a/docs/design/bookings_ods_design.md +++ b/docs/design/bookings_ods_design.md @@ -1,4 +1,20 @@ -# ODS Layer: эталонный учебный план реализации (v3) +# История проектирования ODS для Bookings + +> Архивный материал: исходный план создания слоя. Он сохраняет обоснования, +> варианты SQL и этапы разработки; приведенные фрагменты отражают состояние +> проекта во время проектирования. + +Для работы с текущей учебной веткой откройте +[руководство загрузки ODS](../bookings_to_gp_ods.md). +Смысл строк и связи показаны на [карте](architecture-map.html#node=ods.airports); +поля и правила самостоятельных загрузок определяет [ТЗ](../assignment/analyst_spec.md). +Готовые примеры доступны из [разбора SQL](reading_the_pipeline.md). + +Ниже сохранен план "ODS Layer: эталонный учебный план реализации (v3)". +Задачи по созданию файлов относятся к истории разработки, а примеры SQL +требуют сверки с действующими файлами перед использованием. + +--- ## Контекст diff --git a/docs/design/bookings_stg_design.md b/docs/design/bookings_stg_design.md index 237c95e..737ab1e 100644 --- a/docs/design/bookings_stg_design.md +++ b/docs/design/bookings_stg_design.md @@ -1,4 +1,20 @@ -# Дизайн STG для bookings в Greenplum +# История проектирования STG для Bookings + +> Архивный материал: исходный план создания слоя. Он сохраняет обоснования, +> варианты SQL и этапы разработки; приведенные фрагменты отражают состояние +> проекта во время проектирования. + +Для работы с текущей учебной веткой откройте +[руководство загрузки STG](../bookings_to_gp_stage.md). +Смысл строк и связи показаны на [карте](architecture-map.html#node=stg.bookings); +поля и правила самостоятельных загрузок определяет [ТЗ](../assignment/analyst_spec.md). +Готовые примеры доступны из [разбора SQL](reading_the_pipeline.md). + +Ниже сохранен план "Дизайн STG для bookings в Greenplum". +Задачи по созданию файлов относятся к истории разработки, а примеры SQL +требуют сверки с действующими файлами перед использованием. + +--- ## 1. Цель и общий контур diff --git a/docs/design/db_schema.md b/docs/design/db_schema.md index 737a716..92ae928 100644 --- a/docs/design/db_schema.md +++ b/docs/design/db_schema.md @@ -209,9 +209,9 @@ SQL использует `LEFT JOIN`, поэтому строка с таким о таблице возвращайтесь на карту: там находится каталог объектов, их источников, потребителей, SQL и DQ. -Для проверки результата пригодится [справочник DQ](../reference/dq_taxonomy.md). -Технические материалы по слоям остаются доступны: -[STG](bookings_stg_design.md), [ODS](bookings_ods_design.md), -[DDS](bookings_dds_design.md), [DM](bookings_dm_design.md). -В них есть планы разработки; текущее поведение проверяйте по SQL, -а требования к самостоятельной работе - по ТЗ. +Проверяйте задание по [инструкции самопроверки](../assignment/README.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). +[Индекс документации](../README.md) ведет к справочникам, проверкам разработки +и истории проектирования. diff --git a/docs/design/naming_conventions.md b/docs/design/naming_conventions.md index ecabbf9..aee8ffa 100644 --- a/docs/design/naming_conventions.md +++ b/docs/design/naming_conventions.md @@ -6,25 +6,24 @@ ## Зачем этот документ -Чтобы имена полей не «плыли» между слоями, DAG и SQL-скриптами: -- студенты видят один и тот же словарь во всех задачах; -- новые реализации (ODS/DDS/DM) не расходятся с тем, как уже учили на `dwh-modeling`; -- ревью становится проще: сразу видно, где отклонение от стандарта. +Этот справочник задает имена служебных полей. Состав колонок конкретной +таблицы проверяйте по ее DDL, а требования к учебной загрузке - по +[ТЗ](../assignment/analyst_spec.md). ## 1. Базовые правила - Имена колонок: `snake_case`, на английском. - Бизнес-ключи источника не переименовываем без необходимости (`book_ref`, `ticket_no`, `route_no`). - Булевы поля начинаются с `is_` (`is_outbound`, `is_boarded`). -- Денежные/количественные поля называем явно (`total_amount`, `segment_amount`, `range_km`). +- Денежные и количественные поля называем по смыслу (`total_amount`, `amount`, `range_km`). ## 2. Каноничные служебные поля | Поле | Тип (рекомендация) | Смысл | Где применять | |---|---|---|---| -| `_load_id` | `TEXT NOT NULL` | Идентификатор загрузки/батча | STG/ODS (новые объекты), при необходимости DDS | -| `_load_ts` | `TIMESTAMP NOT NULL` | Когда запись попала в слой | STG/ODS (новые объекты) | -| `event_ts` | `TIMESTAMP` | Когда событие произошло в источнике (effective time) | ODS/DDS при событийной природе данных | +| `_load_id` | `TEXT NOT NULL` | Идентификатор запуска или исходного STG-батча; зависит от слоя | STG, ODS, DDS и DM, кроме календаря | +| `_load_ts` | `TIMESTAMP NOT NULL` | Метка загрузки; в транзакционных ODS сохраняется время STG | STG, ODS, DDS и DM, кроме календаря | +| `event_ts` | `TIMESTAMP` | Опорное время источника или получения снимка | Все таблицы STG и транзакционные ODS | | `created_at` | `TIMESTAMP NOT NULL` | Когда строка создана в таблице слоя | DDS/DM, где есть lifecycle строки | | `updated_at` | `TIMESTAMP NOT NULL` | Когда строка обновлена в таблице слоя | DDS/DM, где есть UPDATE | | `valid_from` | `DATE NOT NULL` (базовый трек) | Начало действия версии SCD2 | DDS SCD2 | @@ -35,26 +34,42 @@ - Бизнес-ключ измерения: суффикс `_bk` (`customer_bk`, `airport_bk`). - Суррогатный ключ измерения: суффикс `_sk` (`customer_sk`, `airport_sk`). +- В существующих таблицах есть бизнес-ключи без `_bk`: `passenger_id` + у пассажиров, `fare_conditions` у тарифов, `date_actual` у календаря. + Используйте имена из DDL и ТЗ; переименовывать их в задании не нужно. ## 4. Правило времени (важно для обучения) -- `event_ts` (effective time) и `_load_ts` (load time) — разные сущности, не смешиваем. -- Если `event_ts` отсутствует в источнике, используем `_load_ts` как fallback и явно документируем это в SQL/доке. -- Для snapshot-справочников (airports, airplanes, routes, seats) в STG нет бизнес-события с точным временем: `event_ts` заполняется через `now()` при загрузке. Это намеренно и задокументировано в каждом load-скрипте. +- В STG `event_ts` задает опору инкремента: `book_date` у бронирований, + билетов, сегментов и посадочных талонов; `scheduled_departure` у рейсов. + Например, у талона это дата бронирования, а не `boarding_time`. +- У снимков `airports`, `airplanes`, `routes`, `seats` в STG + `event_ts = now()`: точного времени изменения записи в источнике здесь нет. +- `_load_ts` относится к загрузке. Какой именно слой ее поставил, указано ниже. + Правила отбора строк описаны в руководствах [STG](../bookings_to_gp_stage.md) + и [ODS](../bookings_to_gp_ods.md). ## 5. Применение по слоям ### STG -- Используем канон `_load_id`, `_load_ts`, `event_ts` для всех таблиц. +- Все таблицы содержат `_load_id = run_id` запуска STG, `_load_ts = now()` + и `event_ts` по правилам выше. ### ODS -- Используем канон `_load_id`, `_load_ts`, `event_ts`. -- Базовый эталон ODS в этом стенде: SCD Type 1 (current state + UPSERT). +- Снимки `airports`, `airplanes`, `routes`, `seats`: `_load_id` выбранного + STG-батча, `_load_ts = now()`, без `event_ts`. Загрузка заменяет снимок целиком. +- Транзакционные таблицы: `_load_id`, `_load_ts` и `event_ts` переносятся из STG. + Сохраненная `_load_ts` служит границей инкремента для `UPDATE + INSERT`. -### DDS +### DDS и DM +- `_load_id` измененной или вставленной строки относится к текущему запуску + своего слоя, `_load_ts` фиксирует время записи. +- Календарь статический: у `dim_calendar` нет меток загрузки. +- `created_at` и `updated_at` используем там, где они предусмотрены DDL. + Например, у факта и полностью пересчитываемой `route_performance` их нет. - Для SCD2 используем: - `valid_from`, `valid_to`, `hashdiff`, `created_at`, `updated_at`. - Интервалы считаем как `[valid_from, valid_to)`, current-версия: `valid_to IS NULL`. diff --git a/docs/reference/dq_taxonomy.md b/docs/reference/dq_taxonomy.md index c2a2e60..262ccfa 100644 --- a/docs/reference/dq_taxonomy.md +++ b/docs/reference/dq_taxonomy.md @@ -2,6 +2,11 @@ DQ-проверка должна отвечать на конкретный вопрос. В этом проекте есть три класса. Один `*_dq.sql` может содержать проверки нескольких классов. +Сверяйте их с полями и правилами своего объекта в [ТЗ](../assignment/analyst_spec.md). +Как запустить DQ и проверить задание, описано в +[инструкции самопроверки](../assignment/README.md#запуск-и-проверка). +На `main` учебные DQ содержат `SELECT 1;` и начнут проверять данные +после вашей реализации. ## 1. Универсальные структурные проверки diff --git a/docs/reference/qa-plan.md b/docs/reference/qa-plan.md index 84a3ee2..4b33365 100644 --- a/docs/reference/qa-plan.md +++ b/docs/reference/qa-plan.md @@ -1,20 +1,19 @@ -# План отладки эталонного пайплайна +# История QA: план отладки полной цепочки -> Цель: убедиться, что эталонный вертикальный срез (STG→ODS→DDS→DM) работает -> корректно при разных сценариях. Найти и исправить баги ДО того, как начнём -> готовить ветку main для студентов. -> -> Аудитория документа: AI-агент (Sonnet) или человек, выполняющий отладку. -> -> **Ветка `main`:** на main часть загрузок — заглушки (`SELECT 1;`). -> Пустые таблицы на main: `ods.airplanes`, `ods.seats`, -> `dds.dim_airplanes`, `dds.dim_passengers`, `dds.dim_routes`, -> `dm.route_performance`, `dm.passenger_loyalty`, `dm.airport_traffic`, `dm.monthly_overview`. -> Ожидания ниже написаны для ветки `solution` (полная реализация). -> -> Зависимость: перед запуском этого плана нужно починить bookings-db -> (см. `docs/reference/bookings_db_issues.md`). -> Классы проверок для разбора ошибок: [`dq_taxonomy.md`](dq_taxonomy.md). +Этот план сохраняет сценарии первоначальной проверки полной реализации: +повторную загрузку, несколько учебных дней и граничные случаи. +Он предназначен для подготовки углубленной диагностики на отдельном тестовом +стенде. Ожидания и SQL ниже нужно сверять с текущей веткой; учебная `main` +содержит заглушки и не обязана проходить проверки полной реализации. + +Для своего задания используйте [самопроверку](../assignment/README.md#запуск-и-проверка). +Действующие команды разработки и границы автоматических проверок описаны +в [TESTING.md](../../TESTING.md), устройство DQ - в [справочнике](dq_taxonomy.md). + +Сценарии ниже очищают DWH и изменяют данные. `make bookings-init` +пересоздает базу Bookings, а `make dwh-truncate` удаляет строки хранилища. +Выполняйте их только на стенде, где данные можно потерять. +Параметры доступа к Airflow берите из `.env`. ---