docs(course): добавлен урок 2 (STG→ODS, типизация и DQ-split)

- Зачем:
  - нужен учебный урок «руки» про типизацию слоя ODS и разделение
    чистых/битых записей; по пути убрать мусор и неочевидности в
    эталонном пути, чтобы он читался за один проход.
- Что:
  - добавлен lessons/02_stg_to_ods.md по LESSON_STANDARD (6 секций,
    режим «руки», эталон голоса — урок 1); регистр смягчён под уровень
    «обзорно» с расшифровкой терминов (click-контекст, WITH, двойной учёт).
  - 20_stg_to_ods.sql: поток данных в шапку + блок «DQ-split» (почему
    строка может попасть и в основную таблицу, и в *_errors).
  - 20_ods.sql: убран мусорный блок из 8 DROP TABLE mv_*_to_ods;
    пояснено разное партиционирование (browser — по бизнес-дате,
    click-контекст — по дате загрузки).
- Проверка:
  - make ddl && make transform — проходят чисто, counts не изменились
    (browser/location 50, device/geo 26 дедуп, *_errors 0).
  - правка §4 (toFloat64OrNull→toInt64OrNull для geo_latitude) на стенде
    даёт geo_by_click_errors 0→50 и NULL-широту с флагом bad_geo_latitude.
This commit is contained in:
2026-06-05 17:08:29 +03:00
parent b15ee90939
commit 256ac14c66
3 changed files with 375 additions and 13 deletions
+4 -13
View File
@@ -9,21 +9,9 @@
--
-- Важно:
-- - Наполнение ODS выполняется batch-процессом из sql/ods/20_stg_to_ods.sql
-- - Materialized View STG → ODS в target-state не используются
-- (а не Materialized View, как в STG): батч ради наблюдаемости пересчёта
-- ============================================================================
-- ----------------------------------------------------------------------------
-- Удаляем legacy MV STG → ODS (если ранее были созданы)
-- ----------------------------------------------------------------------------
DROP TABLE IF EXISTS stg.mv_browser_raw_to_ods;
DROP TABLE IF EXISTS stg.mv_browser_raw_to_ods_errors;
DROP TABLE IF EXISTS stg.mv_location_raw_to_ods;
DROP TABLE IF EXISTS stg.mv_location_raw_to_ods_errors;
DROP TABLE IF EXISTS stg.mv_device_raw_to_ods;
DROP TABLE IF EXISTS stg.mv_device_raw_to_ods_errors;
DROP TABLE IF EXISTS stg.mv_geo_raw_to_ods;
DROP TABLE IF EXISTS stg.mv_geo_raw_to_ods_errors;
-- ============================================================================
-- BROWSER EVENTS
-- ============================================================================
@@ -46,6 +34,8 @@ CREATE TABLE IF NOT EXISTS ods.browser_event
parse_errors Array(LowCardinality(String)) -- Ошибки парсинга (если есть)
)
ENGINE = ReplacingMergeTree(src_ingest_ts)
-- Партиционируем по бизнес-дате события: у browser_event есть собственное время (event_ts).
-- У click-контекста ниже (location/device/geo) такого времени нет — там партиция по дате загрузки.
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_id)
SETTINGS allow_nullable_key = 1;
@@ -87,6 +77,7 @@ CREATE TABLE IF NOT EXISTS ods.location_event
parse_errors Array(LowCardinality(String))
)
ENGINE = ReplacingMergeTree(src_ingest_ts)
-- По дате загрузки: у location нет собственного времени события (только время приёма в STG)
PARTITION BY toYYYYMM(toDate(src_ingest_ts))
ORDER BY (event_id)
SETTINGS allow_nullable_key = 1;
+9
View File
@@ -1,11 +1,20 @@
-- ============================================================================
-- Batch-трансформация: STG → ODS
-- ============================================================================
-- Поток данных:
-- stg.*_raw → ods.* (валидный ключ) + ods.*_errors (любая ошибка)
--
-- Назначение:
-- - Перенос типизации STG → ODS из Materialized View в управляемый batch
-- - Полная пересборка ODS для прозрачного мониторинга в Airflow
-- - Сохранение DQ-логики: parse_errors + отдельные *_errors таблицы
--
-- DQ-split (почему одна строка может попасть в оба места):
-- - Основная таблица ods.* — строки с валидным КЛЮЧОМ (event_id / click_id).
-- В ней допустимы parse_errors по НЕключевым полям — строка остаётся, но помечена.
-- - Таблица ods.*_errors — копия строк с ЛЮБОЙ ошибкой парсинга (для разбора).
-- Поэтому строка с валидным ключом, но битым неключевым полем, попадёт И туда, И туда.
--
-- Когда запускать:
-- - В DAG etl_pipeline перед ODS → DDS
-- - После загрузки очередного среза данных в Kafka/STG