Merge pull request 'docs: согласованы руководства загрузок и учебный маршрут' (#7) from docs/4-loading-guides into main
This commit was merged in pull request #7.
This commit is contained in:
commit
351dbbb3c1
17 files changed
+699
-755
No files matched your search
@@ -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).
|
||||
|
||||
## Типичные проблемы и решения
|
||||
|
||||
| Проблема | Решение |
|
||||
|
||||
+104
-66
@@ -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 или интерфейс не проверялись, укажите это прямо.
|
||||
+29
-24
@@ -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`.
|
||||
@@ -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`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+109
-161
@@ -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;` этих свойств не проверяет.
|
||||
+86
-154
@@ -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 и исправьте причину до следующего запуска слоя.
|
||||
+102
-143
@@ -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": "<Run ID успешного запуска STG>"}
|
||||
```
|
||||
|
||||
Если конфиг не передан, 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.
|
||||
+79
-127
@@ -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 само по себе не исправляет недоступное подключение.
|
||||
+47
-26
@@ -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 временно меняет данные и восстанавливает их из копии.
|
||||
@@ -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
|
||||
требуют сверки с действующими файлами перед использованием.
|
||||
|
||||
---
|
||||
|
||||
## Контекст
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
требуют сверки с действующими файлами перед использованием.
|
||||
|
||||
---
|
||||
|
||||
## Контекст
|
||||
|
||||
|
||||
@@ -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. Цель и общий контур
|
||||
|
||||
|
||||
@@ -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) ведет к справочникам, проверкам разработки
|
||||
и истории проектирования.
|
||||
@@ -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`.
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
DQ-проверка должна отвечать на конкретный вопрос. В этом проекте есть три
|
||||
класса. Один `*_dq.sql` может содержать проверки нескольких классов.
|
||||
Сверяйте их с полями и правилами своего объекта в [ТЗ](../assignment/analyst_spec.md).
|
||||
Как запустить DQ и проверить задание, описано в
|
||||
[инструкции самопроверки](../assignment/README.md#запуск-и-проверка).
|
||||
На `main` учебные DQ содержат `SELECT 1;` и начнут проверять данные
|
||||
после вашей реализации.
|
||||
|
||||
## 1. Универсальные структурные проверки
|
||||
|
||||
|
||||
+15
-16
@@ -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`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in new issue
Block a user