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:
@@ -84,27 +84,27 @@ ODS мы пересобираем целиком, одной задачей Airf
|
||||
|
||||
## 2. Руки: смотрим базовый прогон
|
||||
|
||||
Поднимаем стенд, создаём схему, заливаем **малый срез** (50 строк на топик) и запускаем
|
||||
трансформацию:
|
||||
Поднимаем стенд и создаём стартовую историю. Это штатный путь курса: готовый источник
|
||||
данных стенда пишет события в Kafka, ClickHouse читает их в STG, затем batch строит
|
||||
ODS, DDS и DM. Файлы `data/*.jsonl` пока остаются только кладовкой значений для этого
|
||||
источника, а не источником аналитического контура.
|
||||
|
||||
```bash
|
||||
make up # поднять инфраструктуру
|
||||
make ddl # создать базы и таблицы (в т.ч. слой ODS)
|
||||
LIMIT=50 make data # залить по 50 строк каждого файла в Kafka → STG
|
||||
make transform # батч STG → ODS → DDS → DM (нас интересует первый шаг)
|
||||
make generated-history-analytics
|
||||
make up
|
||||
```
|
||||
|
||||
`make transform` прогоняет всю цепочку слоёв сразу, но прямо в консоли печатает то, что нам
|
||||
нужно сейчас, — блок **«Статистика ODS»**. Это просто счётчики строк по всем восьми таблицам
|
||||
слоя (четыре основных и четыре с ошибками):
|
||||
Команда прогоняет всю цепочку слоёв и прямо в консоли печатает то, что нам нужно сейчас, —
|
||||
блок **«Статистика ODS»**. Это просто счётчики строк по всем восьми таблицам слоя (четыре
|
||||
основных и четыре с ошибками). Пример формы вывода:
|
||||
|
||||
```
|
||||
Статистика ODS:
|
||||
┌─table──────────────────────┬─rows─┐
|
||||
│ ods.browser_event │ 50 │
|
||||
│ ods.location_event │ 50 │
|
||||
│ ods.device_by_click │ 26 │
|
||||
│ ods.geo_by_click │ 26 │
|
||||
│ ods.browser_event │ ... │
|
||||
│ ods.location_event │ ... │
|
||||
│ ods.device_by_click │ ... │
|
||||
│ ods.geo_by_click │ ... │
|
||||
│ ods.browser_event_errors │ 0 │
|
||||
│ ods.location_event_errors │ 0 │
|
||||
│ ods.device_by_click_errors │ 0 │
|
||||
@@ -114,35 +114,34 @@ make transform # батч STG → ODS → DDS → DM (нас инте
|
||||
|
||||
Прочитаем эту табличку — в ней три вещи, которые стоит заметить.
|
||||
|
||||
**Все четыре `*_errors` — по нулям.** Значит, наш срез чистый: ни одна запись не дала ошибки
|
||||
разбора, столбец `parse_errors` у всех пустой. Это нормально — данные в демо аккуратные.
|
||||
**Все четыре `*_errors` — по нулям.** Значит, стартовая история чистая: ни одна запись не дала
|
||||
ошибки разбора, столбец `parse_errors` у всех пустой. Это нормально — данные стенда аккуратные.
|
||||
Ошибки мы увидим в секции 4, когда сами их устроим.
|
||||
|
||||
**`browser` и `location` дали 50 из 50.** Сколько событий пришло — столько и легло, один к
|
||||
одному.
|
||||
**`browser` и `location` идут в одном зерне события.** Сколько событий пришло, столько строк
|
||||
и ожидаем увидеть после типизации, если ключи валидны.
|
||||
|
||||
**А `device` и `geo` — только 26 из 50.** Вот это уже интересно. Половина куда-то делась? Нет.
|
||||
И это важно понять, иначе дальше будет казаться, что данные текут.
|
||||
**А `device` и `geo` обычно меньше, чем событий.** Вот это уже интересно. Часть строк
|
||||
куда-то делась? Нет. И это важно понять, иначе дальше будет казаться, что данные текут.
|
||||
|
||||
Дело в том, что эти две таблицы хранят не события, а **контекст клика**: с какого устройства
|
||||
был клик и из какой точки на карте. Ключ у них — `click_id`. А в срезе на 50 событий разных
|
||||
кликов всего 26: на один клик приходится несколько событий, и `click_id` у них повторяется.
|
||||
Движок таблицы (про него — в секции 3) схлопывает повторы по ключу, оставляя по одной строке
|
||||
на клик. Отсюда и 26.
|
||||
был клик и из какой точки на карте. Ключ у них — `click_id`. Разных кликов меньше, чем событий:
|
||||
на один клик приходится несколько событий, и `click_id` у них повторяется. Движок таблицы
|
||||
(про него — в секции 3) схлопывает повторы по ключу, оставляя по одной строке на клик.
|
||||
|
||||
Проверь это сам, а не верь на слово. Открой SQL-консоль `http://localhost:9123/play`
|
||||
(пользователь `default`, пароль `123456`) и посчитай, сколько в срезе *различных* `click_id`:
|
||||
(пользователь `default`, пароль `123456`) и посчитай, сколько в STG *различных* `click_id`:
|
||||
|
||||
```sql
|
||||
-- Всего строк в STG — 50, но различных click_id среди них — ровно 26
|
||||
-- Строк событий больше, чем различных click_id
|
||||
SELECT count() AS stg_rows,
|
||||
uniqExact(toUUIDOrNull(JSONExtractString(raw, 'click_id'))) AS distinct_clicks
|
||||
FROM stg.geo_raw;
|
||||
```
|
||||
|
||||
Получишь `stg_rows = 50`, `distinct_clicks = 26` — ровно столько, сколько строк в
|
||||
`ods.geo_by_click`. Значит, 26 — это схлопнутые повторы, а не пропавшие данные. Ничего не
|
||||
потерялось молча.
|
||||
`distinct_clicks` должен быть меньше или равен `stg_rows` и совпадать с числом строк в
|
||||
`ods.geo_by_click`. Значит, это схлопнутые повторы по `click_id`, а не пропавшие данные.
|
||||
Ничего не потерялось молча.
|
||||
|
||||
---
|
||||
|
||||
@@ -174,7 +173,7 @@ toFloat64OrNull(JSONExtractString(raw, 'geo_latitude')) AS
|
||||
Весь смысл — в суффиксе `OrNull`. Если значение **не** приводится к нужному типу (вместо UUID
|
||||
пришёл мусор), функция не падает с ошибкой, а просто возвращает `NULL`. Это ровно то правило
|
||||
стенда, что и в STG — «грязная запись не валит пайплайн», — только теперь на уровне типов.
|
||||
Один кривой `event_id` станет `NULL` и будет помечен, а остальные 49 строк спокойно доедут.
|
||||
Один кривой `event_id` станет `NULL` и будет помечен, а остальные строки спокойно доедут.
|
||||
|
||||
> Кстати, про `AS`: эти строки живут в блоке `WITH` в начале запроса. `WITH` — это просто
|
||||
> способ заранее посчитать значение и дать ему имя, чтобы ниже по запросу ссылаться на него
|
||||
@@ -230,7 +229,7 @@ arrayFilter(x -> x != '', [
|
||||
> если поменять разбор только в одном из двух мест, они разойдутся. В секции 4 мы как раз этим
|
||||
> воспользуемся — и увидим, чем грозит такой рассинхрон.
|
||||
|
||||
### Движок: откуда взялись 26 строк
|
||||
### Движок: почему строк контекста меньше
|
||||
|
||||
И последнее место — строчка про движок основных таблиц:
|
||||
|
||||
@@ -241,8 +240,8 @@ ORDER BY (click_id)
|
||||
|
||||
`ReplacingMergeTree` — это таблица, которая схлопывает строки с одинаковым ключом (ключ берётся
|
||||
из `ORDER BY`), оставляя самую свежую по `src_ingest_ts` — времени загрузки в ODS. Вот она,
|
||||
причина «26 из 50» из секции 2: у `device` и `geo` много строк с одинаковым `click_id`, и
|
||||
движок оставляет по одной на клик.
|
||||
причина разницы из секции 2: у `device` и `geo` много строк с одинаковым `click_id`, и движок
|
||||
оставляет по одной на клик.
|
||||
|
||||
---
|
||||
|
||||
@@ -276,8 +275,8 @@ make transform
|
||||
И смотрим на ту же «Статистику ODS». Таблица ошибок гео, которая была пустой, теперь полная:
|
||||
|
||||
```
|
||||
│ ods.geo_by_click │ 26 │
|
||||
│ ods.geo_by_click_errors │ 50 │ ← было 0
|
||||
│ ods.geo_by_click │ ... │
|
||||
│ ods.geo_by_click_errors │ ... │ ← было 0
|
||||
```
|
||||
|
||||
А в самой основной таблице широта пропала — но не молча, рядом стоит метка:
|
||||
@@ -295,11 +294,10 @@ LIMIT 4;
|
||||
└──────────────┴──────────────┴───────────────┴──────────────────────┘
|
||||
```
|
||||
|
||||
Вот теперь видно всё разом — и DQ-split, и «двойной учёт» из секции 3 вживую. 26 строк
|
||||
остались в основной таблице (ключ `click_id` цел) с пометкой `bad_geo_latitude`. И те же
|
||||
записи попали в число 50 строк `geo_by_click_errors`. Долгота на месте, а широты больше нет:
|
||||
один неверный тип — и целое поле потеряно по всему слою. Заметили это `parse_errors` и таблица
|
||||
ошибок — для того DQ-split и нужен.
|
||||
Вот теперь видно всё разом — и DQ-split, и «двойной учёт» из секции 3 вживую. Строки с валидным
|
||||
`click_id` остались в основной таблице с пометкой `bad_geo_latitude`. И те же записи попали в
|
||||
`geo_by_click_errors`. Долгота на месте, а широты больше нет: один неверный тип — и целое поле
|
||||
потеряно по всему слою. Заметили это `parse_errors` и таблица ошибок — для того DQ-split и нужен.
|
||||
|
||||
> **Бывает и хуже — тихо, совсем без метки.** Здесь нас спас суффикс `OrNull`: неверный тип
|
||||
> дал `NULL`, а `NULL` мы умеем замечать (на него и сработал `parse_errors`). По-настоящему
|
||||
@@ -319,7 +317,7 @@ make transform
|
||||
```
|
||||
|
||||
После этого `geo_by_click_errors` снова `0`, широта на месте. А если стенд совсем «поплыл» —
|
||||
всегда есть полный сброс: `make clean && make up && make ddl && LIMIT=50 make data && make transform`.
|
||||
всегда есть полный чистый прогон: `make generated-history-analytics && make up`.
|
||||
|
||||
---
|
||||
|
||||
@@ -327,9 +325,9 @@ make transform
|
||||
|
||||
| Действие | Где смотреть | Что ожидать |
|
||||
|----------|--------------|-------------|
|
||||
| `make transform` (базовый прогон) | блок «Статистика ODS» | `browser`/`location` = 50, `device`/`geo` = 26, все `*_errors` = 0 |
|
||||
| почему 26, а не 50 | запрос `uniqExact(click_id)` по `stg.geo_raw` | 26 различных `click_id` — это схлопывание повторов, а не потеря |
|
||||
| правка из секции 4 | блок «Статистика ODS» | `ods.geo_by_click_errors` прыгнул `0 → 50` |
|
||||
| базовый прогон | блок «Статистика ODS» | основные таблицы не пустые, все `*_errors` = 0 |
|
||||
| почему `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` |
|
||||
|
||||
---
|
||||
@@ -338,7 +336,8 @@ make transform
|
||||
|
||||
После урока у тебя на руках — видимый результат (одно на выбор):
|
||||
|
||||
- скрин блока «Статистика ODS», где после правки `ods.geo_by_click_errors` ушёл с `0` на `50`;
|
||||
- скрин блока «Статистика ODS», где после правки `ods.geo_by_click_errors` ушёл с `0`
|
||||
на ненулевое число;
|
||||
- либо выборка из `ods.geo_by_click` с пустой широтой и меткой `bad_geo_latitude` рядом.
|
||||
|
||||
И проверь себя на словах — примерно эти вопросы всплывут на еженедельном созвоне:
|
||||
|
||||
Reference in New Issue
Block a user