docs: согласованы руководства загрузок и учебный маршрут
Руководства STG/ODS/DDS/DM сверены с действующими SQL и DAG, ожидания учебных заглушек связаны с ТЗ. Самопроверка заданий отделена от проверок разработки и исторических планов. Для dim_routes закреплено обновление описательных полей только у открытых версий. SQL, DAG, DDL и настройки стенда не менялись. Проверки: make lint; make test (4 passed, 14 skipped); карта; 292 локальные ссылки; маршрут чтения и карта в Chromium. Живой ETL не запускался.
This commit is contained in:
1 parent
a6fdd43e15
commit
35144e49f6
17 files changed
+699
-755
No files matched your search
+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.
|
||||
Reference in new issue
Block a user