From b839a671a10fe598189fb265239f557a063cb9f5 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Tue, 22 Sep 2026 05:57:11 -0400 Subject: [PATCH 1/2] =?UTF-8?q?docs(labs):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B8=D1=81=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=81=D0=B2=D1=8F=D0=B7=D0=B5=D0=B9=20=D0=B8=20?= =?UTF-8?q?=D0=B8=D0=BD=D0=BA=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D1=82=D0=B0?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20=D0=BF=D0=B5=D1=80=D0=B5=D1=81?= =?UTF-8?q?=D1=87=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Зачем: дать практику учета новых связей пользователей при обновлении прежних дней витрины. Что: добавлены две связанные лабораторные с наблюдениями через Airflow, собственными таблицами student и повтором частично записанной загрузки. Обновлены программа, индекс и ссылки из справочников DDS и DM. Проверка: DDL и SQL под student на ClickHouse 26.3.17.56, четыре модельных дня через Airflow, полные сверки и повтор незавершенной загрузки; config-test, lint, локальные ссылки и независимое ревью. --- docs/architecture/dds/identity.md | 2 +- docs/architecture/dm.md | 5 +- docs/labs/README.md | 13 +- docs/labs/optional/identity-over-time.md | 133 ++++++++++++++++ .../optional/incremental-daily-traffic.md | 146 ++++++++++++++++++ docs/specs/2026-09-01-labs-program.md | 45 +++++- 6 files changed, 335 insertions(+), 9 deletions(-) create mode 100644 docs/labs/optional/identity-over-time.md create mode 100644 docs/labs/optional/incremental-daily-traffic.md diff --git a/docs/architecture/dds/identity.md b/docs/architecture/dds/identity.md index f883eb3..4f2859b 100644 --- a/docs/architecture/dds/identity.md +++ b/docs/architecture/dds/identity.md @@ -30,7 +30,7 @@ `first_matched_at` нет. Запланированные витрины его не читают, а два возможных смысла — «первая покупка вообще» и «момент, с которого связь была известна хранилищу» — требуют разных исторических моделей. Этот вопрос оставлен -[самостоятельной работе](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/145), +[самостоятельной работе](../../labs/optional/identity-over-time.md), а текущий стенд не выдаёт один смысл за другой. Статус заказа связь не меняет: любой заказ, сопоставленный с событием покупки, diff --git a/docs/architecture/dm.md b/docs/architecture/dm.md index 056b350..3ef9e6d 100644 --- a/docs/architecture/dm.md +++ b/docs/architecture/dm.md @@ -281,7 +281,10 @@ business_key`. - **Инкрементальный пересчёт затронутых дней по `first_matched_at`.** Производственный ход, но на объёмах стенда его экономия неизмерима, а след новых связок требует вернуть колонку, выкинутую в #133. Стал лабой - повышенной сложности — issue #149. + повышенной сложности — + [«Как обновлять витрину без полной пересборки»](../labs/optional/incremental-daily-traffic.md). + В ней новые связи находятся сравнением наблюдений: время покупки + не заменяет момент обнаружения связи. - **Superset поверх DDS без слоя DM.** Терялся бы урок «витрина — контракт под потребителя», против раздела 7 спеки. diff --git a/docs/labs/README.md b/docs/labs/README.md index 59a2ff7..877ff75 100644 --- a/docs/labs/README.md +++ b/docs/labs/README.md @@ -140,8 +140,13 @@ Airflow; для нее вы сохраняете состояния заказо ### Тонкий анализ данных -Открывается после ступени 1. Знание о пользователе на дату и обогащение -отчета своим словарем. +Открывается после ступени 1. + +- [Когда хранилище узнало пользователя](optional/identity-over-time.md): + сохранить историю связей, сравнить расчеты на прежних сессиях и определить, + какие сведения нужны для воспроизведения результата. + +Обогащение отчета своим словарем регионов пока описано только в программе. ### Superset и BI @@ -157,6 +162,10 @@ Airflow; для нее вы сохраняете состояния заказо Открывается после ступени 1. Прием и хранение данных, раскладка по шардам, повторная обработка дня и обновление витрин. +- [Как обновлять витрину без полной пересборки](optional/incremental-daily-traffic.md): + продолжить работу о связях пользователей, пересчитать затронутые дни + и проверить повтор незавершенной загрузки. Повышенная сложность. + ### Что делать, когда стенд сломался Открывается после ступени 7. Поломки стенда и инструкции по восстановлению diff --git a/docs/labs/optional/identity-over-time.md b/docs/labs/optional/identity-over-time.md new file mode 100644 index 0000000..e10ace0 --- /dev/null +++ b/docs/labs/optional/identity-over-time.md @@ -0,0 +1,133 @@ +# Когда хранилище узнало пользователя + +**Чему учит:** различать время события и время появления связи в хранилище, +выбирать сведения для обогащения данных на дату расчета. + +Факультатив доступен после ступени 1. Понадобятся Airflow и DBeaver с +подключением `student` по [инструкции](../../../README.md#подключиться-из-dbeaver). +Наблюдения сохраняйте в `default` через одно подключение: ваши таблицы +локальны узлу. Если предложенное имя занято, выберите свое. + +На сайте посетителя узнают по куке `client_id`, а магазин знает покупателя +по `user_id`. Связь между ними можно установить, когда событие покупки +сопоставлено с заказом. Она позволяет обогатить и прежние визиты этой куки. +Вы проверите, как новый заказ меняет результат расчета за уже прошедший день. + +## Соберите связи с подтверждением + +Поставьте `world_live` на паузу, дождитесь завершения начатых прогонов +генерации и обработки. Запустите `etl_pipeline` и дождитесь успеха. +На время работы мир будете продвигать вручную; новые дни останутся в стенде. + +Напишите запрос по двум источникам: + +| Источник | Нужные поля | +|---|---| +| `dds.event_v` | `event_type`, `event_time`, `client_id`, `purchase_id` | +| `dds.order_v` | `order_id`, `user_id` | + +В результате нужна одна строка на пару `client_id`, `user_id` и самое раннее +время найденной покупки, подтверждающей эту пару. Назовите его `first_purchase_at`. +В событии `purchase` массив `purchase_id` содержит один номер заказа. +Статус заказа не ограничивайте: отмена покупки не меняет владельца куки. + +Источники распределены по разным ключам. Для соединения используйте +`GLOBAL INNER JOIN`, заключив выборки событий и заказов в подзапросы. +Устройство источников описано в [карте идентичностей](../../architecture/dds/identity.md). + +Проверьте уникальность пар и совпадение их полного набора с +`dds.identity_map_v`: одного равенства количества недостаточно. +Для одной пары покажите номера заказов и события, по которым нашли время. + +## Сохраните три наблюдения + +Создайте таблицу: + +```sql +CREATE TABLE default.lab_identity_history +( + observation_day Date, + client_id UInt64, + user_id UInt64, + first_purchase_at DateTime('Europe/Samara') +) +ENGINE = MergeTree +ORDER BY (observation_day, client_id, user_id); +``` + +Для метки наблюдения используйте последний обработанный день событий: + +```sql +SELECT max(event_date) AS observation_day FROM dds.event_v; +``` + +Обозначим начальную дату **H0**. Это модельная дата, а не календарный день +запуска Airflow. После успешного `etl_pipeline` сохраните полный результат +своего запроса через `INSERT INTO ... SELECT`, добавив эту дату. +Проверьте число строк и уникальность пар за H0. + +Затем дважды выполните последовательность: + +1. Запустите `world_next_day` с `days = 1` и дождитесь успеха. +2. Запустите `etl_pipeline` и дождитесь успеха. +3. Получите новую `observation_day`, сохраните полный набор связей и проверьте его. + +Получатся наблюдения **H0**, **H1** и **H2**. Каждую дату записывайте один раз: +обычный `MergeTree` не устраняет повторы вставки. Между этими действиями +не запускайте генерацию отдельно от наблюдения. + +## Сопоставьте два времени + +Сравните наборы пар в соседних наблюдениях. Для нескольких новых связей +покажите `client_id`, `user_id`, `first_purchase_at` и первую дату наблюдения, +в которой пара появилась. Найдите связь, покупка которой произошла до дня +ее обнаружения. + +Для одной такой связи вернитесь к заказу и событию. По данным и +[порядку доставки заказов](../../architecture/orders/ingestion.md) объясните, +почему связь отсутствовала в прежнем наблюдении. Был ли момент покупки +достаточным основанием считать ее уже известной хранилищу? + +У пары, которая присутствовала уже в H0, тоже получится минимальная дата +наблюдения. В этом случае она означает «пара была известна к началу опыта»: +истории до H0 у вас нет. Для новых пар ежедневные наблюдения устанавливают +интервал появления связи, а не точное время внутри него. + +## Пересчитайте прежние визиты + +Возьмите один и тот же набор сессий из `dds.session_v` за даты до H0 +включительно. Постройте два дневных расчета: + +- со связями, сохраненными в H0; +- со связями, сохраненными в H2. + +Для каждого дня посчитайте посетителей по разным `client_id` и известных +пользователей по разным `user_id`. Сессии без связи должны остаться в расчете, +но неизвестный пользователь не должен увеличивать число известных. +В соединении распределенных сессий с локальной историей используйте +`GLOBAL LEFT JOIN` и заранее отберите в истории одну дату наблюдения. + +Найдите день с разным числом известных пользователей. Проследите изменение +до конкретных связей и прежних сессий. Проверьте, что число посетителей +не изменилось и известные пользователи не превышают его. + +Напишите, какой вариант нужен для двух требований к витрине: + +- «пересчитать прошлые визиты с учетом всех сведений, доступных сейчас»; +- «воспроизвести расчет, сделанный при наблюдении H0». + +Для второго требования проверьте еще один способ: взять текущие связи, +оставив только `first_purchase_at` до конца H0. Совпадет ли полный набор +пар с сохраненным в H0? Найденным расхождением обоснуйте выбор истории +для воспроизводимого расчета. + +## Что должно получиться + +У вас есть запрос связей с подтверждающими покупками, три сохраненных +наблюдения и сравнение двух расчетов на одном наборе сессий. Вы можете +показать, когда новая связь стала видна в наблюдениях, и объяснить, +какие данные понадобятся для воспроизведения прежнего расчета. + +Сохраните SQL и таблицу наблюдений: они нужны в продолжении +[«Как обновлять витрину без полной пересборки»](incremental-daily-traffic.md). +Если продолжать не планируете, удалите свою таблицу через `DROP TABLE`. diff --git a/docs/labs/optional/incremental-daily-traffic.md b/docs/labs/optional/incremental-daily-traffic.md new file mode 100644 index 0000000..d09d34c --- /dev/null +++ b/docs/labs/optional/incremental-daily-traffic.md @@ -0,0 +1,146 @@ +# Как обновлять витрину без полной пересборки + +**Чему учит:** выбирать дни для пересчета по новым данным, проверять результат +и повторять незавершенную загрузку. + +Факультатив повышенной сложности продолжает работу +[«Когда хранилище узнало пользователя»](identity-over-time.md). +Понадобятся ее запрос сохранения связей и таблица `default.lab_identity_history`. +Работайте под `student` через то же подключение DBeaver. + +Стенд пересчитывает `dm.daily_traffic_v` целиком: новая связь может изменить +число известных пользователей в старом дне. Вы построите рядом свою витрину, +которая пересчитывает только затронутые дни и дает тот же результат. + +В опыте появляются новые дни сессий и новые связи. Мир стенда не отзывает +связи и не исправляет старые сессии при обычном продвижении времени. +Удаление данных, исправления старых сессий и пересоздание мира потребовали бы +другого способа обнаружения изменений. + +## Подготовьте витрину и журнал загрузок + +Создайте две локальные таблицы. Если имена заняты, выберите свои. + +```sql +CREATE TABLE default.lab_daily_traffic +( + report_date Date, + visitors UInt64, + known_users UInt64, + observation_day Date +) +ENGINE = ReplacingMergeTree(observation_day) +ORDER BY report_date; + +CREATE TABLE default.lab_traffic_runs +( + observation_day Date +) +ENGINE = MergeTree +ORDER BY observation_day; +``` + +В витрине одна актуальная строка на день. Для обновления вставляйте полный +новый результат дня с новой `observation_day`; читайте его с `FINAL`. +Движок выбирает максимальную `observation_day` для каждого `report_date`. +Старая версия может исчезнуть при слиянии: эта таблица хранит текущий расчет, +а не историю наблюдений. + +Журнал хранит даты успешно завершенных загрузок. Добавляйте дату только +после записи всех выбранных дней и успешной сверки. Последняя строка журнала +будет границей, от которой вы ищете новые связи в следующем прогоне. + +## Выполните первую загрузку целиком + +Убедитесь, что `world_live` стоит на паузе и нет активных прогонов генерации +и обработки. Запустите `etl_pipeline` и дождитесь успеха. Получите текущую +`observation_day` способом из первой работы. Если такого наблюдения еще нет +в вашей истории, сохраните его; уже сохраненную дату повторно не вставляйте. + +Напишите `INSERT INTO ... SELECT`, который рассчитывает все даты сессий из +`dds.session_v` со связями текущего наблюдения. Правила результата: + +| Колонка | Что считать | +|---|---| +| `report_date` | `session_date` | +| `visitors` | число разных `client_id` | +| `known_users` | число разных известных `user_id` | +| `observation_day` | дата текущего наблюдения | + +Сессии без известного пользователя сохраняйте. Несколько кук одного +пользователя не должны увеличивать `known_users`. Для соединения с локальной +историей связей используйте `GLOBAL LEFT JOIN`. + +Построчно сверьте свои `report_date`, `visitors`, `known_users` с +`dm.daily_traffic_v`. Сверка должна находить лишние и отсутствующие даты +и различия обоих показателей. Учебную витрину читайте с `FINAL`. +После совпадения запишите дату наблюдения в журнал один раз. + +## Выберите дни следующего пересчета + +Продвиньте мир на один день: `world_next_day` с `days = 1`, затем +`etl_pipeline`; после каждого запуска дождитесь успеха. Сохраните новое +наблюдение связей запросом из первой работы. + +Обозначим дату последней успешной загрузки из журнала **P**, текущую дату +наблюдения — **H**. До записи результата составьте запрос, возвращающий +разные даты для пересчета. Он должен учесть два изменения: + +- в сессиях появились дни, которых еще нет в вашей витрине; +- в карте появились пары, которых не было при P; у этих кук есть сессии, + в том числе за прежние дни. + +Сначала выведите даты и разберите одну старую дату: какая новая пара привела +к ее выбору и какие сессии с ней связаны? Рядом посчитайте, сколько дат +пересчитала бы полная загрузка. Зафиксируйте оба числа; равенство тоже +возможно, если новые связи затронули всю историю. + +Проверьте альтернативу: искать изменения по `first_purchase_at`, взяв покупки +только после конца P. Сравните полученный набор пар с разностью наблюдений. +Покажите пару, которую такое ограничение пропускает. + +## Обновите выбранные дни + +Напишите второй `INSERT INTO ... SELECT`. Для выбранных дат он должен +рассчитать все сессии с полной картой связей на H. Прибавление числа новых +связей к прежнему `known_users` не заменяет пересчет разных пользователей. + +Вставьте результаты с версией H. Полностью сверьте актуальную учебную витрину +со стендовой, включая дни, которые не пересчитывали. Только после совпадения +добавьте H в журнал. + +Без продвижения мира повторите выбор дат. После успешной загрузки новых дат +и пар быть не должно, а результат витрины должен остаться прежним. +Повторно записывать H в журнал не требуется. + +## Повторите незавершенную загрузку + +Снова запустите `world_next_day` с `days = 1`, затем `etl_pipeline`, +дожидаясь успеха каждого. Сохраните новое наблюдение связей. +Получите даты для пересчета, но запишите результат только за одну из +них. Остальные пока не загружайте и дату в журнал не добавляйте. +Так вы воспроизведете остановку между записями дней, не ломая стенд. + +Запустите свой обычный расчет еще раз, определив P по журналу. Он должен +довести загрузку до совпадения со стендовой витриной без очистки уже +записанных строк. После сверки запишите H в журнал. + +Объясните на этом опыте, почему последнее сохраненное наблюдение нельзя +автоматически считать загруженным в витрину. Что изменится, если записать H +в журнал до обновления витрины? + +## Что должно получиться + +Сохраните SQL первой загрузки, выбора дат, обновления и сверки. +После двух новых модельных дней ваша витрина совпадает со стендовой, +повтор без новых данных сохраняет результат, а незавершенную загрузку +можно продолжить без полной пересборки. + +Сравните цену двух вариантов: сколько дней вы пересчитывали и какие +дополнительные данные хранили. По этому опыту можно судить об объеме работы, +но не об ускорении: на небольшом стенде полный пересчет может быть дешевле. +В вашей реализации поиск связей пока читает всю историю покупок — +инкрементальным стал именно пересчет дневной витрины. + +После работы удалите свои `lab_daily_traffic`, `lab_traffic_runs` и +`lab_identity_history` через `DROP TABLE`, если они больше не нужны. diff --git a/docs/specs/2026-09-01-labs-program.md b/docs/specs/2026-09-01-labs-program.md index 369e665..406ef08 100644 --- a/docs/specs/2026-09-01-labs-program.md +++ b/docs/specs/2026-09-01-labs-program.md @@ -207,11 +207,27 @@ Открывается после ступени 1 основной программы. -- **Что мы знали о пользователе в тот день (#145).** Текущее знание и знание на - дату дают разные отчеты; сведения из будущего меняют прошлое. +- **[Когда хранилище узнало пользователя](../labs/optional/identity-over-time.md) (#145).** + Учащийся собирает связи с подтверждающими покупками и сохраняет три + ежедневных наблюдения. На одном наборе прежних сессий сравнивает расчет + с прежними и новыми связями, выбирает историю для воспроизведения расчета. - **Добавь названия регионов в отчет (#143).** Менти собирает свой словарь по образцу каталога и обогащает отчет через `dictGet`. +Работа о связях продолжается в инженерном разделе лабораторной про +инкрементальный `daily_traffic`. В обеих учащийся продвигает мир через +`world_next_day` и `etl_pipeline`, сохраняет наблюдения под `student` в +свои локальные таблицы `default` и сравнивает их SQL-запросами. + +Время первой подтверждающей покупки и время обнаружения связи разделены. +Минимум времени `purchase` не принят за границу появления новых связей: +заказ может поступить позже события. Нужные изменения определяются по +разности сохраненных наборов пар. Ежедневное наблюдение ограничивает момент +появления связи интервалом; для пар начального наблюдения более ранняя +история неизвестна. История сессий в этом опыте растет без исправлений +прошлых дней, а связи не отзываются — поэтому сохраненных наборов связей +достаточно для сравнения расчетов на прежних сессиях. + ### Superset и BI Открывается после ступени 2 основной программы. @@ -267,9 +283,28 @@ - **Что будет, если день заменился не на всех шардах.** Замена дня завершается отдельно на каждом шарде, поэтому целостность результата подтверждает отдельная сверка. -- **Как обновлять витрину без полной пересборки (#149) — повышенная - сложность.** Экономия на пересчете требует хранить историю изменений, которую - полная пересборка стирает. +- **[Как обновлять витрину без полной пересборки](../labs/optional/incremental-daily-traffic.md) + (#149) — повышенная сложность.** Продолжение работы о связях пользователей: + учащийся строит свою витрину, выбирает затронутые дни по новым сессиям + и связям, сверяет весь результат с `dm.daily_traffic_v`. Журнал успешных + загрузок позволяет повторить опыт с частично записанным результатом. + +Инкрементальная работа начинается с полной загрузки текущего состояния, +затем охватывает два новых модельных дня. Обновление собственной витрины +выполнено вставкой версий в `ReplacingMergeTree`, чтение — с `FINAL`. +Это оставляет права `student` прежними и не требует менять конвейер стенда. +Версия — дата наблюдения; ключ — день отчета. История связей хранится +отдельно от версий витрины. Выбор опирается на +[договор движка](https://clickhouse.com/docs/engines/table-engines/mergetree-family/replacingmergetree): +максимальная версия по ключу и удаление прежних версий при слияниях. +DDL, обновление вставкой и чтение с `FINAL` проверены под `student` +на ClickHouse 26.3.17.56. + +Полный пересчет остается эталоном, а экономия оценивается числом выбранных +дней. На малом объеме ускорение не обещано. Поиск связей продолжает читать +историю источников целиком; инкрементальным становится расчет дневной витрины. +Полная замена стендового ETL отклонена: для учебного сравнения достаточно +своей витрины и журнала рядом с работающим конвейером. ### Что делать, когда стенд сломался -- 2.54.0 From d6b172e511319d9a5d29c95768f51a361496e5fd Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Tue, 22 Sep 2026 06:15:04 -0400 Subject: [PATCH 2/2] =?UTF-8?q?docs(labs):=20=D1=83=D1=82=D0=BE=D1=87?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D1=8B=20=D1=84=D0=BE=D1=80=D0=BC=D1=83=D0=BB?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B8=20=D0=B8=20=D1=88=D0=B0?= =?UTF-8?q?=D0=B3=D0=B8=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81?= =?UTF-8?q?=D0=BE=20=D1=81=D0=B2=D1=8F=D0=B7=D1=8F=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Зачем: сделать действия в двух лабораторных понятными студенту без дополнительной расшифровки названий и временных обозначений. Что: переименована первая работа, абстрактные заголовки заменены конкретными действиями. Уточнены выбор копии списка связей и обновление даты из журнала перед повтором загрузки. Выводы требуют сравнения данных. Проверка: последовательное чтение глазами студента и независимая проверка по исходникам; 81 локальная ссылка, неизменность SQL-блоков, diff --check. --- docs/labs/README.md | 2 +- docs/labs/optional/identity-over-time.md | 114 ++++++++++-------- .../optional/incremental-daily-traffic.md | 100 +++++++-------- docs/specs/2026-09-01-labs-program.md | 2 +- 4 files changed, 115 insertions(+), 103 deletions(-) diff --git a/docs/labs/README.md b/docs/labs/README.md index 877ff75..7b82156 100644 --- a/docs/labs/README.md +++ b/docs/labs/README.md @@ -142,7 +142,7 @@ Airflow; для нее вы сохраняете состояния заказо Открывается после ступени 1. -- [Когда хранилище узнало пользователя](optional/identity-over-time.md): +- [Как новые заказы меняют отчет о посетителях](optional/identity-over-time.md): сохранить историю связей, сравнить расчеты на прежних сессиях и определить, какие сведения нужны для воспроизведения результата. diff --git a/docs/labs/optional/identity-over-time.md b/docs/labs/optional/identity-over-time.md index e10ace0..7140157 100644 --- a/docs/labs/optional/identity-over-time.md +++ b/docs/labs/optional/identity-over-time.md @@ -1,19 +1,21 @@ -# Когда хранилище узнало пользователя +# Как новые заказы меняют отчет о посетителях -**Чему учит:** различать время события и время появления связи в хранилище, -выбирать сведения для обогащения данных на дату расчета. +**Чему учит:** сохранять историю связей посетителей с покупателями +и использовать ее для воспроизведения прежнего расчета. Факультатив доступен после ступени 1. Понадобятся Airflow и DBeaver с подключением `student` по [инструкции](../../../README.md#подключиться-из-dbeaver). -Наблюдения сохраняйте в `default` через одно подключение: ваши таблицы -локальны узлу. Если предложенное имя занято, выберите свое. +Результаты запросов сохраняйте в `default` через одно подключение: +ваши таблицы будут доступны только на узле этого подключения. +Если предложенное имя таблицы занято, выберите свое. -На сайте посетителя узнают по куке `client_id`, а магазин знает покупателя -по `user_id`. Связь между ними можно установить, когда событие покупки -сопоставлено с заказом. Она позволяет обогатить и прежние визиты этой куки. -Вы проверите, как новый заказ меняет результат расчета за уже прошедший день. +В событиях сайта посетитель обозначен кукой `client_id`, а в заказах +покупатель — `user_id`. Сопоставив событие покупки с заказом, можно связать +эти идентификаторы. Тогда и к прежним визитам этой куки можно добавить +пользователя магазина. Вы проверите, как появление такой связи меняет число +известных пользователей в отчете за прошедший день. -## Соберите связи с подтверждением +## Сопоставьте посетителей с покупателями Поставьте `world_live` на паузу, дождитесь завершения начатых прогонов генерации и обработки. Запустите `etl_pipeline` и дождитесь успеха. @@ -26,8 +28,9 @@ | `dds.event_v` | `event_type`, `event_time`, `client_id`, `purchase_id` | | `dds.order_v` | `order_id`, `user_id` | -В результате нужна одна строка на пару `client_id`, `user_id` и самое раннее -время найденной покупки, подтверждающей эту пару. Назовите его `first_purchase_at`. +В результате нужна одна строка на пару `client_id`, `user_id`. Для каждой пары +найдите самое раннее время события покупки, которое удалось сопоставить +с заказом. Назовите эту колонку `first_purchase_at`. В событии `purchase` массив `purchase_id` содержит один номер заказа. Статус заказа не ограничивайте: отмена покупки не меняет владельца куки. @@ -37,9 +40,10 @@ Проверьте уникальность пар и совпадение их полного набора с `dds.identity_map_v`: одного равенства количества недостаточно. -Для одной пары покажите номера заказов и события, по которым нашли время. +Для одной пары покажите номера заказов и события, по которым получили +`first_purchase_at`. -## Сохраните три наблюдения +## Сохраните список связей после каждого модельного дня Создайте таблицу: @@ -55,16 +59,19 @@ ENGINE = MergeTree ORDER BY (observation_day, client_id, user_id); ``` -Для метки наблюдения используйте последний обработанный день событий: +Каждая копия списка должна содержать все найденные пары. В колонку +`observation_day` запишите последний обработанный день событий: ```sql SELECT max(event_date) AS observation_day FROM dds.event_v; ``` -Обозначим начальную дату **H0**. Это модельная дата, а не календарный день -запуска Airflow. После успешного `etl_pipeline` сохраните полный результат -своего запроса через `INSERT INTO ... SELECT`, добавив эту дату. -Проверьте число строк и уникальность пар за H0. +Обозначим полученную дату **H0**. Она показывает, после обработки какого +модельного дня сохранен список; календарная дата запуска Airflow здесь +не используется. Сохраните полный результат своего запроса через +`INSERT INTO ... SELECT`, добавив H0 в `observation_day` каждой строки. +Сверьте число записанных строк с результатом исходного запроса и проверьте +уникальность пар для этой даты. Затем дважды выполните последовательность: @@ -72,62 +79,63 @@ SELECT max(event_date) AS observation_day FROM dds.event_v; 2. Запустите `etl_pipeline` и дождитесь успеха. 3. Получите новую `observation_day`, сохраните полный набор связей и проверьте его. -Получатся наблюдения **H0**, **H1** и **H2**. Каждую дату записывайте один раз: -обычный `MergeTree` не устраняет повторы вставки. Между этими действиями -не запускайте генерацию отдельно от наблюдения. +Получатся три копии списка: после дней **H0**, **H1** и **H2**. +Для каждой даты сохраняйте список один раз: обычный `MergeTree` +не устраняет повторы вставки. Сохраните и проверьте список перед тем, +как запускать следующий модельный день. -## Сопоставьте два времени +## Сравните дату покупки с датой появления связи -Сравните наборы пар в соседних наблюдениях. Для нескольких новых связей -покажите `client_id`, `user_id`, `first_purchase_at` и первую дату наблюдения, -в которой пара появилась. Найдите связь, покупка которой произошла до дня -ее обнаружения. +Сравните списки за соседние даты: H0 с H1, затем H1 с H2. Найдите пары, +которых не было в прежнем списке. Для нескольких таких пар покажите +`client_id`, `user_id`, `first_purchase_at` и первую `observation_day`, +в которой пара появилась. Найдите пример, где покупка произошла раньше +этой даты. Для одной такой связи вернитесь к заказу и событию. По данным и [порядку доставки заказов](../../architecture/orders/ingestion.md) объясните, -почему связь отсутствовала в прежнем наблюдении. Был ли момент покупки -достаточным основанием считать ее уже известной хранилищу? +почему пары не было в прежнем списке. Какие данные понадобились для ее +появления и когда они стали доступны? -У пары, которая присутствовала уже в H0, тоже получится минимальная дата -наблюдения. В этом случае она означает «пара была известна к началу опыта»: -истории до H0 у вас нет. Для новых пар ежедневные наблюдения устанавливают -интервал появления связи, а не точное время внутри него. +Для пары, которая присутствовала уже в H0, нельзя определить дату появления: +у вас нет более ранних копий списка. Для новой пары можно установить +интервал между двумя сохранениями, но не точное время внутри него. -## Пересчитайте прежние визиты +## Пересчитайте отчет за прежние дни Возьмите один и тот же набор сессий из `dds.session_v` за даты до H0 включительно. Постройте два дневных расчета: -- со связями, сохраненными в H0; -- со связями, сохраненными в H2. +- со списком связей на H0; +- со списком связей на H2. Для каждого дня посчитайте посетителей по разным `client_id` и известных пользователей по разным `user_id`. Сессии без связи должны остаться в расчете, но неизвестный пользователь не должен увеличивать число известных. -В соединении распределенных сессий с локальной историей используйте -`GLOBAL LEFT JOIN` и заранее отберите в истории одну дату наблюдения. +В соединении сессий с вашей таблицей используйте `GLOBAL LEFT JOIN`. +В таблице связей заранее отберите одну `observation_day`, чтобы копии +списка за разные дни не смешались. Найдите день с разным числом известных пользователей. Проследите изменение до конкретных связей и прежних сессий. Проверьте, что число посетителей -не изменилось и известные пользователи не превышают его. +не изменилось, а число известных пользователей не больше числа посетителей. -Напишите, какой вариант нужен для двух требований к витрине: - -- «пересчитать прошлые визиты с учетом всех сведений, доступных сейчас»; -- «воспроизвести расчет, сделанный при наблюдении H0». - -Для второго требования проверьте еще один способ: взять текущие связи, -оставив только `first_purchase_at` до конца H0. Совпадет ли полный набор -пар с сохраненным в H0? Найденным расхождением обоснуйте выбор истории -для воспроизводимого расчета. +Теперь проверьте, можно ли воспроизвести расчет на H0 без сохраненного +списка за эту дату. Возьмите список на H2 и оставьте только пары, +у которых `first_purchase_at` приходится на H0 или более ранний день. +Сравните полный набор пар с копией на H0. Выведите пары, которые есть +только в одном из списков. +На примере найденных различий объясните, какие данные нужно сохранять, +чтобы повторить прежний расчет. ## Что должно получиться -У вас есть запрос связей с подтверждающими покупками, три сохраненных -наблюдения и сравнение двух расчетов на одном наборе сессий. Вы можете -показать, когда новая связь стала видна в наблюдениях, и объяснить, -какие данные понадобятся для воспроизведения прежнего расчета. +У вас есть запрос сопоставления посетителей с покупателями, три сохраненных +списка связей и сравнение двух расчетов на одном наборе сессий. На конкретных +парах вы можете показать разницу между датой покупки и датой появления пары +в сохраненном списке, а также объяснить, что нужно для повторения прежнего +расчета. -Сохраните SQL и таблицу наблюдений: они нужны в продолжении +Сохраните SQL и таблицу `lab_identity_history`: они нужны в продолжении [«Как обновлять витрину без полной пересборки»](incremental-daily-traffic.md). Если продолжать не планируете, удалите свою таблицу через `DROP TABLE`. diff --git a/docs/labs/optional/incremental-daily-traffic.md b/docs/labs/optional/incremental-daily-traffic.md index d09d34c..195ee06 100644 --- a/docs/labs/optional/incremental-daily-traffic.md +++ b/docs/labs/optional/incremental-daily-traffic.md @@ -4,20 +4,21 @@ и повторять незавершенную загрузку. Факультатив повышенной сложности продолжает работу -[«Когда хранилище узнало пользователя»](identity-over-time.md). -Понадобятся ее запрос сохранения связей и таблица `default.lab_identity_history`. +[«Как новые заказы меняют отчет о посетителях»](identity-over-time.md). +Понадобятся ее запрос сохранения списка связей и таблица `default.lab_identity_history`. Работайте под `student` через то же подключение DBeaver. Стенд пересчитывает `dm.daily_traffic_v` целиком: новая связь может изменить число известных пользователей в старом дне. Вы построите рядом свою витрину, которая пересчитывает только затронутые дни и дает тот же результат. -В опыте появляются новые дни сессий и новые связи. Мир стенда не отзывает -связи и не исправляет старые сессии при обычном продвижении времени. +При обычном продвижении модельного времени появляются новые дни сессий +и новые связи. Уже найденные пары сохраняются, а сессии прежних дней +не меняются. Удаление данных, исправления старых сессий и пересоздание мира потребовали бы другого способа обнаружения изменений. -## Подготовьте витрину и журнал загрузок +## Создайте витрину и журнал успешных загрузок Создайте две локальные таблицы. Если имена заняты, выберите свои. @@ -41,93 +42,96 @@ ORDER BY observation_day; ``` В витрине одна актуальная строка на день. Для обновления вставляйте полный -новый результат дня с новой `observation_day`; читайте его с `FINAL`. -Движок выбирает максимальную `observation_day` для каждого `report_date`. -Старая версия может исчезнуть при слиянии: эта таблица хранит текущий расчет, -а не историю наблюдений. +новый результат дня с новой `observation_day`. Читайте витрину с `FINAL`: +для каждого `report_date` останется строка с максимальной `observation_day`. +Старые версии могут исчезнуть при слиянии, +поэтому для воспроизведения прежнего расчета нужна сохраненная история связей. -Журнал хранит даты успешно завершенных загрузок. Добавляйте дату только -после записи всех выбранных дней и успешной сверки. Последняя строка журнала -будет границей, от которой вы ищете новые связи в следующем прогоне. +В журнал записывайте `observation_day` списка связей, использованного +для расчета. Добавляйте дату только после записи всех выбранных дней +и успешной сверки. Максимальная дата в журнале укажет, какой список +использовался в последней завершенной загрузке. ## Выполните первую загрузку целиком Убедитесь, что `world_live` стоит на паузе и нет активных прогонов генерации и обработки. Запустите `etl_pipeline` и дождитесь успеха. Получите текущую -`observation_day` способом из первой работы. Если такого наблюдения еще нет -в вашей истории, сохраните его; уже сохраненную дату повторно не вставляйте. +`observation_day` способом из первой работы. Если списка за эту дату еще нет +в вашей истории, сохраните его. Повторно вставлять уже сохраненный список +не требуется. Напишите `INSERT INTO ... SELECT`, который рассчитывает все даты сессий из -`dds.session_v` со связями текущего наблюдения. Правила результата: +`dds.session_v` со списком связей на текущую дату. Правила результата: | Колонка | Что считать | |---|---| | `report_date` | `session_date` | | `visitors` | число разных `client_id` | | `known_users` | число разных известных `user_id` | -| `observation_day` | дата текущего наблюдения | +| `observation_day` | дата выбранной копии списка связей | -Сессии без известного пользователя сохраняйте. Несколько кук одного -пользователя не должны увеличивать `known_users`. Для соединения с локальной -историей связей используйте `GLOBAL LEFT JOIN`. +Сессии без известного пользователя сохраняйте в расчете. Несколько кук одного +пользователя не должны увеличивать `known_users`. Для соединения с вашей +таблицей связей используйте `GLOBAL LEFT JOIN`, отобрав одну `observation_day`. Построчно сверьте свои `report_date`, `visitors`, `known_users` с `dm.daily_traffic_v`. Сверка должна находить лишние и отсутствующие даты и различия обоих показателей. Учебную витрину читайте с `FINAL`. После совпадения запишите дату наблюдения в журнал один раз. -## Выберите дни следующего пересчета +## Найдите дни, которые нужно пересчитать Продвиньте мир на один день: `world_next_day` с `days = 1`, затем -`etl_pipeline`; после каждого запуска дождитесь успеха. Сохраните новое -наблюдение связей запросом из первой работы. +`etl_pipeline`; после каждого запуска дождитесь успеха. Сохраните новый +список связей запросом из первой работы. -Обозначим дату последней успешной загрузки из журнала **P**, текущую дату -наблюдения — **H**. До записи результата составьте запрос, возвращающий -разные даты для пересчета. Он должен учесть два изменения: +Обозначим максимальную `observation_day` в журнале **P**, а дату нового +списка связей — **H**. До записи результата составьте запрос, возвращающий +даты для пересчета без повторов. Он должен учесть два изменения: - в сессиях появились дни, которых еще нет в вашей витрине; -- в карте появились пары, которых не было при P; у этих кук есть сессии, - в том числе за прежние дни. +- в списке на H появились пары, которых не было в списке на P; у этих кук + есть сессии, в том числе за прежние дни. -Сначала выведите даты и разберите одну старую дату: какая новая пара привела -к ее выбору и какие сессии с ней связаны? Рядом посчитайте, сколько дат -пересчитала бы полная загрузка. Зафиксируйте оба числа; равенство тоже -возможно, если новые связи затронули всю историю. +Сначала выведите даты и выберите один прежний день: какая новая пара +потребовала его пересчета и какие сессии с ней связаны? Рядом посчитайте, +сколько дат пересчитала бы полная загрузка. Зафиксируйте оба числа; +равенство тоже возможно, если новые связи затронули всю историю. -Проверьте альтернативу: искать изменения по `first_purchase_at`, взяв покупки -только после конца P. Сравните полученный набор пар с разностью наблюдений. -Покажите пару, которую такое ограничение пропускает. +Для сравнения выберите из списка на H пары, у которых день покупки +по `first_purchase_at` позже P. Совпадет ли результат с новыми парами, +найденными сравнением списков? Покажите пару, которую ограничение по дате +покупки пропускает. ## Обновите выбранные дни Напишите второй `INSERT INTO ... SELECT`. Для выбранных дат он должен -рассчитать все сессии с полной картой связей на H. Прибавление числа новых +рассчитать все сессии с полным списком связей на H. Прибавление числа новых связей к прежнему `known_users` не заменяет пересчет разных пользователей. Вставьте результаты с версией H. Полностью сверьте актуальную учебную витрину со стендовой, включая дни, которые не пересчитывали. Только после совпадения добавьте H в журнал. -Без продвижения мира повторите выбор дат. После успешной загрузки новых дат -и пар быть не должно, а результат витрины должен остаться прежним. +Без продвижения мира снова определите P по журналу и повторите выбор дат. +Запрос должен вернуть пустой результат, а витрина — остаться прежней. Повторно записывать H в журнал не требуется. -## Повторите незавершенную загрузку +## Проверьте повторный запуск после частичной загрузки Снова запустите `world_next_day` с `days = 1`, затем `etl_pipeline`, -дожидаясь успеха каждого. Сохраните новое наблюдение связей. +дожидаясь успеха каждого. Сохраните новый список связей с его датой H. Получите даты для пересчета, но запишите результат только за одну из них. Остальные пока не загружайте и дату в журнал не добавляйте. Так вы воспроизведете остановку между записями дней, не ломая стенд. -Запустите свой обычный расчет еще раз, определив P по журналу. Он должен -довести загрузку до совпадения со стендовой витриной без очистки уже -записанных строк. После сверки запишите H в журнал. +Снова определите P по журналу, выполните запрос выбора дат и пересчет +этих дней. Проверьте, что результат совпал со стендовой витриной без очистки +уже записанных строк. После сверки запишите H в журнал. -Объясните на этом опыте, почему последнее сохраненное наблюдение нельзя -автоматически считать загруженным в витрину. Что изменится, если записать H -в журнал до обновления витрины? +Объясните на этом опыте, почему сохранение нового списка связей еще +не означает, что витрина обновлена. Что изменится, если записать H в журнал +до обновления витрины? ## Что должно получиться @@ -136,9 +140,9 @@ ORDER BY observation_day; повтор без новых данных сохраняет результат, а незавершенную загрузку можно продолжить без полной пересборки. -Сравните цену двух вариантов: сколько дней вы пересчитывали и какие -дополнительные данные хранили. По этому опыту можно судить об объеме работы, -но не об ускорении: на небольшом стенде полный пересчет может быть дешевле. +Сравните число пересчитанных дней с полной загрузкой и перечислите, +какие дополнительные данные пришлось хранить. Так можно оценить объем работы. +На небольшом стенде полный пересчет может быть дешевле. В вашей реализации поиск связей пока читает всю историю покупок — инкрементальным стал именно пересчет дневной витрины. diff --git a/docs/specs/2026-09-01-labs-program.md b/docs/specs/2026-09-01-labs-program.md index 406ef08..ed6e519 100644 --- a/docs/specs/2026-09-01-labs-program.md +++ b/docs/specs/2026-09-01-labs-program.md @@ -207,7 +207,7 @@ Открывается после ступени 1 основной программы. -- **[Когда хранилище узнало пользователя](../labs/optional/identity-over-time.md) (#145).** +- **[Как новые заказы меняют отчет о посетителях](../labs/optional/identity-over-time.md) (#145).** Учащийся собирает связи с подтверждающими покупками и сохраняет три ежедневных наблюдения. На одном наборе прежних сессий сравнивает расчет с прежними и новыми связями, выбирает историю для воспроизведения расчета. -- 2.54.0