refactor(all): унифицированы служебные поля STG — переход на канонический нейминг
- Зачем:
- STG-слой использовал legacy-имена (batch_id, load_dttm, src_created_at_ts),
тогда как ODS/DDS/DM уже работали с каноном (_load_id, _load_ts, event_ts).
Студент видел разные имена для одного понятия — это убрано.
- Что:
- переименованы колонки в 9 STG DDL: batch_id→_load_id, load_dttm→_load_ts,
src_created_at_ts→event_ts; добавлен NOT NULL для _load_id во всех таблицах.
- обновлены 9 STG Load, 9 STG DQ, 9 ODS Load, 9 ODS DQ (INSERT/SELECT/WHERE).
- обновлены DAG-файлы bookings_to_gp_stage.py и bookings_to_gp_ods.py
(встроенный SQL резолвера, комментарии; Python-идентификаторы не тронуты).
- обновлены тесты и ~15 документов (naming_conventions, PRD, db_schema,
design-docs, qa-plan, README, TESTING и др.).
- Проверка:
- grep -rn 'load_dttm\|src_created_at_ts' sql/ airflow/ tests/ — 0 совпадений.
- make test — 4 passed.
- e2e-etl: day1 прошёл полностью, day2 стартовал без ошибок.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
STG-слой уже реализован как учебный эталон:
|
||||
- данные из `bookings-db` читаются через PXF;
|
||||
- в STG бизнес-колонки хранятся как `TEXT`;
|
||||
- загрузка и DQ работают батчами (`batch_id = {{ run_id }}`).
|
||||
- загрузка и DQ работают батчами (`_load_id = {{ run_id }}`).
|
||||
|
||||
Этот документ фиксирует **простую и каноничную** реализацию ODS для менти.
|
||||
|
||||
@@ -36,7 +36,7 @@ ODS в учебном проекте — это:
|
||||
|
||||
### 1.3. Где хранится история изменений
|
||||
|
||||
- История «как приходили данные» уже сохраняется в STG (append + `batch_id`).
|
||||
- История «как приходили данные» уже сохраняется в STG (append + `_load_id`).
|
||||
- Историзацию измерений (SCD2) показываем позже в DDS (как в учебной статье `dwh-modeling`).
|
||||
|
||||
Итог: **ODS = текущий слой (current state), простой и понятный**.
|
||||
@@ -55,9 +55,9 @@ ODS в учебном проекте — это:
|
||||
|
||||
### 2.1. Маппинг из текущего STG
|
||||
|
||||
- `stg.batch_id` -> `ods._load_id`
|
||||
- `stg.load_dttm` не переносим 1:1; в ODS пишем собственный `ods._load_ts = now()`
|
||||
- `stg.src_created_at_ts` -> `ods.event_ts` (для транзакционных таблиц)
|
||||
- `stg._load_id` -> `ods._load_id`
|
||||
- `stg._load_ts` не переносим 1:1; в ODS пишем собственный `ods._load_ts = now()`
|
||||
- `stg.event_ts` -> `ods.event_ts` (для транзакционных таблиц)
|
||||
|
||||
### 2.2. Почему так
|
||||
|
||||
@@ -239,8 +239,8 @@ DISTRIBUTED BY (ticket_no)
|
||||
| `flights` | `flight_id` | `flight_id` | `INTEGER` | Только cast TEXT → INT |
|
||||
| `segments` | `flight_id` | `flight_id` | `INTEGER` | Только cast TEXT → INT |
|
||||
| `boarding_passes` | `flight_id` | `flight_id` | `INTEGER` | Только cast TEXT → INT |
|
||||
| все транзакционные | `src_created_at_ts` | `event_ts` | `TIMESTAMP` | Маппинг legacy → канон |
|
||||
| все | `batch_id` | `_load_id` | `TEXT` | Маппинг legacy → канон |
|
||||
| все транзакционные | `event_ts` | `event_ts` | `TIMESTAMP` | Прямой перенос (канон) |
|
||||
| все | `_load_id` | `_load_id` | `TEXT` | Прямой перенос (канон) |
|
||||
|
||||
Пример каста с переименованием в SQL (в CTE):
|
||||
```sql
|
||||
@@ -282,32 +282,32 @@ def _resolve_stg_batch_id(**context):
|
||||
stg_batch_id = conf.get("stg_batch_id")
|
||||
|
||||
if not stg_batch_id:
|
||||
# Берём batch_id, который присутствует во всех snapshot-таблицах STG:
|
||||
# Берём _load_id, который присутствует во всех snapshot-таблицах STG:
|
||||
# airports, airplanes, routes, seats. Это защищает от частично успешных запусков.
|
||||
hook = PostgresHook(postgres_conn_id=GREENPLUM_CONN_ID)
|
||||
result = hook.get_first(
|
||||
'''
|
||||
WITH candidate_batches AS (
|
||||
SELECT batch_id FROM stg.airports WHERE batch_id IS NOT NULL GROUP BY batch_id
|
||||
SELECT _load_id FROM stg.airports WHERE _load_id IS NOT NULL GROUP BY _load_id
|
||||
INTERSECT
|
||||
SELECT batch_id FROM stg.airplanes WHERE batch_id IS NOT NULL GROUP BY batch_id
|
||||
SELECT _load_id FROM stg.airplanes WHERE _load_id IS NOT NULL GROUP BY _load_id
|
||||
INTERSECT
|
||||
SELECT batch_id FROM stg.routes WHERE batch_id IS NOT NULL GROUP BY batch_id
|
||||
SELECT _load_id FROM stg.routes WHERE _load_id IS NOT NULL GROUP BY _load_id
|
||||
INTERSECT
|
||||
SELECT batch_id FROM stg.seats WHERE batch_id IS NOT NULL GROUP BY batch_id
|
||||
SELECT _load_id FROM stg.seats WHERE _load_id IS NOT NULL GROUP BY _load_id
|
||||
),
|
||||
batch_ready AS (
|
||||
SELECT
|
||||
c.batch_id,
|
||||
c._load_id,
|
||||
GREATEST(
|
||||
(SELECT MAX(load_dttm) FROM stg.airports a WHERE a.batch_id = c.batch_id),
|
||||
(SELECT MAX(load_dttm) FROM stg.airplanes a WHERE a.batch_id = c.batch_id),
|
||||
(SELECT MAX(load_dttm) FROM stg.routes r WHERE r.batch_id = c.batch_id),
|
||||
(SELECT MAX(load_dttm) FROM stg.seats s WHERE s.batch_id = c.batch_id)
|
||||
(SELECT MAX(_load_ts) FROM stg.airports a WHERE a._load_id = c._load_id),
|
||||
(SELECT MAX(_load_ts) FROM stg.airplanes a WHERE a._load_id = c._load_id),
|
||||
(SELECT MAX(_load_ts) FROM stg.routes r WHERE r._load_id = c._load_id),
|
||||
(SELECT MAX(_load_ts) FROM stg.seats s WHERE s._load_id = c._load_id)
|
||||
) AS ready_dttm
|
||||
FROM candidate_batches c
|
||||
)
|
||||
SELECT batch_id
|
||||
SELECT _load_id
|
||||
FROM batch_ready
|
||||
ORDER BY ready_dttm DESC
|
||||
LIMIT 1
|
||||
@@ -334,7 +334,7 @@ resolve_batch = PythonOperator(
|
||||
Во всех `sql/ods/*_load.sql` и `sql/ods/*_dq.sql` значение `stg_batch_id` подставляется через Jinja-шаблон:
|
||||
|
||||
```sql
|
||||
WHERE batch_id = '{{ ti.xcom_pull(task_ids="resolve_stg_batch_id") }}'::text
|
||||
WHERE _load_id = '{{ ti.xcom_pull(task_ids="resolve_stg_batch_id") }}'::text
|
||||
```
|
||||
|
||||
Для читаемости в DAG можно вынести шаблон в константу:
|
||||
@@ -370,7 +370,7 @@ WITH src AS (
|
||||
coordinates,
|
||||
timezone
|
||||
FROM stg.airports
|
||||
WHERE batch_id = '{{ ti.xcom_pull(task_ids="resolve_stg_batch_id") }}'::text
|
||||
WHERE _load_id = '{{ ti.xcom_pull(task_ids="resolve_stg_batch_id") }}'::text
|
||||
)
|
||||
UPDATE ods.airports AS o
|
||||
SET airport_name = s.airport_name,
|
||||
@@ -403,7 +403,7 @@ WITH src AS (
|
||||
coordinates,
|
||||
timezone
|
||||
FROM stg.airports
|
||||
WHERE batch_id = '{{ ti.xcom_pull(task_ids="resolve_stg_batch_id") }}'::text
|
||||
WHERE _load_id = '{{ ti.xcom_pull(task_ids="resolve_stg_batch_id") }}'::text
|
||||
)
|
||||
INSERT INTO ods.airports (
|
||||
airport_code, airport_name, city, country, coordinates, timezone,
|
||||
@@ -423,7 +423,7 @@ WHERE NOT EXISTS (
|
||||
WITH src_keys AS (
|
||||
SELECT DISTINCT airport_code
|
||||
FROM stg.airports
|
||||
WHERE batch_id = '{{ ti.xcom_pull(task_ids="resolve_stg_batch_id") }}'::text
|
||||
WHERE _load_id = '{{ ti.xcom_pull(task_ids="resolve_stg_batch_id") }}'::text
|
||||
)
|
||||
DELETE FROM ods.airports o
|
||||
WHERE NOT EXISTS (
|
||||
@@ -447,13 +447,13 @@ WITH src AS (
|
||||
book_ref,
|
||||
book_date::TIMESTAMP WITH TIME ZONE AS book_date,
|
||||
total_amount::NUMERIC(10,2) AS total_amount,
|
||||
src_created_at_ts AS event_ts,
|
||||
event_ts,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY book_ref
|
||||
ORDER BY src_created_at_ts DESC NULLS LAST, load_dttm DESC
|
||||
ORDER BY event_ts DESC NULLS LAST, _load_ts DESC
|
||||
) AS rn
|
||||
FROM stg.bookings
|
||||
WHERE batch_id = '{{ ti.xcom_pull(task_ids="resolve_stg_batch_id") }}'::text
|
||||
WHERE _load_id = '{{ ti.xcom_pull(task_ids="resolve_stg_batch_id") }}'::text
|
||||
)
|
||||
UPDATE ods.bookings AS o
|
||||
SET book_date = s.book_date,
|
||||
@@ -475,13 +475,13 @@ WITH src AS (
|
||||
book_ref,
|
||||
book_date::TIMESTAMP WITH TIME ZONE AS book_date,
|
||||
total_amount::NUMERIC(10,2) AS total_amount,
|
||||
src_created_at_ts AS event_ts,
|
||||
event_ts,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY book_ref
|
||||
ORDER BY src_created_at_ts DESC NULLS LAST, load_dttm DESC
|
||||
ORDER BY event_ts DESC NULLS LAST, _load_ts DESC
|
||||
) AS rn
|
||||
FROM stg.bookings
|
||||
WHERE batch_id = '{{ ti.xcom_pull(task_ids="resolve_stg_batch_id") }}'::text
|
||||
WHERE _load_id = '{{ ti.xcom_pull(task_ids="resolve_stg_batch_id") }}'::text
|
||||
)
|
||||
INSERT INTO ods.bookings (
|
||||
book_ref, book_date, total_amount, event_ts,
|
||||
@@ -547,7 +547,7 @@ SELECT COUNT(*)
|
||||
FROM (
|
||||
SELECT DISTINCT book_ref
|
||||
FROM stg.bookings
|
||||
WHERE batch_id = '{{ ti.xcom_pull(task_ids="resolve_stg_batch_id") }}'::text
|
||||
WHERE _load_id = '{{ ti.xcom_pull(task_ids="resolve_stg_batch_id") }}'::text
|
||||
) s
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
@@ -694,7 +694,7 @@ SELECT COUNT(*)
|
||||
FROM (
|
||||
SELECT DISTINCT book_ref
|
||||
FROM stg.bookings
|
||||
WHERE batch_id = '<stg_batch_id>'
|
||||
WHERE _load_id = '<stg_batch_id>'
|
||||
) s
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
|
||||
Reference in New Issue
Block a user