docs(course): каркас курса и переобвязка уроков 0–6 на путь import (#21)

Зачем: после редизайна пути менти курс ссылался на старый путь
generated-history-analytics/backfill и не проходился по новому стенду.

Что: в README курса — единый блок подготовки и канонического сброса
(make clean -> make up + ddl_init/world_init -> make superset-init),
таблица уроков дополнена лабами 07–08 («в работе»); LESSON_STANDARD и
уроки 0–6 ссылаются на канонический блок; урок 1 переведён на дозаливку
через world_next_day (кнопкой-анонсом, цена в минутах названа); урок 4 —
лесенка DAG-ов; урок 5 — словарь «база import / живой поток»; урок 6
обязателен; цифры старого мира помечены маркером «сверить-на-стенде».

Проверка: grep по generated-history-analytics/backfill в docs/course/
пуст; правки только в docs/course/; git diff --check чистый.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-23 13:22:41 +03:00
co-authored by Claude Opus 4.8
parent b57a4e2292
commit cc1cffe2f3
11 changed files with 142 additions and 171 deletions
+4 -6
View File
@@ -41,12 +41,9 @@ consumer читает в своём темпе, не трогая producer'а.
## 2. Наблюдай: открой Kafka UI
Стенд уже должен быть поднят, а в топиках должны лежать события после стартовой истории:
```bash
make generated-history-analytics
make up
```
Подготовь стенд по
[канонической инструкции курса](../README.md#подготовка-и-канонический-сброс).
После успешного `world_init` в топиках уже лежат события эталонного мира.
Открой Kafka UI:
`http://localhost:8082`. Ходи по нему свободно — это режим чтения, сломать тут ничего нельзя.
@@ -85,6 +82,7 @@ make up
{"event_id": "8cca1c7d-...", "event_timestamp": "2026-01-01 00:01:00.000000",
"event_type": "pageview", "browser_name": "Chrome", "browser_language": "sat_IN"}
```
<!-- сверить-на-стенде -->
Загляни внутрь Value: у события есть своё `event_timestamp` (когда оно случилось,
модельное время стенда), и оно отличается от Kafka-Timestamp (когда оно попало в топик).
+17 -38
View File
@@ -49,15 +49,9 @@
## 2. Руки: убедись, что данные текут
Поднимаем стенд и создаём стартовую историю. Это штатный путь курса: готовый источник
данных стенда пишет события в Kafka, ClickHouse читает их в STG, затем batch строит
ODS, DDS и DM. Файлы `data/*.jsonl` в этом пути не источник аналитики; пока это только
кладовка значений для источника данных стенда.
```bash
make generated-history-analytics
make up
```
Подготовь стенд по
[канонической инструкции курса](../README.md#подготовка-и-канонический-сброс).
После успешного `world_init` эталонный мир уже прошёл путь Kafka → STG → ODS → DDS → DM.
Теперь смотрим, что доехало до ClickHouse. Открой SQL-консоль:
`http://localhost:9123/play` (или Kafka UI на `http://localhost:8082`, чтобы тем же
@@ -189,27 +183,23 @@ SELECT
FROM stg.kafka_browser_raw;
```
**Перезаливаем срез, чтобы новая колонка заполнилась.** Сначала чистим только таблицу
приёмника — иначе рядом останутся строки из секции 2, вставленные ещё *до*
`ADD COLUMN`, и в них `kafka_msg_ts` будет пустой (`1970-01-01`):
**Дозаливаем следующий день, чтобы новая колонка заполнилась.** Не очищай
`stg.browser_raw`: `world_next_day` растит уже импортированный мир, а его финальная
проверка сверяет весь накопленный результат. В старых строках, вставленных до
`ADD COLUMN`, `kafka_msg_ts` останется пустым (`1970-01-01`) — ниже мы их отфильтруем.
```sql
TRUNCATE TABLE stg.browser_raw;
```
Затем повторно создаём стартовую историю **без очистки volumes**. Это важно:
`CLEAN_START=0` сохраняет твою новую колонку и пересозданное MV, но добавляет свежие
сообщения в Kafka, чтобы ClickHouse прочитал их уже с новой схемой.
```bash
CLEAN_START=0 make generated-history-analytics
```
Открой Airflow и кнопкой **Trigger DAG** запусти `world_next_day` с пустой формой.
Он дозальёт в Kafka следующий день, а ClickHouse прочитает новые сообщения уже с
изменённой схемой. Такой прогон занимает несколько минут — дождись состояния
`success`. Здесь мы только нажимаем готовую кнопку; подробно `world_next_day`
разберём в лабе 07.
**Смотрим результат:**
```sql
SELECT kafka_ts, kafka_msg_ts
FROM stg.browser_raw
WHERE kafka_msg_ts > toDateTime(0)
ORDER BY kafka_offset
LIMIT 5;
```
@@ -224,19 +214,8 @@ LIMIT 5;
> теряется. Обратный случай — колонку добавил, а в MV не указал — тоже не упадёт: поле
> заполнится дефолтом. Вывод: за синхронность схемы и MV отвечаешь ты, а не движок.
**Верни как было** (откат — тоже две операции, и порядок важен):
```sql
DROP VIEW stg.mv_kafka_browser_to_stg; -- сначала MV, что ссылается на колонку
ALTER TABLE stg.browser_raw DROP COLUMN kafka_msg_ts;
```
```bash
make ddl # пересоздаёт эталонное MV из 10_stg.sql, схема снова как в репозитории
```
Если запутался в состоянии — всегда есть полный чистый прогон:
`make generated-history-analytics && make up`.
**Верни как было.** После дозаливки мир уже вырос, поэтому верни схему и данные
[каноническим сбросом](../README.md#подготовка-и-канонический-сброс).
---
@@ -244,10 +223,10 @@ make ddl # пересоздаёт эталонное MV из 10_stg.sql, с
| Действие | Где смотреть | Что ожидать |
|----------|--------------|-------------|
| `make generated-history-analytics && make up` | `SELECT count() FROM stg.browser_raw` | счётчик > 0 |
| каноническая подготовка | `SELECT count() FROM stg.browser_raw` | счётчик > 0 |
| глянуть строку | `SELECT raw FROM stg.browser_raw LIMIT 1` | валидный JSON целиком, неразобранный |
| глянуть offset'ы | `SELECT kafka_offset FROM stg.browser_raw ORDER BY kafka_offset` | идут по возрастанию, без дублей |
| правка из секции 4 | `SELECT kafka_msg_ts FROM stg.browser_raw LIMIT 5` | колонка заполнена временем сообщения |
| `world_next_day` после правки из секции 4 | `SELECT kafka_msg_ts FROM stg.browser_raw WHERE kafka_msg_ts > toDateTime(0) LIMIT 5` | новые строки заполнены временем сообщения |
---
+16 -14
View File
@@ -84,19 +84,12 @@ ODS мы пересобираем целиком, одной задачей Airf
## 2. Руки: смотрим базовый прогон
Поднимаем стенд и создаём стартовую историю. Это штатный путь курса: готовый источник
данных стенда пишет события в Kafka, ClickHouse читает их в STG, затем batch строит
ODS, DDS и DM. Файлы `data/*.jsonl` пока остаются только кладовкой значений для этого
источника, а не источником аналитического контура.
Подготовь стенд по
[канонической инструкции курса](../README.md#подготовка-и-канонический-сброс).
После успешного `world_init` эталонный мир уже прошёл путь Kafka → STG → ODS → DDS → DM.
```bash
make generated-history-analytics
make up
```
Команда прогоняет всю цепочку слоёв и прямо в консоли печатает то, что нам нужно сейчас, —
блок **«Статистика ODS»**. Это просто счётчики строк по всем восьми таблицам слоя (четыре
основных и четыре с ошибками). Пример формы вывода:
Ниже — форма блока **«Статистика ODS»**, который печатает `make transform`. Это просто
счётчики строк по всем восьми таблицам слоя (четыре основных и четыре с ошибками):
```
Статистика ODS:
@@ -111,12 +104,14 @@ make up
│ ods.geo_by_click_errors │ 0 │
└────────────────────────────┴──────┘
```
<!-- сверить-на-стенде -->
Прочитаем эту табличку — в ней три вещи, которые стоит заметить.
**Все четыре `*_errors` — по нулям.** Значит, стартовая история чистая: ни одна запись не дала
ошибки разбора, столбец `parse_errors` у всех пустой. Это нормально — данные стенда аккуратные.
Ошибки мы увидим в секции 4, когда сами их устроим.
<!-- сверить-на-стенде -->
**`browser` и `location` идут в одном зерне события.** Сколько событий пришло, столько строк
и ожидаем увидеть после типизации, если ключи валидны.
@@ -254,6 +249,7 @@ ORDER BY (click_id)
её через `toFloat64OrNull` — «привести к дробному числу». Заменим тип на целочисленный —
`toInt64OrNull`, «привести к целому». Для строки `"50.82709"` целого числа не получится
(там точка, дробная часть), и функция вернёт `NULL`. То есть широта просто исчезнет.
<!-- сверить-на-стенде -->
Из секции 3 помним: разбор продублирован, поэтому правок будет **две** — в обоих `INSERT`
блока `GEO EVENTS`. Открой `sql/ods/20_stg_to_ods.sql`, найди оба вхождения и в каждом замени
@@ -293,6 +289,7 @@ LIMIT 4;
│ 9ffd819b-... │ ᴺᵁᴸᴸ │ 85.37752 │ ['bad_geo_latitude'] │
└──────────────┴──────────────┴───────────────┴──────────────────────┘
```
<!-- сверить-на-стенде -->
Вот теперь видно всё разом — и DQ-split, и «двойной учёт» из секции 3 вживую. Строки с валидным
`click_id` остались в основной таблице с пометкой `bad_geo_latitude`. И те же записи попали в
@@ -307,6 +304,7 @@ LIMIT 4;
> это молча срезало миллисекунды, и время по всему стенду уехало в `1970-01-21`. Ничто на это
> не указывало — поймали только прогоном на стенде. Мораль урока: тип выбирают осознанно, даже
> когда функция «не падает».
<!-- сверить-на-стенде -->
**Верни как было.** Откати обе правки — верни `toFloat64OrNull` в оба места. Если запутался,
проще одной командой откатить весь файл к версии из репозитория:
@@ -316,8 +314,10 @@ git checkout -- sql/ods/20_stg_to_ods.sql
make transform
```
После этого `geo_by_click_errors` снова `0`, широта на месте. А если стенд совсем «поплыл» —
всегда есть полный чистый прогон: `make generated-history-analytics && make up`.
После этого `geo_by_click_errors` снова `0`, широта на месте. А если стенд совсем
«поплыл», пройди
[канонический сброс](../README.md#подготовка-и-канонический-сброс).
<!-- сверить-на-стенде -->
---
@@ -329,6 +329,7 @@ make transform
| почему `device`/`geo` меньше событий | запрос `uniqExact(click_id)` по `stg.geo_raw` | число различных `click_id` совпадает с `ods.geo_by_click` |
| правка из секции 4 | блок «Статистика ODS» | `ods.geo_by_click_errors` прыгнул с `0` на ненулевое число |
| правка из секции 4 | `SELECT geo_latitude, parse_errors FROM ods.geo_by_click` | широта `NULL`, в `parse_errors``bad_geo_latitude` |
<!-- сверить-на-стенде -->
---
@@ -339,6 +340,7 @@ make transform
- скрин блока «Статистика ODS», где после правки `ods.geo_by_click_errors` ушёл с `0`
на ненулевое число;
- либо выборка из `ods.geo_by_click` с пустой широтой и меткой `bad_geo_latitude` рядом.
<!-- сверить-на-стенде -->
И проверь себя на словах — примерно эти вопросы всплывут на еженедельном созвоне:
+14 -13
View File
@@ -65,18 +65,12 @@
## 2. Руки: смотрим базовый прогон
Поднимаем стенд и создаём стартовую историю. Это штатный путь курса: готовый источник
данных стенда пишет события в Kafka, ClickHouse читает их в STG, затем batch строит
ODS, DDS и DM. Файлы `data/*.jsonl` пока остаются только кладовкой значений для этого
источника, а не источником аналитического контура.
Подготовь стенд по
[канонической инструкции курса](../README.md#подготовка-и-канонический-сброс).
После успешного `world_init` эталонный мир уже прошёл путь Kafka → STG → ODS → DDS → DM.
```bash
make generated-history-analytics
make up
```
Команда прогоняет всю цепочку слоёв и по дороге печатает в консоль блок **«Статистика DDS»** —
счётчики строк по двум нашим сущностям:
Ниже — форма блока **«Статистика DDS»**, который печатает `make transform`: счётчики
строк по двум нашим сущностям.
```
Статистика DDS:
@@ -104,6 +98,7 @@ make up
│ 2026-06-05 │ dds │ event_without_click │ orphan_events │ 0 │
└────────────┴───────┴─────────────────────┴───────────────┴─────────────┘
```
<!-- сверить-на-стенде -->
В колонке `check_date` стоит `today()` из кода витрины, так что у тебя там будет сегодняшняя
дата — не пугайся, если она не совпадёт с примером.
@@ -111,6 +106,7 @@ make up
`orphan_events = 0` — ни одной сироты. Каждое событие нашло свой клик в `dds.click`. На
чистой стартовой истории так и должно быть: данные аккуратные, ничего не потерялось. В секции
4 мы сироту устроим сами — и эта строка оживёт.
<!-- сверить-на-стенде -->
Проверь нолик сам, не верь на слово. Открой SQL-консоль `http://localhost:9123/play`
(пользователь `default`, пароль `123456`) и посчитай сирот напрямую:
@@ -126,6 +122,7 @@ WHERE click_id IS NOT NULL
`NOT IN (SELECT ...)` читается прямо по словам: «click_id события **не входит** в список всех
click_id из `dds.click`». То есть событие ссылается на клик, которого в карточках кликов нет.
Сейчас таких ноль — запомни этот запрос, в секции 4 он покажет другое число.
<!-- сверить-на-стенде -->
---
@@ -308,8 +305,10 @@ DDS — `make transform` чистит `dds.event` (`TRUNCATE`) и наполня
make transform
```
После этого `orphan_events` снова `0`, придуманное событие исчезло. А если стенд совсем «поплыл» —
полный чистый прогон: `make generated-history-analytics && make up`.
После этого `orphan_events` снова `0`, придуманное событие исчезло. А если стенд
совсем «поплыл», пройди
[канонический сброс](../README.md#подготовка-и-канонический-сброс).
<!-- сверить-на-стенде -->
---
@@ -322,6 +321,7 @@ make transform
| почему `click` меньше `event` | запрос `count()` по `dds.click` и `dds.event` | много событий ссылаются на меньшее число кликов — норма |
| правка из секции 4 (вставили сироту) | запрос `count()` сирот в play-консоли | `0 → 1` |
| та же сирота через `dm.v_events_enriched` | `SELECT device_type, geo_country ...` | поля клика пустые (`NULL`) — это `LEFT JOIN` |
<!-- сверить-на-стенде -->
---
@@ -332,6 +332,7 @@ make transform
- скрин запроса со счётчиком сирот: было `0`, после вставки стало `1`;
- либо выборка из `dm.v_events_enriched` по событию-сироте, где `device_type` и `geo_country`
пусты, — `LEFT JOIN` сохранил событие без клика.
<!-- сверить-на-стенде -->
И проверь себя на словах — примерно эти вопросы всплывут на еженедельном созвоне:
+17 -17
View File
@@ -56,15 +56,19 @@ Airflow. Главная единица Airflow — **DAG** (Directed Acyclic Gra
## 2. Руки: запускаем DAG и смотрим зелёный прогон
Подними стенд и создай стартовую историю. Это штатный путь курса: готовый источник
данных стенда пишет события в Kafka, ClickHouse читает их в STG, затем batch строит
ODS, DDS и DM. Файлы `data/*.jsonl` пока остаются только кладовкой значений для этого
источника, а не источником аналитического контура.
Подготовь стенд по
[канонической инструкции курса](../README.md#подготовка-и-канонический-сброс).
После успешного `world_init` эталонный мир уже прошёл путь Kafka → STG → ODS → DDS → DM.
```bash
make generated-history-analytics
make up
```
Перед разбором `etl_pipeline` вспомни пульт курса как лесенку:
1. `ddl_init` создаёт схему ClickHouse;
2. `world_init` импортирует эталонный мир и запускает его обработку;
3. `world_next_day` дозаливает следующий день и снова запускает обработку.
Первые две ступени ты уже прошёл при подготовке. Третью пока только запомни: подробно
её разберём в лабе 07. Внутри двух последних ступеней работает тот самый
`etl_pipeline`, который мы сейчас откроем отдельно.
Открой Airflow: `http://localhost:8080` (логин `admin`, пароль `admin`). Найди DAG
`etl_pipeline` и запусти его через **Trigger DAG with config**:
@@ -97,6 +101,7 @@ WHERE layer = 'dds'
Ожидаем `check_value = 0`. Это тот же смысл, что в уроке 3, только теперь число появилось внутри
управляемого прогона Airflow.
<!-- сверить-на-стенде -->
---
@@ -307,15 +312,9 @@ WHERE click_id IS NOT NULL
AND click_id NOT IN (SELECT click_id FROM dds.click);
```
Снова должно быть `0`. Если стенд после экспериментов совсем запутался, сделай штатный
чистый прогон:
```bash
make generated-history-analytics
make up
```
После этого при необходимости запусти `etl_pipeline` с `{"full_refresh": true}`.
Снова должно быть `0`. Если стенд после экспериментов совсем запутался, пройди
[канонический сброс](../README.md#подготовка-и-канонический-сброс).
<!-- сверить-на-стенде -->
---
@@ -328,6 +327,7 @@ make up
| вставка события-сироты | прямой SQL-счётчик сирот | `0 → 1` |
| `etl_pipeline` с `{"full_refresh": false}` после вставки | task `transform.assert_dds_integrity` | task красная, DAG failed |
| откат через `{"full_refresh": true}` | прямой SQL-счётчик сирот | снова `0` |
<!-- сверить-на-стенде -->
---
+14 -28
View File
@@ -57,24 +57,10 @@
## 2. Руки: открываем дашборды и targets
Подними стенд и создай стартовую историю, если он ещё не поднят. Это штатный путь курса:
готовый источник данных стенда пишет события в Kafka, ClickHouse читает их в STG, затем
batch строит ODS, DDS и DM. Файлы `data/*.jsonl` пока остаются только кладовкой значений
для этого источника, а не источником аналитического контура.
```bash
make generated-history-analytics
make up
```
Учти: путь `generated-history-analytics` собирает витрины напрямую, без запуска
`etl_pipeline`, поэтому панели про задачи Airflow в `Airflow Overview` останутся
пустыми, пока ты хотя бы раз не запустишь `etl_pipeline` сам (это делалось в
уроке 4). Запустить его можно в Airflow с конфигом:
```json
{"full_refresh": true}
```
Подготовь стенд по
[канонической инструкции курса](../README.md#подготовка-и-канонический-сброс).
После успешного `world_init` эталонный мир уже прошёл путь Kafka → STG → ODS → DDS → DM,
а в Airflow есть завершённый прогон `etl_pipeline`.
Нам нужны не идеальные объёмы, а живой стенд, в котором есть Kafka-топики, строки в ClickHouse
и хотя бы один прогон Airflow.
@@ -92,10 +78,10 @@ make up
| `airflow` | `statsd-exporter:9102` | `statsd-exporter` отдаёт метрики Airflow в формате Prometheus |
| `generator` | `generator:9109` | live-генератор отдаёт свои метрики, только когда явно запущен |
У `clickhouse`, `kafka` и `airflow` состояние должно быть `UP`. `generator` на штатном
backfill-only стенде может быть `DOWN`, потому что `make up` не запускает live-генератор.
Это нормально для курса до явного `make generator-continue`. Если один из трёх основных
target `DOWN`, Grafana дальше будет показывать `No data` или старые значения.
У `clickhouse`, `kafka` и `airflow` состояние должно быть `UP`. `generator` на базе
импортированного мира может быть `DOWN`: живой поток включается отдельно через
`make generator-continue`. Если один из трёх основных target `DOWN`, Grafana дальше
будет показывать `No data` или старые значения.
То же можно проверить из терминала:
@@ -125,7 +111,7 @@ curl -s http://localhost:9090/api/v1/targets | grep -o '"health":"[^"]*"'
- `ClickHouse Overview`;
- `Kafka Overview`;
- `Airflow Overview`;
- `Generator Overview` — про live-генератор; на backfill-only стенде он пуст,
- `Generator Overview` — про живой поток; на базе импортированного мира он пуст,
как и target `generator` выше, и в этом уроке не понадобится.
Открой каждый и смотри не на красоту графиков, а на смысл: какой слой стенда он показывает и
@@ -272,11 +258,11 @@ Grafana.
| Airflow Alerts | `High Task Failure Rate` | растёт rate failed tasks |
| Airflow Alerts | `High DAG Parse Time` | DAG-файлы долго парсятся |
Не все эти правила обязаны быть тихими в учебном стенде. По умолчанию `make up` и
`make generated-history-analytics` **не запускают live-генератор**, поэтому после готовой
стартовой истории новые сообщения перестают приходить. Из-за этого `Kafka No Messages Produced`
может перейти в `Alerting` на полностью здоровом backfill-only стенде. Если хочешь проверить
это правило в спокойном состоянии, явно включи live:
Не все эти правила обязаны быть тихими в учебном стенде. После импорта эталонной базы
живой поток не запущен, поэтому новые сообщения не приходят. Из-за этого
`Kafka No Messages Produced` может перейти в `Alerting` на полностью здоровом стенде:
алерт честно говорит, что потока сейчас нет, а не что импорт сломан. Если хочешь
проверить правило в спокойном состоянии, явно включи живой поток:
```bash
make generator-continue
+4 -11
View File
@@ -63,17 +63,10 @@ DM-витрина — это SQL-объект в ClickHouse. Она задаёт
## 2. Руки: запускаем Superset и смотрим дашборд
Подними стенд и создай стартовую историю. Это штатный путь курса: готовый источник
данных стенда пишет события в Kafka, ClickHouse читает их в STG, затем batch строит
ODS, DDS, DM и обновляет Superset. Файлы `data/*.jsonl` пока остаются только кладовкой
значений для этого источника, а не источником аналитического контура.
```bash
make generated-history-analytics
make up
```
Команда уже прогоняет цепочку STG → ODS → DDS → DM и создаёт metadata Superset:
Подготовь стенд по
[канонической инструкции курса](../README.md#подготовка-и-канонический-сброс).
После успешного `world_init` эталонный мир уже прошёл путь Kafka → STG → ODS → DDS → DM,
а `make superset-init` создал метаданные Superset:
- подключение `clickhouse_dwh`;
- 6 datasets поверх `dm.*`.