diff --git a/docs/course/lessons/03_ods_to_dds.md b/docs/course/lessons/03_ods_to_dds.md index d06d13e..fe1c8d9 100644 --- a/docs/course/lessons/03_ods_to_dds.md +++ b/docs/course/lessons/03_ods_to_dds.md @@ -151,7 +151,7 @@ SELECT click_id FROM ods.geo_by_click ... `UNION DISTINCT` — это «склей два списка в один и выкинь повторы». Получается полный набор уникальных `click_id` из обоих источников — будем называть его **универсумом кликов** (полный -список всех клиентов, по которому дальше идём). Именно от него, а не от одной из таблиц, мы +список всех кликов, по которому дальше идём). Именно от него, а не от одной из таблиц, мы строим карточки: так не потеряется клик, который есть, например, в `geo`, но почему-то не доехал в `device`. @@ -206,15 +206,15 @@ LEFT JOIN ( ...снапшот geo... ) AS g ON g.click_id = c.click_id останутся пустыми (`NULL`). Почему именно `LEFT`: левая таблица здесь — это полный список кликов, и **ни один клик терять -нельзя**. Не доехало гео — ладно, сохраним клик с пустым гео и пометкой, что гео нет. Эта пометка -тут же и ставится: рядом со сборкой стоит `if(g.click_id IS NULL, ['geo_not_found'], [])` — если -гео не подтянулось, в список ошибок карточки добавится метка `geo_not_found`. Тот же принцип -«не теряем и помечаем», что и `parse_errors` в ODS, только теперь про пропавшие связи. +нельзя**. Не доехало гео — ладно, сохраним клик, а гео-поля (страна, координаты, IP) останутся +пустыми (`NULL`). И эта пустота — уже видимый сигнал: по ней сразу понятно, что контекст по клику +не подтянулся. Тот же принцип, что и в ODS: **не теряем, а оставляем видимый след**, — только +теперь не про кривое поле, а про пропавшую связь между таблицами. > Сущность `dds.event` (события) собирается так же, только проще: `browser` и `location` > связаны по `event_id` один-к-одному, и `LEFT JOIN` приклеивает к каждому событию его страницу -> и UTM. Если `location` не доехал — событие остаётся, а поля страницы пустые с меткой -> `location_not_found`. Разбирать этот блок построчно не будем — он повторяет ту же логику. +> и UTM. Если `location` не доехал — событие остаётся, а поля страницы остаются пустыми. Разбирать +> этот блок построчно не будем — он повторяет ту же логику. ### Сироты: событие без клика @@ -231,10 +231,11 @@ WHERE click_id IS NOT NULL 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; - почему соединяем через `LEFT JOIN`, а не обычный `JOIN`, — что было бы с кликами без гео; -- что такое сирота и чем разрыв «событие без клика» отличается от пропуска `geo_not_found` внутри - карточки клика. +- что такое сирота и чем разрыв «событие без клика» отличается от пустого гео внутри карточки + клика. Если запнёшься на `argMax` — вернись к секции 3: он берёт самую свежую строку на каждый `click_id`, чтобы дубли `ReplacingMergeTree` не пролезли в сборку. @@ -354,10 +355,11 @@ make transform - **DDS** (этот урок) — склеили кусочки в цельные сущности `dds.click` и `dds.event` и впервые спросили про целостность связей между ними (сироты). -Заметь общий принцип всех трёх слоёв — **«не теряем, а помечаем»**. На STG не роняем приём из-за -кривого сообщения. На ODS не выкидываем битую запись, а помечаем `parse_errors` и копим в -`*_errors`. На DDS не выкидываем клик без гео и событие без клика, а помечаем (`geo_not_found`) -и считаем (`orphan_events`). Один и тот же подход к качеству, проведённый через весь пайплайн. +Заметь общий принцип всех трёх слоёв — **«не теряем, а оставляем след»**. На STG не роняем приём +из-за кривого сообщения. На ODS не выкидываем битую запись, а помечаем `parse_errors` и копим в +`*_errors`. На DDS не выкидываем клик без гео (оставляем его с пустыми полями) и событие без клика +(считаем такие сироты через `orphan_events`). Один и тот же подход к качеству, проведённый через +весь пайплайн. > **Короткая заметка про DM.** За DDS есть ещё слой **DM** (Data Marts, витрины для BI) — те самые > `dm.v_events_enriched` и `dm.v_daily_traffic`, которыми ты только что пользовался. Сейчас они diff --git a/sql/dds/30_ods_to_dds.sql b/sql/dds/30_ods_to_dds.sql index 851939d..074eba6 100644 --- a/sql/dds/30_ods_to_dds.sql +++ b/sql/dds/30_ods_to_dds.sql @@ -101,7 +101,11 @@ LEFT JOIN ( FROM ods.geo_by_click WHERE click_id IS NOT NULL 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) @@ -165,4 +169,7 @@ LEFT JOIN ( FROM ods.location_event WHERE event_id IS NOT NULL 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;