# План обучения: курс «Кликстрим на ClickHouse»

> Дата: 2026-06-03 (аудит путей выполнен; середина расщеплена —
> один паттерн на урок, см. §1–2).
> Поправка 2026-07-23: весь маршрут стал обязательным. После урока 6 добавлены
> лабы 07–08 про следующий день и живое продолжение; обе лабы написаны.
> Назначение: высокоуровневый маршрут менти по курсу — карта уроков, порядок,
> результаты аудита эталонных путей. Рамка курса (зачем/что/скоуп) — в `PRD.md`;
> как устроен отдельный урок — в `LESSON_STANDARD.md`.
>
> Раздел 3 содержит финальные вердикты аудита и конкретный список правок —
> их применяем при написании соответствующих уроков, а не отдельным забегом.

---

## 1. Маршрут

Порядок линейный, все уроки и лабы обязательны. Урок 0 — разминка, а уроки 1→4
повторяют сам пайплайн (STG → ODS → DDS → оркестрация). Мониторинг (5) и
Superset (6) работают как надстройки поверх готовых данных. Лабы 07–08 затем
растят импортированный мир: сначала детерминированным следующим днём, потом
живым продолжением.

Принцип нарезки — **один прод-паттерн на урок** (`LESSON_STANDARD` §2). Поэтому
середина пайплайна разнесена: типизация+DQ (ODS) и сборка сущностей (DDS) — разные
уроки. Витрины DM **не отдельный урок**: их показываем в деле там, где их потребляют —
в мониторинге (DQ-витрины) и BI (traffic/utm-витрины).

**Что нужно знать заранее по Kafka:** перед уроком 0 менти смотрит обзорное видео по
Kafka из роадмапа — оно даёт словарь терминов. Урок 0 связывает этот словарь с тем,
что менти сразу видит в живом стенде.

## 2. Карта уроков

| # | Урок | Эталонный путь (файлы стенда) | Статус | Режим |
|---|------|-------------------------------|--------|-------|
| 0 | Вводный урок по Kafka | Kafka UI: топики, партиции, offset'ы, consumer-группы, отставание (lag) | обязательный | наблюдение |
| 1 | Заземление Kafka → ClickHouse | `sql/ddl/stg/10_stg.sql` (Kafka engine → MV → MergeTree) | обязательный | руки |
| 2 | STG → ODS: типизация и DQ-split | `sql/ods/20_stg_to_ods.sql` + DDL `sql/ddl/ods/20_ods.sql` | обязательный | руки |
| 3 | ODS → DDS: сборка сущностей | `sql/dds/30_ods_to_dds.sql` + DDL `sql/ddl/dds/30_dds.sql` (argMax, UNION, сироты) | обязательный | руки |
| 4 | Оркестрация в Airflow | `airflow/dags/etl_pipeline_dag.py` (зависимости, гейты, остановка при нарушениях) | обязательный | руки |
| 5 | Мониторинг | Prometheus + Grafana + экспортёры | обязательный | наблюдение |
| 6 | BI-витрина | Superset поверх ClickHouse | обязательный | руки |
| 7 | Лаба: следующий день | `airflow/dags/world_next_day_dag.py` | написана, обязательная | руки |
| 8 | Лаба: живое продолжение | `make generator-continue` и `etl_pipeline` | написана, обязательная | руки |

Урок 0 — обязательная разминка без правок кода. В уроках 1–6 и лабах 07–08
менти запускает стенд или делает управляемую правку; каждый такой эксперимент
заканчивается явным возвратом к чистому состоянию.

**Где «MV vs батч»:** контраст из цели №2 PRD — это мост уроков 1→2. Урок 1 (STG)
заканчивается вопросом «мы приземлили поток через MV — почему дальше не MV?»; урок 2
(ODS) отвечает: батч ради наблюдаемости и управляемости пересчёта. Отдельным уроком
контраст не выделяем — он живёт на стыке.

**Витрины DM** показываем в деле в уроках 5–6; единственную концепцию DM («VIEW
сейчас, материализуем если затормозит») даём короткой заметкой в финале урока 3.

## 3. Аудит эталонных путей

Вердикт по каждому пути — один из трёх: **годно как есть / точечно править /
переписать** (критерии «учебного качества» — в `LESSON_STANDARD.md`).

Финальная оценка (детальный аудит от 2026-06-03):

| Урок | Путь | Файлы | Вердикт |
|------|------|-------|---------|
| 1 | STG (Kafka→CH) | `sql/ddl/stg/10_stg.sql` | **точечно править** (баг конвертации `kafka_ts`, найден на стенде) |
| 2 | STG→ODS | `sql/ods/20_stg_to_ods.sql` + DDL `sql/ddl/ods/20_ods.sql` | **точечно править** |
| 3 | ODS→DDS (+ демоут DM) | `sql/dds/30_ods_to_dds.sql` + DDL `sql/ddl/dds/30_dds.sql`; DM `sql/dm/40_dds_to_dm.sql` + `sql/ddl/dm/40_dm.sql` | **точечно править** |
| 4 | Airflow DAG | `airflow/dags/etl_pipeline_dag.py` | **точечно править** |
| 5 | Мониторинг | `configs/*`, экспортёры | **годно как есть** (для режима наблюдения) |
| 7 | Следующий день | `airflow/dags/world_next_day_dag.py` | **точечно править** |

> Карты путей раздела 2 включают и DDL целевых таблиц (`sql/ddl/{ods,dds}/*`), а не
> только батч-трансформации: без формы целевых таблиц слой читается неполно.

### 3.1. Список правок (применяем при написании урока)

Правки точечные, делаем не отдельным забегом, а в составе соответствующего урока.

**Урок 1 — STG (`sql/ddl/stg/10_stg.sql`):**
- [x] **Баг конвертации `kafka_ts` (найден прогоном на стенде 2026-06-03).**
      Было `fromUnixTimestamp64Milli(toInt64(_timestamp_ms))` во всех 4 MV: `_timestamp_ms`
      это `DateTime64(3)`, `toInt64()` срезает его до **секунд**, и `fromUnixTimestamp64Milli`
      читает секунды как миллисекунды → `kafka_ts` = `1970-01-21` по всему стенду.
      Исправлено на `_timestamp_ms AS kafka_ts` (прямое присвоение, мс сохраняются).
      Прежний аудит ошибочно пометил путь «годно как есть» — потому что его не прогоняли.
- Колоночные комментарии-пересказы (`-- Партиция`, `-- Имя топика`) безвредны — не трогаем.
- В финале урока — мост к уроку 2: вопрос «почему дальше не MV?».

**Урок 2 — STG→ODS, типизация + DQ-split (`sql/ods/20_stg_to_ods.sql` + DDL `sql/ddl/ods/20_ods.sql`):**
- [x] **Кандидат на war-story по типам:** баг `kafka_ts` из урока 1 (`toInt64(DateTime64)`
      молча срезал миллисекунды → 1970). Это идеальная иллюстрация темы урока — «тихая
      потеря данных на неверном типе». Решено: дан как пример в уроке 2 (war-story в финале).
- [x] Добавить однострочный «поток данных» в шапку `20_stg_to_ods.sql`
      (`stg.*_raw → ods.* + ods.*_errors`) — п.3 чеклиста.
- [x] Добавить строку «зачем»: основная таблица = валидный ключ (но может иметь
      `parse_errors` по неключевым полям), а `*_errors` = любая ошибка. Без этого
      непонятно, почему одна строка попадает в оба места (двойной учёт) — проваливается
      тест одного прохода.
- [x] Убрать мусор: в `sql/ddl/ods/20_ods.sql` (стр. ~15–25) — восемь
      `DROP TABLE IF EXISTS stg.mv_*_to_ods` (чистка legacy-MV). Шум прошлой
      архитектуры, мента читает его раньше сути. Вынести из учебного файла.
- [x] Минор: в `20_ods.sql` разнобой партиционирования (`browser` по `event_date`,
      остальные по `src_ingest_ts`) — одна строка «почему» либо унифицировать.
- В «Зачем» урока — ответ на мост из урока 1: батч ради наблюдаемости/управляемости.

**Урок 3 — ODS→DDS, сборка сущностей (`sql/dds/30_ods_to_dds.sql` + DDL `sql/ddl/dds/30_dds.sql`):**
- [x] Добавить однострочный «поток данных» в шапку `30_ods_to_dds.sql`
      (`ods.* → dds.click + dds.event`) — п.3 чеклиста.
- [x] В финале урока — **recap всей цепочки** STG→ODS→DDS: защита от фрагментации после
      расщепления середины (менти должен собрать сквозную модель, а не три изолированных слоя).
- [x] **Демоут DM** оформляем здесь: убрать мусор в `sql/dm/40_dds_to_dm.sql`
      (стр. ~14–37, закомментированный «пример материализации витрины») и превратить его
      в короткую заметку «VIEW сейчас, материализуем если затормозит, см. `docs/ARCHITECTURE.md`».
      Добавить «поток данных» в шапку `40_dds_to_dm.sql`.
- [x] Минор: `sql/ddl/dm/40_dm.sql` — магическое `1919` в `groupArraySample`
      (строка «зачем» или упростить).
- Сами витрины DM показываем в деле в уроках 5–6, отдельного разбора не делаем.

**Урок 4 — Airflow DAG (`airflow/dags/etl_pipeline_dag.py`):**
- [x] **Главная правка (код↔доки):** сделать `check_dds_integrity` честным гейтом —
      добавить `assert_dds_integrity` (PythonOperator), роняющий DAG при
      `orphan_events > 0`. Сейчас «проверка» только считает сирот в xcom и пишет их в
      `dq_summary`, но DAG остаётся зелёным, хотя `LESSON_STANDARD` §3 и `PRD` §2 (цель 3)
      обещают остановку при нарушении целостности. После правки доки и код сходятся.
- [x] **Управляемая правка урока 4** строится на этом гейте: менти намеренно ломает
      целостность (вставляет «осиротевшее» событие) и видит, как DAG краснеет на
      `assert_dds_integrity`. Опирается на понятие сирот из урока 3.
- [x] Добавить ASCII-поток задач в docstring DAG
      (`precheck → transform: wait → ods → dq → branch → dds → integrity → dm → validate`)
      — для теста одного прохода.
- Замечание: `check_ods_quality` тоже measure-only (метрики в xcom, без гейта) — это
      ок и намеренно (DQ по `parse_errors` информативен, но не блокирует). В тексте
      урока развести: какие проверки **гейтят** пайплайн, а какие только **измеряют**.

**Урок 5 — мониторинг (`configs/*`):**
- Конфиги (`prometheus.yml`, экспортёры, provisioning Grafana) полировать под «учебное
      качество кода» не нужно: дашборды/JSON не читаются построчно. Вся учебная нагрузка
      ложится на **текст урока** (маршрут по дашбордам + смысл панелей).
- [x] При написании проверить, что имена дашбордов/панелей/метрик в тексте совпадают
      с реальными (`configs/grafana/provisioning/dashboards/*.json`).
- [x] Кандидат на мини-правку (зеркало урока 4): погасить сервис → увидеть, как панель/
      алерт в Grafana краснеет. Решает открытый вопрос PRD про глубину урока (даёт
      «сломал-увидел» вместо чистого наблюдения) — обсудить при написании.

## 4. Что дальше

Все уроки 0–6 и лабы 07–08 написаны (`lessons/`). Правки кода §3.1 применены
в составе уроков; чекбоксы выше сверены с реальным кодом 2026-06-06. Маршрут
курса собран полностью.

Исходный план фазы написания (оставлен как контекст): пишем уроки по одному, по
шаблону из `LESSON_STANDARD.md`, начиная с урока 1 (Kafka→CH); урок 0 (разминка на
Kafka UI) готовим параллельно — он не зависит от полировки кода; правки кода из §3.1
применяем в составе соответствующего урока.
