docs(labs): история связей и инкрементальное обновление витрины #256
No files matched your search
@@ -30,7 +30,7 @@
|
||||
`first_matched_at` нет. Запланированные витрины его не читают, а два возможных
|
||||
смысла — «первая покупка вообще» и «момент, с которого связь была известна
|
||||
хранилищу» — требуют разных исторических моделей. Этот вопрос оставлен
|
||||
[самостоятельной работе](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/145),
|
||||
[самостоятельной работе](../../labs/optional/identity-over-time.md),
|
||||
а текущий стенд не выдаёт один смысл за другой.
|
||||
|
||||
Статус заказа связь не меняет: любой заказ, сопоставленный с событием покупки,
|
||||
|
||||
@@ -281,7 +281,10 @@ business_key`.
|
||||
- **Инкрементальный пересчёт затронутых дней по `first_matched_at`.**
|
||||
Производственный ход, но на объёмах стенда его экономия неизмерима, а
|
||||
след новых связок требует вернуть колонку, выкинутую в #133. Стал лабой
|
||||
повышенной сложности — issue #149.
|
||||
повышенной сложности —
|
||||
[«Как обновлять витрину без полной пересборки»](../labs/optional/incremental-daily-traffic.md).
|
||||
В ней новые связи находятся сравнением наблюдений: время покупки
|
||||
не заменяет момент обнаружения связи.
|
||||
- **Superset поверх DDS без слоя DM.** Терялся бы урок «витрина — контракт
|
||||
под потребителя», против раздела 7 спеки.
|
||||
|
||||
|
||||
+11
-2
@@ -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. Поломки стенда и инструкции по восстановлению
|
||||
|
||||
@@ -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`.
|
||||
@@ -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`, если они больше не нужны.
|
||||
@@ -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 отклонена: для учебного сравнения достаточно
|
||||
своей витрины и журнала рядом с работающим конвейером.
|
||||
|
||||
### Что делать, когда стенд сломался
|
||||
|
||||
|
||||
Reference in new issue
Block a user