docs(labs): уточнены формулировки и шаги работы со связями

Зачем: сделать действия в двух лабораторных понятными студенту без
дополнительной расшифровки названий и временных обозначений.

Что: переименована первая работа, абстрактные заголовки заменены
конкретными действиями. Уточнены выбор копии списка связей и обновление
даты из журнала перед повтором загрузки. Выводы требуют сравнения данных.

Проверка: последовательное чтение глазами студента и независимая проверка
по исходникам; 81 локальная ссылка, неизменность SQL-блоков, diff --check.
This commit is contained in:
ddadmin committed 2026-09-22 06:15:04 -04:00
1 parent b839a671a1
commit d6b172e511
4 files changed
+115 -103

No files matched your search

+1 -1
View File
@@ -142,7 +142,7 @@ Airflow; для нее вы сохраняете состояния заказо
Открывается после ступени 1. Открывается после ступени 1.
- [Когда хранилище узнало пользователя](optional/identity-over-time.md): - [Как новые заказы меняют отчет о посетителях](optional/identity-over-time.md):
сохранить историю связей, сравнить расчеты на прежних сессиях и определить, сохранить историю связей, сравнить расчеты на прежних сессиях и определить,
какие сведения нужны для воспроизведения результата. какие сведения нужны для воспроизведения результата.
+61 -53
View File
@@ -1,19 +1,21 @@
# Когда хранилище узнало пользователя # Как новые заказы меняют отчет о посетителях
**Чему учит:** различать время события и время появления связи в хранилище, **Чему учит:** сохранять историю связей посетителей с покупателями
выбирать сведения для обогащения данных на дату расчета. и использовать ее для воспроизведения прежнего расчета.
Факультатив доступен после ступени 1. Понадобятся Airflow и DBeaver с Факультатив доступен после ступени 1. Понадобятся Airflow и DBeaver с
подключением `student` по [инструкции](../../../README.md#подключиться-из-dbeaver). подключением `student` по [инструкции](../../../README.md#подключиться-из-dbeaver).
Наблюдения сохраняйте в `default` через одно подключение: ваши таблицы Результаты запросов сохраняйте в `default` через одно подключение:
локальны узлу. Если предложенное имя занято, выберите свое. ваши таблицы будут доступны только на узле этого подключения.
Если предложенное имя таблицы занято, выберите свое.
На сайте посетителя узнают по куке `client_id`, а магазин знает покупателя В событиях сайта посетитель обозначен кукой `client_id`, а в заказах
по `user_id`. Связь между ними можно установить, когда событие покупки покупатель — `user_id`. Сопоставив событие покупки с заказом, можно связать
сопоставлено с заказом. Она позволяет обогатить и прежние визиты этой куки. эти идентификаторы. Тогда и к прежним визитам этой куки можно добавить
Вы проверите, как новый заказ меняет результат расчета за уже прошедший день. пользователя магазина. Вы проверите, как появление такой связи меняет число
известных пользователей в отчете за прошедший день.
## Соберите связи с подтверждением ## Сопоставьте посетителей с покупателями
Поставьте `world_live` на паузу, дождитесь завершения начатых прогонов Поставьте `world_live` на паузу, дождитесь завершения начатых прогонов
генерации и обработки. Запустите `etl_pipeline` и дождитесь успеха. генерации и обработки. Запустите `etl_pipeline` и дождитесь успеха.
@@ -26,8 +28,9 @@
| `dds.event_v` | `event_type`, `event_time`, `client_id`, `purchase_id` | | `dds.event_v` | `event_type`, `event_time`, `client_id`, `purchase_id` |
| `dds.order_v` | `order_id`, `user_id` | | `dds.order_v` | `order_id`, `user_id` |
В результате нужна одна строка на пару `client_id`, `user_id` и самое раннее В результате нужна одна строка на пару `client_id`, `user_id`. Для каждой пары
время найденной покупки, подтверждающей эту пару. Назовите его `first_purchase_at`. найдите самое раннее время события покупки, которое удалось сопоставить
с заказом. Назовите эту колонку `first_purchase_at`.
В событии `purchase` массив `purchase_id` содержит один номер заказа. В событии `purchase` массив `purchase_id` содержит один номер заказа.
Статус заказа не ограничивайте: отмена покупки не меняет владельца куки. Статус заказа не ограничивайте: отмена покупки не меняет владельца куки.
@@ -37,9 +40,10 @@
Проверьте уникальность пар и совпадение их полного набора с Проверьте уникальность пар и совпадение их полного набора с
`dds.identity_map_v`: одного равенства количества недостаточно. `dds.identity_map_v`: одного равенства количества недостаточно.
Для одной пары покажите номера заказов и события, по которым нашли время. Для одной пары покажите номера заказов и события, по которым получили
`first_purchase_at`.
## Сохраните три наблюдения ## Сохраните список связей после каждого модельного дня
Создайте таблицу: Создайте таблицу:
@@ -55,16 +59,19 @@ ENGINE = MergeTree
ORDER BY (observation_day, client_id, user_id); ORDER BY (observation_day, client_id, user_id);
``` ```
Для метки наблюдения используйте последний обработанный день событий: Каждая копия списка должна содержать все найденные пары. В колонку
`observation_day` запишите последний обработанный день событий:
```sql ```sql
SELECT max(event_date) AS observation_day FROM dds.event_v; SELECT max(event_date) AS observation_day FROM dds.event_v;
``` ```
Обозначим начальную дату **H0**. Это модельная дата, а не календарный день Обозначим полученную дату **H0**. Она показывает, после обработки какого
запуска Airflow. После успешного `etl_pipeline` сохраните полный результат модельного дня сохранен список; календарная дата запуска Airflow здесь
своего запроса через `INSERT INTO ... SELECT`, добавив эту дату. не используется. Сохраните полный результат своего запроса через
Проверьте число строк и уникальность пар за H0. `INSERT INTO ... SELECT`, добавив H0 в `observation_day` каждой строки.
Сверьте число записанных строк с результатом исходного запроса и проверьте
уникальность пар для этой даты.
Затем дважды выполните последовательность: Затем дважды выполните последовательность:
@@ -72,62 +79,63 @@ SELECT max(event_date) AS observation_day FROM dds.event_v;
2. Запустите `etl_pipeline` и дождитесь успеха. 2. Запустите `etl_pipeline` и дождитесь успеха.
3. Получите новую `observation_day`, сохраните полный набор связей и проверьте его. 3. Получите новую `observation_day`, сохраните полный набор связей и проверьте его.
Получатся наблюдения **H0**, **H1** и **H2**. Каждую дату записывайте один раз: Получатся три копии списка: после дней **H0**, **H1** и **H2**.
обычный `MergeTree` не устраняет повторы вставки. Между этими действиями Для каждой даты сохраняйте список один раз: обычный `MergeTree`
не запускайте генерацию отдельно от наблюдения. не устраняет повторы вставки. Сохраните и проверьте список перед тем,
как запускать следующий модельный день.
## Сопоставьте два времени ## Сравните дату покупки с датой появления связи
Сравните наборы пар в соседних наблюдениях. Для нескольких новых связей Сравните списки за соседние даты: H0 с H1, затем H1 с H2. Найдите пары,
покажите `client_id`, `user_id`, `first_purchase_at` и первую дату наблюдения, которых не было в прежнем списке. Для нескольких таких пар покажите
в которой пара появилась. Найдите связь, покупка которой произошла до дня `client_id`, `user_id`, `first_purchase_at` и первую `observation_day`,
ее обнаружения. в которой пара появилась. Найдите пример, где покупка произошла раньше
этой даты.
Для одной такой связи вернитесь к заказу и событию. По данным и Для одной такой связи вернитесь к заказу и событию. По данным и
[порядку доставки заказов](../../architecture/orders/ingestion.md) объясните, [порядку доставки заказов](../../architecture/orders/ingestion.md) объясните,
почему связь отсутствовала в прежнем наблюдении. Был ли момент покупки почему пары не было в прежнем списке. Какие данные понадобились для ее
достаточным основанием считать ее уже известной хранилищу? появления и когда они стали доступны?
У пары, которая присутствовала уже в H0, тоже получится минимальная дата Для пары, которая присутствовала уже в H0, нельзя определить дату появления:
наблюдения. В этом случае она означает «пара была известна к началу опыта»: у вас нет более ранних копий списка. Для новой пары можно установить
истории до H0 у вас нет. Для новых пар ежедневные наблюдения устанавливают интервал между двумя сохранениями, но не точное время внутри него.
интервал появления связи, а не точное время внутри него.
## Пересчитайте прежние визиты ## Пересчитайте отчет за прежние дни
Возьмите один и тот же набор сессий из `dds.session_v` за даты до H0 Возьмите один и тот же набор сессий из `dds.session_v` за даты до H0
включительно. Постройте два дневных расчета: включительно. Постройте два дневных расчета:
- со связями, сохраненными в H0; - со списком связей на H0;
- со связями, сохраненными в H2. - со списком связей на H2.
Для каждого дня посчитайте посетителей по разным `client_id` и известных Для каждого дня посчитайте посетителей по разным `client_id` и известных
пользователей по разным `user_id`. Сессии без связи должны остаться в расчете, пользователей по разным `user_id`. Сессии без связи должны остаться в расчете,
но неизвестный пользователь не должен увеличивать число известных. но неизвестный пользователь не должен увеличивать число известных.
В соединении распределенных сессий с локальной историей используйте В соединении сессий с вашей таблицей используйте `GLOBAL LEFT JOIN`.
`GLOBAL LEFT JOIN` и заранее отберите в истории одну дату наблюдения. В таблице связей заранее отберите одну `observation_day`, чтобы копии
списка за разные дни не смешались.
Найдите день с разным числом известных пользователей. Проследите изменение Найдите день с разным числом известных пользователей. Проследите изменение
до конкретных связей и прежних сессий. Проверьте, что число посетителей до конкретных связей и прежних сессий. Проверьте, что число посетителей
не изменилось и известные пользователи не превышают его. не изменилось, а число известных пользователей не больше числа посетителей.
Напишите, какой вариант нужен для двух требований к витрине: Теперь проверьте, можно ли воспроизвести расчет на H0 без сохраненного
списка за эту дату. Возьмите список на H2 и оставьте только пары,
- «пересчитать прошлые визиты с учетом всех сведений, доступных сейчас»; у которых `first_purchase_at` приходится на H0 или более ранний день.
- «воспроизвести расчет, сделанный при наблюдении H0». Сравните полный набор пар с копией на H0. Выведите пары, которые есть
только в одном из списков.
Для второго требования проверьте еще один способ: взять текущие связи, На примере найденных различий объясните, какие данные нужно сохранять,
оставив только `first_purchase_at` до конца H0. Совпадет ли полный набор чтобы повторить прежний расчет.
пар с сохраненным в H0? Найденным расхождением обоснуйте выбор истории
для воспроизводимого расчета.
## Что должно получиться ## Что должно получиться
У вас есть запрос связей с подтверждающими покупками, три сохраненных У вас есть запрос сопоставления посетителей с покупателями, три сохраненных
наблюдения и сравнение двух расчетов на одном наборе сессий. Вы можете списка связей и сравнение двух расчетов на одном наборе сессий. На конкретных
показать, когда новая связь стала видна в наблюдениях, и объяснить, парах вы можете показать разницу между датой покупки и датой появления пары
какие данные понадобятся для воспроизведения прежнего расчета. в сохраненном списке, а также объяснить, что нужно для повторения прежнего
расчета.
Сохраните SQL и таблицу наблюдений: они нужны в продолжении Сохраните SQL и таблицу `lab_identity_history`: они нужны в продолжении
[«Как обновлять витрину без полной пересборки»](incremental-daily-traffic.md). [«Как обновлять витрину без полной пересборки»](incremental-daily-traffic.md).
Если продолжать не планируете, удалите свою таблицу через `DROP TABLE`. Если продолжать не планируете, удалите свою таблицу через `DROP TABLE`.
+52 -48
View File
@@ -4,20 +4,21 @@
и повторять незавершенную загрузку. и повторять незавершенную загрузку.
Факультатив повышенной сложности продолжает работу Факультатив повышенной сложности продолжает работу
[«Когда хранилище узнало пользователя»](identity-over-time.md). [«Как новые заказы меняют отчет о посетителях»](identity-over-time.md).
Понадобятся ее запрос сохранения связей и таблица `default.lab_identity_history`. Понадобятся ее запрос сохранения списка связей и таблица `default.lab_identity_history`.
Работайте под `student` через то же подключение DBeaver. Работайте под `student` через то же подключение DBeaver.
Стенд пересчитывает `dm.daily_traffic_v` целиком: новая связь может изменить Стенд пересчитывает `dm.daily_traffic_v` целиком: новая связь может изменить
число известных пользователей в старом дне. Вы построите рядом свою витрину, число известных пользователей в старом дне. Вы построите рядом свою витрину,
которая пересчитывает только затронутые дни и дает тот же результат. которая пересчитывает только затронутые дни и дает тот же результат.
В опыте появляются новые дни сессий и новые связи. Мир стенда не отзывает При обычном продвижении модельного времени появляются новые дни сессий
связи и не исправляет старые сессии при обычном продвижении времени. и новые связи. Уже найденные пары сохраняются, а сессии прежних дней
не меняются.
Удаление данных, исправления старых сессий и пересоздание мира потребовали бы Удаление данных, исправления старых сессий и пересоздание мира потребовали бы
другого способа обнаружения изменений. другого способа обнаружения изменений.
## Подготовьте витрину и журнал загрузок ## Создайте витрину и журнал успешных загрузок
Создайте две локальные таблицы. Если имена заняты, выберите свои. Создайте две локальные таблицы. Если имена заняты, выберите свои.
@@ -41,93 +42,96 @@ ORDER BY observation_day;
``` ```
В витрине одна актуальная строка на день. Для обновления вставляйте полный В витрине одна актуальная строка на день. Для обновления вставляйте полный
новый результат дня с новой `observation_day`; читайте его с `FINAL`. новый результат дня с новой `observation_day`. Читайте витрину с `FINAL`:
Движок выбирает максимальную `observation_day` для каждого `report_date`. для каждого `report_date` останется строка с максимальной `observation_day`.
Старая версия может исчезнуть при слиянии: эта таблица хранит текущий расчет, Старые версии могут исчезнуть при слиянии,
а не историю наблюдений. поэтому для воспроизведения прежнего расчета нужна сохраненная история связей.
Журнал хранит даты успешно завершенных загрузок. Добавляйте дату только В журнал записывайте `observation_day` списка связей, использованного
после записи всех выбранных дней и успешной сверки. Последняя строка журнала для расчета. Добавляйте дату только после записи всех выбранных дней
будет границей, от которой вы ищете новые связи в следующем прогоне. и успешной сверки. Максимальная дата в журнале укажет, какой список
использовался в последней завершенной загрузке.
## Выполните первую загрузку целиком ## Выполните первую загрузку целиком
Убедитесь, что `world_live` стоит на паузе и нет активных прогонов генерации Убедитесь, что `world_live` стоит на паузе и нет активных прогонов генерации
и обработки. Запустите `etl_pipeline` и дождитесь успеха. Получите текущую и обработки. Запустите `etl_pipeline` и дождитесь успеха. Получите текущую
`observation_day` способом из первой работы. Если такого наблюдения еще нет `observation_day` способом из первой работы. Если списка за эту дату еще нет
в вашей истории, сохраните его; уже сохраненную дату повторно не вставляйте. в вашей истории, сохраните его. Повторно вставлять уже сохраненный список
не требуется.
Напишите `INSERT INTO ... SELECT`, который рассчитывает все даты сессий из Напишите `INSERT INTO ... SELECT`, который рассчитывает все даты сессий из
`dds.session_v` со связями текущего наблюдения. Правила результата: `dds.session_v` со списком связей на текущую дату. Правила результата:
| Колонка | Что считать | | Колонка | Что считать |
|---|---| |---|---|
| `report_date` | `session_date` | | `report_date` | `session_date` |
| `visitors` | число разных `client_id` | | `visitors` | число разных `client_id` |
| `known_users` | число разных известных `user_id` | | `known_users` | число разных известных `user_id` |
| `observation_day` | дата текущего наблюдения | | `observation_day` | дата выбранной копии списка связей |
Сессии без известного пользователя сохраняйте. Несколько кук одного Сессии без известного пользователя сохраняйте в расчете. Несколько кук одного
пользователя не должны увеличивать `known_users`. Для соединения с локальной пользователя не должны увеличивать `known_users`. Для соединения с вашей
историей связей используйте `GLOBAL LEFT JOIN`. таблицей связей используйте `GLOBAL LEFT JOIN`, отобрав одну `observation_day`.
Построчно сверьте свои `report_date`, `visitors`, `known_users` с Построчно сверьте свои `report_date`, `visitors`, `known_users` с
`dm.daily_traffic_v`. Сверка должна находить лишние и отсутствующие даты `dm.daily_traffic_v`. Сверка должна находить лишние и отсутствующие даты
и различия обоих показателей. Учебную витрину читайте с `FINAL`. и различия обоих показателей. Учебную витрину читайте с `FINAL`.
После совпадения запишите дату наблюдения в журнал один раз. После совпадения запишите дату наблюдения в журнал один раз.
## Выберите дни следующего пересчета ## Найдите дни, которые нужно пересчитать
Продвиньте мир на один день: `world_next_day` с `days = 1`, затем Продвиньте мир на один день: `world_next_day` с `days = 1`, затем
`etl_pipeline`; после каждого запуска дождитесь успеха. Сохраните новое `etl_pipeline`; после каждого запуска дождитесь успеха. Сохраните новый
наблюдение связей запросом из первой работы. список связей запросом из первой работы.
Обозначим дату последней успешной загрузки из журнала **P**, текущую дату Обозначим максимальную `observation_day` в журнале **P**, а дату нового
наблюдения — **H**. До записи результата составьте запрос, возвращающий списка связей — **H**. До записи результата составьте запрос, возвращающий
разные даты для пересчета. Он должен учесть два изменения: даты для пересчета без повторов. Он должен учесть два изменения:
- в сессиях появились дни, которых еще нет в вашей витрине; - в сессиях появились дни, которых еще нет в вашей витрине;
- в карте появились пары, которых не было при P; у этих кук есть сессии, - в списке на H появились пары, которых не было в списке на P; у этих кук
в том числе за прежние дни. есть сессии, в том числе за прежние дни.
Сначала выведите даты и разберите одну старую дату: какая новая пара привела Сначала выведите даты и выберите один прежний день: какая новая пара
к ее выбору и какие сессии с ней связаны? Рядом посчитайте, сколько дат потребовала его пересчета и какие сессии с ней связаны? Рядом посчитайте,
пересчитала бы полная загрузка. Зафиксируйте оба числа; равенство тоже сколько дат пересчитала бы полная загрузка. Зафиксируйте оба числа;
возможно, если новые связи затронули всю историю. равенство тоже возможно, если новые связи затронули всю историю.
Проверьте альтернативу: искать изменения по `first_purchase_at`, взяв покупки Для сравнения выберите из списка на H пары, у которых день покупки
только после конца P. Сравните полученный набор пар с разностью наблюдений. по `first_purchase_at` позже P. Совпадет ли результат с новыми парами,
Покажите пару, которую такое ограничение пропускает. найденными сравнением списков? Покажите пару, которую ограничение по дате
покупки пропускает.
## Обновите выбранные дни ## Обновите выбранные дни
Напишите второй `INSERT INTO ... SELECT`. Для выбранных дат он должен Напишите второй `INSERT INTO ... SELECT`. Для выбранных дат он должен
рассчитать все сессии с полной картой связей на H. Прибавление числа новых рассчитать все сессии с полным списком связей на H. Прибавление числа новых
связей к прежнему `known_users` не заменяет пересчет разных пользователей. связей к прежнему `known_users` не заменяет пересчет разных пользователей.
Вставьте результаты с версией H. Полностью сверьте актуальную учебную витрину Вставьте результаты с версией H. Полностью сверьте актуальную учебную витрину
со стендовой, включая дни, которые не пересчитывали. Только после совпадения со стендовой, включая дни, которые не пересчитывали. Только после совпадения
добавьте H в журнал. добавьте H в журнал.
Без продвижения мира повторите выбор дат. После успешной загрузки новых дат Без продвижения мира снова определите P по журналу и повторите выбор дат.
и пар быть не должно, а результат витрины должен остаться прежним. Запрос должен вернуть пустой результат, а витрина — остаться прежней.
Повторно записывать H в журнал не требуется. Повторно записывать H в журнал не требуется.
## Повторите незавершенную загрузку ## Проверьте повторный запуск после частичной загрузки
Снова запустите `world_next_day` с `days = 1`, затем `etl_pipeline`, Снова запустите `world_next_day` с `days = 1`, затем `etl_pipeline`,
дожидаясь успеха каждого. Сохраните новое наблюдение связей. дожидаясь успеха каждого. Сохраните новый список связей с его датой H.
Получите даты для пересчета, но запишите результат только за одну из Получите даты для пересчета, но запишите результат только за одну из
них. Остальные пока не загружайте и дату в журнал не добавляйте. них. Остальные пока не загружайте и дату в журнал не добавляйте.
Так вы воспроизведете остановку между записями дней, не ломая стенд. Так вы воспроизведете остановку между записями дней, не ломая стенд.
Запустите свой обычный расчет еще раз, определив P по журналу. Он должен Снова определите P по журналу, выполните запрос выбора дат и пересчет
довести загрузку до совпадения со стендовой витриной без очистки уже этих дней. Проверьте, что результат совпал со стендовой витриной без очистки
записанных строк. После сверки запишите H в журнал. уже записанных строк. После сверки запишите H в журнал.
Объясните на этом опыте, почему последнее сохраненное наблюдение нельзя Объясните на этом опыте, почему сохранение нового списка связей еще
автоматически считать загруженным в витрину. Что изменится, если записать H не означает, что витрина обновлена. Что изменится, если записать H в журнал
в журнал до обновления витрины? до обновления витрины?
## Что должно получиться ## Что должно получиться
@@ -136,9 +140,9 @@ ORDER BY observation_day;
повтор без новых данных сохраняет результат, а незавершенную загрузку повтор без новых данных сохраняет результат, а незавершенную загрузку
можно продолжить без полной пересборки. можно продолжить без полной пересборки.
Сравните цену двух вариантов: сколько дней вы пересчитывали и какие Сравните число пересчитанных дней с полной загрузкой и перечислите,
дополнительные данные хранили. По этому опыту можно судить об объеме работы, какие дополнительные данные пришлось хранить. Так можно оценить объем работы.
но не об ускорении: на небольшом стенде полный пересчет может быть дешевле. На небольшом стенде полный пересчет может быть дешевле.
В вашей реализации поиск связей пока читает всю историю покупок — В вашей реализации поиск связей пока читает всю историю покупок —
инкрементальным стал именно пересчет дневной витрины. инкрементальным стал именно пересчет дневной витрины.
+1 -1
View File
@@ -207,7 +207,7 @@
Открывается после ступени 1 основной программы. Открывается после ступени 1 основной программы.
- **[Когда хранилище узнало пользователя](../labs/optional/identity-over-time.md) (#145).** - **[Как новые заказы меняют отчет о посетителях](../labs/optional/identity-over-time.md) (#145).**
Учащийся собирает связи с подтверждающими покупками и сохраняет три Учащийся собирает связи с подтверждающими покупками и сохраняет три
ежедневных наблюдения. На одном наборе прежних сессий сравнивает расчет ежедневных наблюдения. На одном наборе прежних сессий сравнивает расчет
с прежними и новыми связями, выбирает историю для воспроизведения расчета. с прежними и новыми связями, выбирает историю для воспроизведения расчета.