fix(dds): починены мёртвые проверки *_not_found + правки урока 3 по ревью
- Зачем:
- перечитка урока 3 свежим взглядом нашла баг в его эталонном пути: проверки
device_not_found/geo_not_found/location_not_found в DDS никогда не срабатывали,
а текст урока ошибочно утверждал, что метки ставятся
- Что:
- sql/dds/30_ods_to_dds.sql: добавлен SETTINGS join_use_nulls=1 в оба
INSERT...SELECT. Без него LEFT JOIN на несовпадении клал в assumeNotNull(click_id)
нулевой UUID (не NULL), и if(...IS NULL, ['*_not_found'], []) молча давал []
(мёртвый код). Тот же класс бага про типы/NULL, что kafka_ts в уроке 1
- docs/course/lessons/03_ods_to_dds.md: убраны ложные claim'ы про geo_not_found/
location_not_found, формулировки приведены к реальному поведению (клик без гео
остаётся с пустыми полями NULL; целостность событий — через orphan_events);
поправлена опечатка «список всех клиентов» → «всех кликов»
- Проверка:
- синтетический тест join_use_nulls=1: клик в device без geo → в ods_parse_errors
появляются geo_not_found и geo_country_missing (до фикса — пусто)
- LIMIT=50 make transform после фикса: dds.click=26, dds.event=50,
orphan_events=0, ни одной строки с непустым ods_parse_errors (вывод не изменился —
на чистом срезе несовпадений нет)
This commit is contained in:
@@ -151,7 +151,7 @@ SELECT click_id FROM ods.geo_by_click ...
|
|||||||
|
|
||||||
`UNION DISTINCT` — это «склей два списка в один и выкинь повторы». Получается полный набор
|
`UNION DISTINCT` — это «склей два списка в один и выкинь повторы». Получается полный набор
|
||||||
уникальных `click_id` из обоих источников — будем называть его **универсумом кликов** (полный
|
уникальных `click_id` из обоих источников — будем называть его **универсумом кликов** (полный
|
||||||
список всех клиентов, по которому дальше идём). Именно от него, а не от одной из таблиц, мы
|
список всех кликов, по которому дальше идём). Именно от него, а не от одной из таблиц, мы
|
||||||
строим карточки: так не потеряется клик, который есть, например, в `geo`, но почему-то не доехал
|
строим карточки: так не потеряется клик, который есть, например, в `geo`, но почему-то не доехал
|
||||||
в `device`.
|
в `device`.
|
||||||
|
|
||||||
@@ -206,15 +206,15 @@ LEFT JOIN ( ...снапшот geo... ) AS g ON g.click_id = c.click_id
|
|||||||
останутся пустыми (`NULL`).
|
останутся пустыми (`NULL`).
|
||||||
|
|
||||||
Почему именно `LEFT`: левая таблица здесь — это полный список кликов, и **ни один клик терять
|
Почему именно `LEFT`: левая таблица здесь — это полный список кликов, и **ни один клик терять
|
||||||
нельзя**. Не доехало гео — ладно, сохраним клик с пустым гео и пометкой, что гео нет. Эта пометка
|
нельзя**. Не доехало гео — ладно, сохраним клик, а гео-поля (страна, координаты, IP) останутся
|
||||||
тут же и ставится: рядом со сборкой стоит `if(g.click_id IS NULL, ['geo_not_found'], [])` — если
|
пустыми (`NULL`). И эта пустота — уже видимый сигнал: по ней сразу понятно, что контекст по клику
|
||||||
гео не подтянулось, в список ошибок карточки добавится метка `geo_not_found`. Тот же принцип
|
не подтянулся. Тот же принцип, что и в ODS: **не теряем, а оставляем видимый след**, — только
|
||||||
«не теряем и помечаем», что и `parse_errors` в ODS, только теперь про пропавшие связи.
|
теперь не про кривое поле, а про пропавшую связь между таблицами.
|
||||||
|
|
||||||
> Сущность `dds.event` (события) собирается так же, только проще: `browser` и `location`
|
> Сущность `dds.event` (события) собирается так же, только проще: `browser` и `location`
|
||||||
> связаны по `event_id` один-к-одному, и `LEFT JOIN` приклеивает к каждому событию его страницу
|
> связаны по `event_id` один-к-одному, и `LEFT JOIN` приклеивает к каждому событию его страницу
|
||||||
> и UTM. Если `location` не доехал — событие остаётся, а поля страницы пустые с меткой
|
> и UTM. Если `location` не доехал — событие остаётся, а поля страницы остаются пустыми. Разбирать
|
||||||
> `location_not_found`. Разбирать этот блок построчно не будем — он повторяет ту же логику.
|
> этот блок построчно не будем — он повторяет ту же логику.
|
||||||
|
|
||||||
### Сироты: событие без клика
|
### Сироты: событие без клика
|
||||||
|
|
||||||
@@ -231,10 +231,11 @@ WHERE click_id IS NOT NULL
|
|||||||
AND click_id NOT IN (SELECT click_id FROM dds.click);
|
AND click_id NOT IN (SELECT click_id FROM dds.click);
|
||||||
```
|
```
|
||||||
|
|
||||||
Заметь разницу с предыдущим пунктом. `geo_not_found` — это когда у **клика** нет гео (внутренний
|
Заметь разницу с предыдущим пунктом. Пустое гео — это когда у **клика** не подтянулся свой
|
||||||
пропуск в карточке). А сирота — это когда у **события** нет вообще никакого клика (порвана связь
|
контекст (внутренний пропуск в карточке, но сам клик есть). А сирота — это когда у **события**
|
||||||
между сущностями). Это разные дырки, и следят за ними по отдельности. На чистом срезе сирот ноль —
|
нет вообще никакого клика (порвана связь между сущностями). Это разные дырки: первую видно по
|
||||||
сейчас мы это изменим.
|
пустым полям внутри карточки, вторую — отдельным счётчиком. На чистом срезе сирот ноль — сейчас
|
||||||
|
мы это изменим.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -333,8 +334,8 @@ make transform
|
|||||||
|
|
||||||
- что такое сущность DDS и зачем собирать `dds.click` и `dds.event`, если данные уже есть в ODS;
|
- что такое сущность DDS и зачем собирать `dds.click` и `dds.event`, если данные уже есть в ODS;
|
||||||
- почему соединяем через `LEFT JOIN`, а не обычный `JOIN`, — что было бы с кликами без гео;
|
- почему соединяем через `LEFT JOIN`, а не обычный `JOIN`, — что было бы с кликами без гео;
|
||||||
- что такое сирота и чем разрыв «событие без клика» отличается от пропуска `geo_not_found` внутри
|
- что такое сирота и чем разрыв «событие без клика» отличается от пустого гео внутри карточки
|
||||||
карточки клика.
|
клика.
|
||||||
|
|
||||||
Если запнёшься на `argMax` — вернись к секции 3: он берёт самую свежую строку на каждый
|
Если запнёшься на `argMax` — вернись к секции 3: он берёт самую свежую строку на каждый
|
||||||
`click_id`, чтобы дубли `ReplacingMergeTree` не пролезли в сборку.
|
`click_id`, чтобы дубли `ReplacingMergeTree` не пролезли в сборку.
|
||||||
@@ -354,10 +355,11 @@ make transform
|
|||||||
- **DDS** (этот урок) — склеили кусочки в цельные сущности `dds.click` и `dds.event` и впервые
|
- **DDS** (этот урок) — склеили кусочки в цельные сущности `dds.click` и `dds.event` и впервые
|
||||||
спросили про целостность связей между ними (сироты).
|
спросили про целостность связей между ними (сироты).
|
||||||
|
|
||||||
Заметь общий принцип всех трёх слоёв — **«не теряем, а помечаем»**. На STG не роняем приём из-за
|
Заметь общий принцип всех трёх слоёв — **«не теряем, а оставляем след»**. На STG не роняем приём
|
||||||
кривого сообщения. На ODS не выкидываем битую запись, а помечаем `parse_errors` и копим в
|
из-за кривого сообщения. На ODS не выкидываем битую запись, а помечаем `parse_errors` и копим в
|
||||||
`*_errors`. На DDS не выкидываем клик без гео и событие без клика, а помечаем (`geo_not_found`)
|
`*_errors`. На DDS не выкидываем клик без гео (оставляем его с пустыми полями) и событие без клика
|
||||||
и считаем (`orphan_events`). Один и тот же подход к качеству, проведённый через весь пайплайн.
|
(считаем такие сироты через `orphan_events`). Один и тот же подход к качеству, проведённый через
|
||||||
|
весь пайплайн.
|
||||||
|
|
||||||
> **Короткая заметка про DM.** За DDS есть ещё слой **DM** (Data Marts, витрины для BI) — те самые
|
> **Короткая заметка про DM.** За DDS есть ещё слой **DM** (Data Marts, витрины для BI) — те самые
|
||||||
> `dm.v_events_enriched` и `dm.v_daily_traffic`, которыми ты только что пользовался. Сейчас они
|
> `dm.v_events_enriched` и `dm.v_daily_traffic`, которыми ты только что пользовался. Сейчас они
|
||||||
|
|||||||
@@ -101,7 +101,11 @@ LEFT JOIN (
|
|||||||
FROM ods.geo_by_click
|
FROM ods.geo_by_click
|
||||||
WHERE click_id IS NOT NULL
|
WHERE click_id IS NOT NULL
|
||||||
GROUP BY click_id
|
GROUP BY click_id
|
||||||
) AS g ON g.click_id = c.click_id;
|
) AS g ON g.click_id = c.click_id
|
||||||
|
-- join_use_nulls=1: при несовпадении LEFT JOIN кладёт в правые колонки NULL, а не дефолт.
|
||||||
|
-- Без этого assumeNotNull(click_id) на несовпавшей строке стал бы нулевым UUID (не NULL),
|
||||||
|
-- и проверки if(d.click_id IS NULL, ...) / if(g.click_id IS NULL, ...) молча не сработали бы.
|
||||||
|
SETTINGS join_use_nulls = 1;
|
||||||
|
|
||||||
-- ----------------------------------------------------------------------------
|
-- ----------------------------------------------------------------------------
|
||||||
-- Сущность: dds.event (объединяет browser + location)
|
-- Сущность: dds.event (объединяет browser + location)
|
||||||
@@ -165,4 +169,7 @@ LEFT JOIN (
|
|||||||
FROM ods.location_event
|
FROM ods.location_event
|
||||||
WHERE event_id IS NOT NULL
|
WHERE event_id IS NOT NULL
|
||||||
GROUP BY event_id
|
GROUP BY event_id
|
||||||
) AS l ON l.event_id = b.event_id;
|
) AS l ON l.event_id = b.event_id
|
||||||
|
-- join_use_nulls=1: чтобы при отсутствии location поле l.event_id было NULL, а не нулевым UUID,
|
||||||
|
-- и проверка if(l.event_id IS NULL, ['location_not_found'], []) реально срабатывала.
|
||||||
|
SETTINGS join_use_nulls = 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user