docs(course): переведены уроки на стартовую историю

- Зачем:
  - учебный путь должен идти через генерацию и штатный пайплайн, а не через архивный сид.
- Что:
  - обновлены уроки 00-06 и стандарт урока под startup-history/backfill.
  - объяснено, что data/*.jsonl остаются кладовкой значений генератора.
  - тест-план переведён на новый штатный запуск и HITL-приёмку.
- Проверка:
  - rg -n \"make clean/up/ddl/data/transform|make data|kafka_load|LIMIT=|2022-11-28|26 из 50\" docs/course docs/TEST_PLAN.md.
  - git diff --cached --check.
This commit is contained in:
2026-07-04 22:22:36 +03:00
parent de938edb06
commit d76c03655b
10 changed files with 192 additions and 201 deletions
+30 -30
View File
@@ -65,35 +65,35 @@
## 2. Руки: смотрим базовый прогон
Поднимаем стенд, создаём схему, заливаем **малый срез** (50 строк на топик) и запускаем
трансформацию:
Поднимаем стенд и создаём стартовую историю. Это штатный путь курса: готовый источник
данных стенда пишет события в Kafka, ClickHouse читает их в STG, затем batch строит
ODS, DDS и DM. Файлы `data/*.jsonl` пока остаются только кладовкой значений для этого
источника, а не источником аналитического контура.
```bash
make up # поднять инфраструктуру
make ddl # создать базы и таблицы (в т.ч. слой DDS)
LIMIT=50 make data # залить по 50 строк каждого файла в Kafka → STG
make transform # батч STG → ODS → DDS → DM
make generated-history-analytics
make up
```
`make transform` прогоняет всю цепочку слоёв и по дороге печатает в консоль блок **«Статистика
DDS»** — счётчики строк по двум нашим сущностям:
Команда прогоняет всю цепочку слоёв и по дороге печатает в консоль блок **«Статистика DDS»** —
счётчики строк по двум нашим сущностям:
```
Статистика DDS:
┌─table─────┬─rows─┐
│ dds.click │ 26
│ dds.event │ 50
│ dds.click │ ...
│ dds.event │ ...
└───────────┴──────┘
```
Прочитаем эти две строки.
**`dds.event`50.** Сколько событий пришло, столько карточек и собралось: одно событие — одна
строка. Ровно как `ods.browser_event` из прошлого урока.
**`dds.event`события.** Сколько событий пришло с валидным ключом, столько карточек и
собралось: одно событие — одна строка. Ровно как `ods.browser_event` из прошлого урока.
**`dds.click` — 26, а не 50.** И это та же история, что мы уже разбирали в уроке 2. Карточка
клика — одна на клик, а в срезе на 50 событий разных кликов всего 26 (на один клик приходится
несколько событий). Поэтому 50 событий ссылаются на 26 кликов — это нормально, так и должно быть.
**`dds.click` обычно меньше, чем `dds.event`.** И это та же история, что мы уже разбирали
в уроке 2. Карточка клика — одна на клик, а событий на один клик может быть несколько.
Поэтому много событий ссылаются на меньшее число кликов — это нормально, так и должно быть.
Теперь — главный счётчик урока. Он печатается чуть ниже, в блоке **«Сводка по качеству данных»**
(это таблица `dm.dq_summary`, куда стенд складывает метрики по всем слоям). Найди в ней строку
@@ -108,9 +108,9 @@ DDS»** — счётчики строк по двум нашим сущност
В колонке `check_date` стоит `today()` из кода витрины, так что у тебя там будет сегодняшняя
дата — не пугайся, если она не совпадёт с примером.
`orphan_events = 0` — ни одной сироты. Каждое из 50 событий нашло свой клик в `dds.click`. На
чистом демо-срезе так и должно быть: данные аккуратные, ничего не потерялось. В секции 4 мы
сироту устроим сами — и эта строка оживёт.
`orphan_events = 0` — ни одной сироты. Каждое событие нашло свой клик в `dds.click`. На
чистой стартовой истории так и должно быть: данные аккуратные, ничего не потерялось. В секции
4 мы сироту устроим сами — и эта строка оживёт.
Проверь нолик сам, не верь на слово. Открой SQL-консоль `http://localhost:9123/play`
(пользователь `default`, пароль `123456`) и посчитай сирот напрямую:
@@ -158,9 +158,9 @@ SELECT click_id FROM ods.geo_by_click ...
строим карточки: так не потеряется клик, который есть, например, в `geo`, но почему-то не доехал
в `device`.
> На нашем срезе `device` и `geo` содержат один и тот же набор из 26 кликов, так что универсум
> тоже 26. Но код написан так, чтобы пережить случай, когда наборы **разойдутся**, — и это
> правильно: в проде они расходятся постоянно.
> На чистой стартовой истории `device` и `geo` должны содержать один и тот же набор кликов,
> так что универсум совпадает с обоими источниками. Но код написан так, чтобы пережить случай,
> когда наборы **разойдутся**, — и это правильно: в проде они расходятся постоянно.
### `argMax`: одна строка на клик, самая свежая
@@ -221,7 +221,7 @@ LEFT JOIN ( ...снапшот geo... ) AS g ON g.click_id = c.click_id
### Сироты: событие без клика
Мы собрали `dds.click` (26 карточек кликов) и `dds.event` (50 карточек событий). Внутри каждого
Мы собрали `dds.click` (карточки кликов) и `dds.event` (карточки событий). Внутри каждого
события лежит `click_id` — ссылка на клик. И вот тут возникает вопрос целостности из секции 1:
**а на каждую ли ссылку есть карточка клика?**
@@ -237,16 +237,16 @@ WHERE click_id IS NOT NULL
Заметь разницу с предыдущим пунктом. Пустое гео — это когда у **клика** не подтянулся свой
контекст (внутренний пропуск в карточке, но сам клик есть). А сирота — это когда у **события**
нет вообще никакого клика (порвана связь между сущностями). Это разные дырки: первую видно по
пустым полям внутри карточки, вторую — отдельным счётчиком. На чистом срезе сирот ноль — сейчас
мы это изменим.
пустым полям внутри карточки, вторую — отдельным счётчиком. На чистой стартовой истории сирот
ноль — сейчас мы это изменим.
---
## 4. Управляемая правка: заведём сироту
Сирота на чистом срезе не появится сама — данные слишком аккуратные. Поэтому **создадим её
руками**: добавим в `dds.event` одно событие, которое ссылается на клик, которого в `dds.click`
нет. И посмотрим, как оживёт счётчик сирот и как себя поведёт `LEFT JOIN`.
Сирота на чистой стартовой истории не появится сама — данные слишком аккуратные. Поэтому
**создадим её руками**: добавим в `dds.event` одно событие, которое ссылается на клик, которого
в `dds.click` нет. И посмотрим, как оживёт счётчик сирот и как себя поведёт `LEFT JOIN`.
Открой SQL-консоль `http://localhost:9123/play` и вставь придуманное событие:
@@ -309,7 +309,7 @@ make transform
```
После этого `orphan_events` снова `0`, придуманное событие исчезло. А если стенд совсем «поплыл» —
полный сброс: `make clean && make up && make ddl && LIMIT=50 make data && make transform`.
полный чистый прогон: `make generated-history-analytics && make up`.
---
@@ -317,9 +317,9 @@ make transform
| Действие | Где смотреть | Что ожидать |
|----------|--------------|-------------|
| `make transform` (базовый прогон) | блок «Статистика DDS» | `dds.click` = 26, `dds.event` = 50 |
| базовый прогон | блок «Статистика DDS» | `dds.click` и `dds.event` не пустые |
| `make transform` (базовый прогон) | блок «Сводка по качеству», строка `orphan_events` | `0` |
| почему `click` = 26, а `event` = 50 | запрос `count()` по `dds.click` и `dds.event` | 50 событий ссылаются на 26 кликов — норма |
| почему `click` меньше `event` | запрос `count()` по `dds.click` и `dds.event` | много событий ссылаются на меньшее число кликов — норма |
| правка из секции 4 (вставили сироту) | запрос `count()` сирот в play-консоли | `0 → 1` |
| та же сирота через `dm.v_events_enriched` | `SELECT device_type, geo_country ...` | поля клика пустые (`NULL`) — это `LEFT JOIN` |