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..7b82156 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..7140157 --- /dev/null +++ b/docs/labs/optional/identity-over-time.md @@ -0,0 +1,141 @@ +# Как новые заказы меняют отчет о посетителях + +**Чему учит:** сохранять историю связей посетителей с покупателями +и использовать ее для воспроизведения прежнего расчета. + +Факультатив доступен после ступени 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`: одного равенства количества недостаточно. +Для одной пары покажите номера заказов и события, по которым получили +`first_purchase_at`. + +## Сохраните список связей после каждого модельного дня + +Создайте таблицу: + +```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); +``` + +Каждая копия списка должна содержать все найденные пары. В колонку +`observation_day` запишите последний обработанный день событий: + +```sql +SELECT max(event_date) AS observation_day FROM dds.event_v; +``` + +Обозначим полученную дату **H0**. Она показывает, после обработки какого +модельного дня сохранен список; календарная дата запуска Airflow здесь +не используется. Сохраните полный результат своего запроса через +`INSERT INTO ... SELECT`, добавив H0 в `observation_day` каждой строки. +Сверьте число записанных строк с результатом исходного запроса и проверьте +уникальность пар для этой даты. + +Затем дважды выполните последовательность: + +1. Запустите `world_next_day` с `days = 1` и дождитесь успеха. +2. Запустите `etl_pipeline` и дождитесь успеха. +3. Получите новую `observation_day`, сохраните полный набор связей и проверьте его. + +Получатся три копии списка: после дней **H0**, **H1** и **H2**. +Для каждой даты сохраняйте список один раз: обычный `MergeTree` +не устраняет повторы вставки. Сохраните и проверьте список перед тем, +как запускать следующий модельный день. + +## Сравните дату покупки с датой появления связи + +Сравните списки за соседние даты: H0 с H1, затем H1 с H2. Найдите пары, +которых не было в прежнем списке. Для нескольких таких пар покажите +`client_id`, `user_id`, `first_purchase_at` и первую `observation_day`, +в которой пара появилась. Найдите пример, где покупка произошла раньше +этой даты. + +Для одной такой связи вернитесь к заказу и событию. По данным и +[порядку доставки заказов](../../architecture/orders/ingestion.md) объясните, +почему пары не было в прежнем списке. Какие данные понадобились для ее +появления и когда они стали доступны? + +Для пары, которая присутствовала уже в H0, нельзя определить дату появления: +у вас нет более ранних копий списка. Для новой пары можно установить +интервал между двумя сохранениями, но не точное время внутри него. + +## Пересчитайте отчет за прежние дни + +Возьмите один и тот же набор сессий из `dds.session_v` за даты до H0 +включительно. Постройте два дневных расчета: + +- со списком связей на H0; +- со списком связей на H2. + +Для каждого дня посчитайте посетителей по разным `client_id` и известных +пользователей по разным `user_id`. Сессии без связи должны остаться в расчете, +но неизвестный пользователь не должен увеличивать число известных. +В соединении сессий с вашей таблицей используйте `GLOBAL LEFT JOIN`. +В таблице связей заранее отберите одну `observation_day`, чтобы копии +списка за разные дни не смешались. + +Найдите день с разным числом известных пользователей. Проследите изменение +до конкретных связей и прежних сессий. Проверьте, что число посетителей +не изменилось, а число известных пользователей не больше числа посетителей. + +Теперь проверьте, можно ли воспроизвести расчет на H0 без сохраненного +списка за эту дату. Возьмите список на H2 и оставьте только пары, +у которых `first_purchase_at` приходится на H0 или более ранний день. +Сравните полный набор пар с копией на H0. Выведите пары, которые есть +только в одном из списков. +На примере найденных различий объясните, какие данные нужно сохранять, +чтобы повторить прежний расчет. + +## Что должно получиться + +У вас есть запрос сопоставления посетителей с покупателями, три сохраненных +списка связей и сравнение двух расчетов на одном наборе сессий. На конкретных +парах вы можете показать разницу между датой покупки и датой появления пары +в сохраненном списке, а также объяснить, что нужно для повторения прежнего +расчета. + +Сохраните 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 new file mode 100644 index 0000000..195ee06 --- /dev/null +++ b/docs/labs/optional/incremental-daily-traffic.md @@ -0,0 +1,150 @@ +# Как обновлять витрину без полной пересборки + +**Чему учит:** выбирать дни для пересчета по новым данным, проверять результат +и повторять незавершенную загрузку. + +Факультатив повышенной сложности продолжает работу +[«Как новые заказы меняют отчет о посетителях»](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`: +для каждого `report_date` останется строка с максимальной `observation_day`. +Старые версии могут исчезнуть при слиянии, +поэтому для воспроизведения прежнего расчета нужна сохраненная история связей. + +В журнал записывайте `observation_day` списка связей, использованного +для расчета. Добавляйте дату только после записи всех выбранных дней +и успешной сверки. Максимальная дата в журнале укажет, какой список +использовался в последней завершенной загрузке. + +## Выполните первую загрузку целиком + +Убедитесь, что `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`, отобрав одну `observation_day`. + +Построчно сверьте свои `report_date`, `visitors`, `known_users` с +`dm.daily_traffic_v`. Сверка должна находить лишние и отсутствующие даты +и различия обоих показателей. Учебную витрину читайте с `FINAL`. +После совпадения запишите дату наблюдения в журнал один раз. + +## Найдите дни, которые нужно пересчитать + +Продвиньте мир на один день: `world_next_day` с `days = 1`, затем +`etl_pipeline`; после каждого запуска дождитесь успеха. Сохраните новый +список связей запросом из первой работы. + +Обозначим максимальную `observation_day` в журнале **P**, а дату нового +списка связей — **H**. До записи результата составьте запрос, возвращающий +даты для пересчета без повторов. Он должен учесть два изменения: + +- в сессиях появились дни, которых еще нет в вашей витрине; +- в списке на H появились пары, которых не было в списке на P; у этих кук + есть сессии, в том числе за прежние дни. + +Сначала выведите даты и выберите один прежний день: какая новая пара +потребовала его пересчета и какие сессии с ней связаны? Рядом посчитайте, +сколько дат пересчитала бы полная загрузка. Зафиксируйте оба числа; +равенство тоже возможно, если новые связи затронули всю историю. + +Для сравнения выберите из списка на H пары, у которых день покупки +по `first_purchase_at` позже P. Совпадет ли результат с новыми парами, +найденными сравнением списков? Покажите пару, которую ограничение по дате +покупки пропускает. + +## Обновите выбранные дни + +Напишите второй `INSERT INTO ... SELECT`. Для выбранных дат он должен +рассчитать все сессии с полным списком связей на H. Прибавление числа новых +связей к прежнему `known_users` не заменяет пересчет разных пользователей. + +Вставьте результаты с версией H. Полностью сверьте актуальную учебную витрину +со стендовой, включая дни, которые не пересчитывали. Только после совпадения +добавьте H в журнал. + +Без продвижения мира снова определите P по журналу и повторите выбор дат. +Запрос должен вернуть пустой результат, а витрина — остаться прежней. +Повторно записывать H в журнал не требуется. + +## Проверьте повторный запуск после частичной загрузки + +Снова запустите `world_next_day` с `days = 1`, затем `etl_pipeline`, +дожидаясь успеха каждого. Сохраните новый список связей с его датой H. +Получите даты для пересчета, но запишите результат только за одну из +них. Остальные пока не загружайте и дату в журнал не добавляйте. +Так вы воспроизведете остановку между записями дней, не ломая стенд. + +Снова определите 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..ed6e519 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 отклонена: для учебного сравнения достаточно +своей витрины и журнала рядом с работающим конвейером. ### Что делать, когда стенд сломался