From 090df8a715df336589de7d7ca7b19e7768441ceb Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Tue, 22 Sep 2026 03:32:12 -0400 Subject: [PATCH 1/7] =?UTF-8?q?docs(agents):=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=BE=20=D1=83=D1=81=D1=82=D0=B0=D1=80=D0=B5=D0=B2=D1=88?= =?UTF-8?q?=D0=B5=D0=B5=20=D1=80=D0=B0=D1=81=D0=BF=D1=80=D0=B5=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D1=81=D1=82=D0=B2=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Зачем: оценка моделей изменилась, назначение Opus автором текстов устарело. Что: автор задачи сам пишет и редактирует материалы, если владелец не назначил иное. Проверка: git diff --check; проверены AGENTS.md, CLAUDE.md и docs/agents. --- AGENTS.md | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 594c75e..d8621b2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,15 +46,10 @@ ## Кому что поручать -Читатель здесь не побочный потребитель, а тот, ради кого стенд существует: -код, комментарии и документы и есть продукт. Поэтому всё, что человек будет -читать и разбирать, пишет модель с чувством меры — сейчас это Opus. Codex -берёт техническую работу без вкусовых решений: миграции, разбор журналов и -объёмного вывода, обход репозитория, повторяющиеся правки. Там его запас -лимитов работает на нас, а экономить их на читаемом коде — экономия ложная: -переписывать выйдет дороже, чем сразу написать хорошо. +Автор задачи сам пишет и редактирует код, комментарии и документы, если +владелец не задал другое распределение работы. -Делить работу между ними стоит не целыми задачами, а подзадачами, и решать +Делить работу стоит не целыми задачами, а подзадачами, и решать это при планировании. Вкусовая часть идёт первой: она задаёт форму, имена и границы, под которые механическая потом подстраивается. From ddb186f841702bf12ba1111015b4b2108de9ec2a Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Tue, 22 Sep 2026 03:32:16 -0400 Subject: [PATCH 2/7] =?UTF-8?q?docs(labs):=20=D1=81=D0=BE=D0=B3=D0=BB?= =?UTF-8?q?=D0=B0=D1=81=D0=BE=D0=B2=D0=B0=D0=BD=D1=8B=20=D1=87=D0=B5=D1=82?= =?UTF-8?q?=D1=8B=D1=80=D0=B5=20=D1=80=D0=B0=D1=81=D1=81=D0=BB=D0=B5=D0=B4?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B2=D1=8B=D1=80=D1=83?= =?UTF-8?q?=D1=87=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Зачем: превратить шесть идей факультативов в связный раздел из четырех работ. Что: определены результаты работ и наблюдение за заказами через Airflow. Проверка: git diff --check; порядок обновления данных сверен с кодом дагов. --- docs/specs/2026-09-01-labs-program.md | 57 +++++++++++++++++++-------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/docs/specs/2026-09-01-labs-program.md b/docs/specs/2026-09-01-labs-program.md index 87f2627..e90985c 100644 --- a/docs/specs/2026-09-01-labs-program.md +++ b/docs/specs/2026-09-01-labs-program.md @@ -149,28 +149,53 @@ ## 4. Факультативы — пять разделов -Ниже перечислены только названия сюжетов и их уроки. Материал, формы и способы -проверки остаются в [инвентаризации](../research/2026-08-31-labs-inventory.md). +Ниже перечислены сюжеты, их уроки и принятые решения по форме заданий. Исходный +материал и способы проверки — в +[инвентаризации](../research/2026-08-31-labs-inventory.md). ### Почему выручка не сходится -Этот раздел рекомендован первым из факультативов. Он открывается после ступени -1 основной программы: материал живет на стенде с этапа 6. +Этот раздел рекомендован первым из факультативов и открывается после ступени +1 основной программы. Шесть сюжетов собраны в четыре лабораторные: три +расследования готовых данных и одно наблюдение во времени. Опоздания, +изменение выручки и история отмен используют одну историю заказов, поэтому +для них выбрана общая работа вместо трех отдельных. -- **Почему трекер недосчитывает покупки (#186).** Разрыв воронки подтверждений - показывает потерянные события и границу доверия к двум источникам. +- **Куда пропали покупки (#186).** Учащийся сравнивает подтверждения и события + покупки, находит заказы без события и объясняет разрыв воронки конкретными + заказами. - **Почему повтор покупки завышает выручку (#187).** Движок убирает технический дубль строки, а повтор одной покупки приходится находить по деловому ключу. -- **Какая позиция объясняет разницу в сумме.** Расхождение суммы нужно - разложить до конкретной вычеркнутой позиции и объяснить до копейки. -- **Был ли отмененный заказ оплачен.** Текущее состояние не заменяет историю, а - некоторые различия после истечения срока хранения сырых данных уже не - восстановить. -- **Когда отчет за день можно считать окончательным (#188).** Отчет за день - становится окончательным лишь после того, как источник больше не может - изменить этот день. -- **Почему выручка за день еще меняется.** Внутри окна изменяемости выручка - движется, а после закрытия дня фиксируется. + Учащийся сравнивает сумму событий с суммой по уникальным покупкам и + подтверждает, что разница равна вкладу повторов. Другие причины расхождения + с выручкой магазина при этом остаются. +- **Какая позиция объясняет разницу в сумме.** Учащийся сопоставляет корзину + из сырого `ecommerce` с позициями заказа, находит удаленный товар и объясняет + разницу до копейки. Для точных цен нужен исходный JSON: в готовых массивах + событий цены округлены до рублей. +- **Почему вчерашняя выручка меняется сегодня (#188).** Учащийся наблюдает один + день по мере поступления слепков: появляются опоздавшие заказы, меняются + статусы и выручка. По сохраненным состояниям объясняет причины изменений, + определяет момент закрытия дня и отделяет доказанную оплату отмененного + заказа от случаев, в которых историю оплаты восстановить нельзя. + +В четвертой лабораторной учащийся сам собирает историю одного дня оформления +заказов. Перед наблюдением он ставит `world_live` на паузу и дожидается +завершения начатых прогонов. Затем продвигает +мир по одному модельному дню через `world_next_day` и после каждого запуска +выполняет `etl_pipeline`. Только после завершения конвейера сохраняет дату +слепка, состояния отдельных заказов и выручку выбранного дня. Эти результаты +позволяют сравнить наблюдения даже после удаления сырых слепков. Готовые +слепки и сочетание готового примера с собственным наблюдением отклонены: +учащийся может получить нужную историю на стенде и сам увидеть, как новые +данные меняют уже рассчитанный отчет. + +День считается закрытым после получения и обработки последнего слепка +семидневного окна изменяемости; этот слепок доставляется следующим модельным +днем. Одного совпадения сумм в соседних наблюдениях для такого вывода +недостаточно. История отмен тоже имеет границу: оплата и отмена между двумя +дневными слепками могут остаться невидимыми. Отсутствие наблюдаемой оплаты +не доказывает, что оплаты не было. ### Тонкий анализ данных From 2502277af18cb7d661bbdb49b1c9141eff513c97 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Tue, 22 Sep 2026 03:46:45 -0400 Subject: [PATCH 3/7] =?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=D1=82=D1=80=D0=B8=20=D1=80?= =?UTF-8?q?=D0=B0=D1=81=D1=81=D0=BB=D0=B5=D0=B4=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=80=D0=B0=D1=81=D1=85=D0=BE=D0=B6=D0=B4=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B9=20=D0=B2=D1=8B=D1=80=D1=83=D1=87=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Зачем: дать практику сверки событий покупки с заказами магазина. Что: задания о потерях событий, повторах покупок и расхождении состава корзин. Проверка: запросы под analyst на ClickHouse 26.3.17.56; счетчики, суммы и точный разбор JSON сошлись. make lint и независимая техническая проверка пройдены. --- docs/labs/optional/duplicate-purchases.md | 65 +++++++++++++++ docs/labs/optional/missing-purchases.md | 79 +++++++++++++++++++ docs/labs/optional/order-amount-difference.md | 70 ++++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 docs/labs/optional/duplicate-purchases.md create mode 100644 docs/labs/optional/missing-purchases.md create mode 100644 docs/labs/optional/order-amount-difference.md diff --git a/docs/labs/optional/duplicate-purchases.md b/docs/labs/optional/duplicate-purchases.md new file mode 100644 index 0000000..38f860e --- /dev/null +++ b/docs/labs/optional/duplicate-purchases.md @@ -0,0 +1,65 @@ +# Почему повтор покупки завышает выручку + +**Чему учит:** отличать повторную доставку строки от повторного события об +одной покупке и выбирать ключ, по которому считать деньги. + +Факультатив доступен после ступени 1. Работайте в DBeaver пользователем +`analyst` по [инструкции](../../../README.md#подключиться-из-dbeaver). +Дождитесь завершения `etl_pipeline`; оставьте `world_live` на паузе и +дождитесь уже начатых прогонов, чтобы данные не менялись между запросами. + +## Посчитайте клиентскую сумму двумя способами + +Возьмите один закрытый день: его дата должна быть не позднее +`max(snapshot_date) - 6` из `ods.order_v`. Во всех запросах к `dds.event_v` +отбирайте этот `event_date` и `event_type = 'purchase'`. + +Номер заказа находится в `purchase_id[1]`, заявленная сумма — в +`purchase_revenue[1]`. По формату источника у события покупки один номер заказа. + +Сначала посчитайте число событий и сумму всех заявленных сумм. Затем найдите +номера заказов, которые встречаются несколько раз. Если таких нет, выберите +другой закрытый день. + +Для двух повторившихся покупок выпишите `event_id`, `client_id`, `event_time`, +номер заказа и сумму. Сопоставьте число событий и заказов в `dds.order_v`. + +Теперь рассчитайте клиентскую сумму, учитывая каждый номер заказа один раз. +До выбора одной суммы на заказ проверьте, что во всех его событиях она +одинакова. Объясните, почему удалять одинаковые денежные значения вместо +повторных номеров заказов было бы ошибкой. + +## Объясните разницу + +Для каждого повторившегося заказа определите число лишних событий и их +вклад в завышение. Сложите эти вклады и сверьте с разностью двух расчетов. +Равенство должно выполняться до копейки. + +Слой ODS убирает повторную доставку одного события по его идентификатору; +`dds.event_v` читает уже это состояние. Объясните по своим строкам, почему +повтор покупки сохранился. Какие поля совпадают и какие различаются? +Какой ключ отвечает на вопрос «это то же событие», а какой — «это тот же заказ»? + +После собственного расчета найдите эти заказы в `dm.purchase_vs_orders_v`. +Если у повторившегося заказа класс отличается от `duplicate_event`, проверьте +его статус и [порядок присвоения классов](../../architecture/dm.md#dmpurchase_vs_orders_v). +Класс витрины не заменяет поиск всех повторов в событиях. + +## Сравните с выручкой магазина + +Сложите `revenue` из `dm.revenue_daily_v` за тот же `report_date` по всем +категориям. Эта витрина считает стоимость позиций оплаченных заказов, +без скидки и доставки. + +Сравните результат с исправленной клиентской суммой. Совпадение не требуется: +удаление повторов устраняет одну причину расхождения. Найдите хотя бы один +заказ, который объясняет, почему деньги трекера и магазина все еще могут +различаться. Для этого сопоставьте события, заказ и готовую сверку. + +## Что должно получиться + +- [ ] сохранены две клиентские суммы и строки повторившихся покупок; +- [ ] разность сумм полностью объяснена вкладом лишних событий; +- [ ] выбор ключа для устранения повторов подтвержден данными; +- [ ] вы можете объяснить, почему устранение повторов не завершает сверку + выручки с магазином. diff --git a/docs/labs/optional/missing-purchases.md b/docs/labs/optional/missing-purchases.md new file mode 100644 index 0000000..75528d3 --- /dev/null +++ b/docs/labs/optional/missing-purchases.md @@ -0,0 +1,79 @@ +# Куда пропали покупки + +**Чему учит:** обнаружить потерю событий при сверке двух источников и +обосновать, каким данным можно доверять при подсчете покупок. + +Факультатив доступен после ступени 1. В DBeaver подключитесь пользователем +`analyst` по [инструкции](../../../README.md#подключиться-из-dbeaver). +Дождитесь завершения `etl_pipeline`; на время расследования оставьте +`world_live` на паузе и дождитесь уже начатых прогонов. + +## Выберите день + +Магазин зарегистрировал заказы, но часть событий покупки не дошла до трекера. +Найдите след этой потери сначала в событиях сайта, затем в заказах. + +Возьмите день, для которого уже получен последний слепок семидневного окна. +Запрос показывает самую позднюю подходящую дату: + +```sql +SELECT max(snapshot_date) - 6 AS last_closed_day +FROM ods.order_v; +``` + +Используйте выбранный день во всех запросах. События отбирайте по +`dds.event_v.event_date`, заказы — по `dds.order_v.order_date`. +Сравнивайте все заказы дня, включая отмененные: сейчас вопрос в том, +зарегистрировал ли магазин заказ, а не в том, был ли он оплачен. + +## Найдите разрыв в событиях + +Посчитайте просмотры страницы подтверждения и события покупки: + +- у просмотра `event_type = 'pageview'`, а `url` заканчивается на `/confirmation`; +- у покупки `event_type = 'purchase'`. + +Запишите оба числа и разность. Какую потерю она позволяет заподозрить? +Достаточно ли этих двух чисел, чтобы назвать пропавшие покупки? + +## Сверьте с магазином + +В событии номер заказа находится в `purchase_id[1]`, в заказах — в `order_id`. +Соберите список заказов выбранного дня, для которых нет события покупки. +Несколько событий с одним номером заказа должны оставаться одной покупкой. + +Проверьте два равенства: + +- число заказов магазина равно числу разных заказов из событий плюс число + заказов без события; +- разность просмотров подтверждения и событий покупки равна числу заказов + без события. + +Первое равенство проверяет полноту сопоставления источников. Второе относится +к устройству этого стенда. Найдите покупку с повторным событием и посмотрите +соседние события того же `client_id` по времени: что произошло со страницей +подтверждения? Объясните, почему повтор не скрыл разрыв воронки. + +Если в выбранном дне повторов нет, возьмите другой закрытый день. +Для оценки доли потерь используйте число заказов магазина и объясните, +почему число просмотров подтверждения может дать другую долю. + +## Сопоставьте вывод с готовой сверкой + +Найдите свои заказы без события в `dm.purchase_vs_orders_v` по `order_id`. +Сравните их `status` и `mismatch_class`. Витрина присваивает заказу один класс, +хотя причин расхождения может быть несколько. По +[правилу классификации](../../architecture/dm.md#dmpurchase_vs_orders_v) +объясните, почему потеря события не всегда попадает в `lost_event`. + +Возьмите один заказ из своего списка и сохраните два запроса: первый находит +его в магазине, второй показывает отсутствие покупки в трекере. Сформулируйте, +какой источник нужен для отчета о поведении посетителей, а какой — для денег. + +## Что должно получиться + +- [ ] сохранены день, счетчики воронки и список заказов без события; +- [ ] оба равенства сходятся, влияние повторного просмотра объяснено; +- [ ] доля потерь посчитана с обоснованным знаменателем; +- [ ] один случай подтвержден запросами к обоим источникам; +- [ ] вы различаете отсутствие события и класс строки в готовой сверке. diff --git a/docs/labs/optional/order-amount-difference.md b/docs/labs/optional/order-amount-difference.md new file mode 100644 index 0000000..7a4973b --- /dev/null +++ b/docs/labs/optional/order-amount-difference.md @@ -0,0 +1,70 @@ +# Какая позиция объясняет разницу в сумме + +**Чему учит:** перейти от расхождения суммы заказа к конкретному товару и +проверить объяснение с точностью до копейки. + +Факультатив доступен после ступени 1. В DBeaver используйте пользователя +`analyst` из [инструкции подключения](../../../README.md#подключиться-из-dbeaver). +Дождитесь завершения `etl_pipeline`; оставьте `world_live` на паузе и +дождитесь уже начатых прогонов. + +## Найдите заказ + +В `dm.purchase_vs_orders_v` выберите один заказ с +`mismatch_class = 'amount_delta'`. Возьмите закрытый день: `order_day` +не позднее `max(snapshot_date) - 6` из `ods.order_v`. Сохраните `order_id`, +`order_day`, `declared_revenue` и `items_total`. + +Найдите этот заказ в `dds.order_v`. Сопоставьте `items_total`, `discount`, +`delivery` и `total`. Как связаны эти суммы? Объясните, почему клиентскую +`declared_revenue` нужно сравнивать с `items_total`, чтобы искать разницу +именно в составе корзины. + +## Разберите позиции с двух сторон + +Найдите событие покупки в `ods.event_v`: `purchaseID[1]` совпадает с номером +заказа, `EventDate` — с выбранным днем, `EventType = 'purchase'`. +Откройте поле `ecommerce`. Внутри JSON массив `purchase.products` содержит +позиции клиента с полями `id`, `price` и `quantity`. + +Разверните этот массив в строки и получите артикул, точную цену, количество +и стоимость каждой позиции. Для разбора пригодятся `JSONExtractArrayRaw` +и `JSONExtract`, для разворачивания массива — `ARRAY JOIN`. +Деньги извлекайте как `Decimal` с двумя знаками после запятой. + +С другой стороны используйте массивы `item_sku`, `item_price` и +`item_quantity` заказа в `dds.order_v`. Элементы с одним индексом относятся +к одной позиции; при разворачивании сохраняйте эту связь. + +Сопоставьте позиции по артикулу так, чтобы остались видны товары, +присутствующие только с одной стороны. Покажите, какие позиции совпали, +а какая объясняет разницу. В результате нужны строки товаров, а не только +две итоговые суммы. + +## Проверьте объяснение + +Подтвердите три равенства: + +- сумма стоимостей клиентских позиций равна `declared_revenue`; +- сумма стоимостей позиций магазина равна `items_total`; +- стоимость отсутствующей у магазина позиции равна + `declared_revenue - items_total`. + +Затем сравните точные цены из JSON с готовым массивом `productPrice` +события. По [формату выгрузки](../../formats/clickstream-event.md) цены в нем +округлены до целых рублей. Покажите на своей корзине, что потеряется при +расчете по этому массиву. Если все цены выбранной корзины целые, возьмите +еще один заказ с расхождением и дробной ценой. + +В коротком объяснении назовите заказ, товар, количество, цену и разницу. +Отделите то, что подтверждают данные, от причины, которую по ним установить +нельзя: отсутствие позиции в заказе видно, а причину ее удаления источник +отдельным полем не сообщает. + +## Что должно получиться + +- [ ] позиции клиента и магазина сопоставлены по артикулам; +- [ ] все три равенства сходятся до копейки; +- [ ] найден пример потери точности в готовом массиве цен; +- [ ] расхождение объяснено конкретной позицией, а скидка и доставка + отделены от сравнения корзин. From 515f386e5f33cb35e4aabfe071aace141d43a87b Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Tue, 22 Sep 2026 03:58:42 -0400 Subject: [PATCH 4/7] =?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=D0=BE=20=D0=BD=D0=B0=D0=B1=D0=BB=D1=8E?= =?UTF-8?q?=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=B0=20=D0=B2=D1=8B?= =?UTF-8?q?=D1=80=D1=83=D1=87=D0=BA=D0=BE=D0=B9=20=D0=B4=D0=BE=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BA=D1=80=D1=8B=D1=82=D0=B8=D1=8F=20=D0=B4=D0=BD=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Зачем: связать опоздания и отмены с изменением уже рассчитанного отчета. Что: лабораторная с сохранением состояний заказов, ссылки в индексе и программе; убраны повторы в описании раздела. Проверка: восемь однодневных world_next_day и etl_pipeline на Airflow 3.3.0; изменения сумм сошлись до копейки, закрытый день неизменен, DQ пройдены. Проверены 54 локальные ссылки, SQL-примеры и git diff --check. --- docs/labs/README.md | 15 ++- docs/labs/optional/revenue-over-time.md | 131 ++++++++++++++++++++++++ docs/specs/2026-09-01-labs-program.md | 41 +++----- 3 files changed, 158 insertions(+), 29 deletions(-) create mode 100644 docs/labs/optional/revenue-over-time.md diff --git a/docs/labs/README.md b/docs/labs/README.md index cf593f1..bc9d870 100644 --- a/docs/labs/README.md +++ b/docs/labs/README.md @@ -124,8 +124,19 @@ DM](../architecture/dm.md). ### Почему выручка не сходится -Открывается после ступени 1. Расхождения между источниками, повторы -покупок и день, после которого отчет можно считать окончательным. +Открывается после ступени 1. Четыре расследования: + +- [Куда пропали покупки](optional/missing-purchases.md): найти заказы без + события в трекере и объяснить разрыв воронки. +- [Почему повтор покупки завышает выручку](optional/duplicate-purchases.md): + посчитать деньги по номеру заказа и объяснить вклад повторов. +- [Какая позиция объясняет разницу в сумме](optional/order-amount-difference.md): + сопоставить корзины и найти причину расхождения до копейки. +- [Почему вчерашняя выручка меняется сегодня](optional/revenue-over-time.md): + проследить опоздания и отмены, затем определить момент закрытия дня. + +Первые три работы используют готовые данные. Четвертая продвигает мир через +Airflow; для нее вы сохраняете состояния заказов после каждого модельного дня. ### Тонкий анализ данных diff --git a/docs/labs/optional/revenue-over-time.md b/docs/labs/optional/revenue-over-time.md new file mode 100644 index 0000000..a11d017 --- /dev/null +++ b/docs/labs/optional/revenue-over-time.md @@ -0,0 +1,131 @@ +# Почему вчерашняя выручка меняется сегодня + +**Чему учит:** объяснять изменения уже рассчитанного отчета поступлением +заказов и сменой их статусов, определять момент закрытия дня и границы +доступной истории. + +Факультатив доступен после ступени 1. Понадобятся Airflow и DBeaver с +подключением `analyst` по [инструкции](../../../README.md#подключиться-из-dbeaver). +Вы будете наблюдать один день оформления заказов, продвигая мир по одному +модельному дню. Новые данные останутся в стенде. + +## Подготовьте первое наблюдение + +Поставьте `world_live` на паузу. Дождитесь завершения начатых прогонов +генерации и обработки, затем запустите `etl_pipeline` и дождитесь успеха. +Так исходное наблюдение будет снято после обновления витрин. + +Узнайте последний день событий и дату последнего полученного слепка: + +```sql +SELECT + (SELECT max(event_date) FROM dds.event_v) AS event_day, + (SELECT max(snapshot_date) FROM ods.order_v) AS snapshot_day; +``` + +День `event_day` обозначим **D**. Наблюдайте именно его до конца работы. +Дата слепка, по которому сделано очередное наблюдение, будет **S**. +Это модельные даты магазина. Календарная дата запуска Airflow день D не задает. + +Слепок снимается к концу суток и доставляется следующим прогоном. Поэтому +покупки последнего сыгранного дня уже видны в событиях, а их заказов в +хранилище еще нет. Проверьте это для D в `dds.order_v` и +`dm.purchase_vs_orders_v`. + +## Подготовьте запросы и сохраняйте результаты + +Напишите запросы для следующих наблюдений: + +| Что сохранить | Откуда взять | +|---|---| +| Дату полученного слепка S | `max(snapshot_date)` из `ods.order_v` | +| Номер, статус и `items_total` каждого заказа дня D | `dds.order_v`, отбор по `order_date` | +| Число заказов в каждом статусе и сумму `items_total` только у `paid` | те же заказы дня D | +| Выручку дня D | сумма `revenue` из `dm.revenue_daily_v`, отбор по `report_date` | +| Номера покупок, еще ожидающих заказ | строки `awaiting_order` из `dm.purchase_vs_orders_v`, отбор по `order_day` | + +Выручку складывайте по всем категориям. В этой витрине она означает стоимость +позиций оплаченных заказов, без скидки и доставки, поэтому должна сходиться +с суммой `items_total` у заказов в статусе `paid`. + +Сохраняйте результаты запросов отдельными файлами, например выгрузками CSV +из DBeaver. Подписывайте D и S. До поступления первого слепка список заказов +пуст, а выручка равна нулю; сохраните и это наблюдение. + +Публичное представление заказа показывает текущее состояние, а сырые слепки +хранятся ограниченное время. Сохраненные выгрузки — история для расследования. + +## Продвигайте мир по одному дню + +Для каждого следующего наблюдения: + +1. Запустите `world_next_day` с `days = 1` и дождитесь успеха. +2. Запустите `etl_pipeline` и дождитесь успеха: `world_next_day` сам витрины + не обновляет. +3. Повторите свои запросы и сохраните новый комплект результатов с датой S. + +Во всех запросах остается прежний день D. За один запуск проигрывайте один +день, чтобы сохранить промежуточные состояния. Пакетный `world_next_day` +отправляет события без пауз; ждать 24 минуты живого дня здесь не нужно. + +Продолжайте до слепка **D + 7** включительно. У заказов дня D окно состоит +из семи дней, от D до D + 6. Слепок D + 6 содержит окончательное состояние, +а еще один шаг позволит проверить, что оно сохранилось за пределами окна. +От исходного наблюдения потребуется восемь однодневных запусков. + +## Объясните изменения отчета + +После каждого шага сравнивайте заказы по `order_id`, а не по положению строк +в выгрузке. Найдите: + +- заказы, отсутствовавшие в первом слепке D, но появившиеся в следующем; +- переходы из `created` в `paid`; +- отмены заказов, для которых раньше был виден статус `paid`. + +Первый слепок, полученный на следующий день, — обычная доставка заказов. +Опоздавшими здесь считаются те, которых не было уже в этом слепке. +Покажите на нескольких номерах, как они исчезают из `awaiting_order`. + +Для двух соседних наблюдений разложите изменение выручки по заказам. +Вклад заказа — его `items_total`, если статус `paid`, и ноль в остальных +случаях, включая отсутствие заказа. Разность вкладов объясняет, сколько +каждый заказ добавил к отчету или убрал из него. Сумма этих изменений должна +совпасть с изменением `revenue` до копейки. + +Запишите даты, между которыми выручка выросла или уменьшилась. Отдельный +отмененный заказ мог убрать деньги, даже если итог дня вырос: покажите это +по его вкладу. Одна общая сумма скрывает встречные изменения. + +## Что известно об отменах + +Возьмите отмененный заказ, у которого в вашей истории был статус `paid`. +Укажите два наблюдения, доказывающих, что он был оплачен, а затем отменен. + +Для отмененного заказа без наблюдаемой оплаты сформулируйте предел вывода. +Дневные слепки могли не застать промежуточное состояние: оплата и отмена +могли произойти между ними. Поэтому отсутствие `paid` в выгрузках не +доказывает, что оплаты не было. Не заменяйте недостающие данные догадкой. + +## Когда день закрыт + +Сравните сохраненные состояния при S = D + 6 и S = D + 7: состав заказов, +их статусы, суммы и выручку. Объясните, почему отчет за D теперь окончательный. +Основание — получен и обработан последний слепок окна изменяемости; +одно совпадение сумм в соседних наблюдениях этого не доказывает. + +После собственных выводов сверьтесь с описанием +[слепка и его доставки](../../architecture/orders/snapshot.md#слепок-и-его-доставка) +и [границ истории отмен](../../architecture/orders/fate.md#судьба-заказа). + +## Что должно получиться + +- [ ] сохранена последовательность наблюдений одного дня D с датами слепков S; +- [ ] найдены опоздавшие заказы и изменения статусов; +- [ ] изменение выручки между двумя наблюдениями объяснено вкладами заказов; +- [ ] оплата одной из последующих отмен подтверждена двумя наблюдениями; +- [ ] вы различаете доказанную оплату и отсутствие сведений о ней; +- [ ] состояние дня за пределами окна совпало с окончательным, а момент + закрытия обоснован правилом доставки слепков. + +После работы оставьте `world_live` на паузе либо верните автоматическое +продвижение, если пользовались им до наблюдения. diff --git a/docs/specs/2026-09-01-labs-program.md b/docs/specs/2026-09-01-labs-program.md index e90985c..2796b44 100644 --- a/docs/specs/2026-09-01-labs-program.md +++ b/docs/specs/2026-09-01-labs-program.md @@ -161,34 +161,21 @@ изменение выручки и история отмен используют одну историю заказов, поэтому для них выбрана общая работа вместо трех отдельных. -- **Куда пропали покупки (#186).** Учащийся сравнивает подтверждения и события - покупки, находит заказы без события и объясняет разрыв воронки конкретными - заказами. -- **Почему повтор покупки завышает выручку (#187).** Движок убирает технический - дубль строки, а повтор одной покупки приходится находить по деловому ключу. - Учащийся сравнивает сумму событий с суммой по уникальным покупкам и - подтверждает, что разница равна вкладу повторов. Другие причины расхождения - с выручкой магазина при этом остаются. -- **Какая позиция объясняет разницу в сумме.** Учащийся сопоставляет корзину - из сырого `ecommerce` с позициями заказа, находит удаленный товар и объясняет - разницу до копейки. Для точных цен нужен исходный JSON: в готовых массивах - событий цены округлены до рублей. -- **Почему вчерашняя выручка меняется сегодня (#188).** Учащийся наблюдает один - день по мере поступления слепков: появляются опоздавшие заказы, меняются - статусы и выручка. По сохраненным состояниям объясняет причины изменений, - определяет момент закрытия дня и отделяет доказанную оплату отмененного - заказа от случаев, в которых историю оплаты восстановить нельзя. +- **[Куда пропали покупки](../labs/optional/missing-purchases.md) (#186).** + Учащийся объясняет разрыв воронки заказами, для которых нет события покупки. +- **[Почему повтор покупки завышает выручку](../labs/optional/duplicate-purchases.md) (#187).** + Разность клиентских сумм объясняется повторными событиями одной покупки. +- **[Какая позиция объясняет разницу в сумме](../labs/optional/order-amount-difference.md).** + Сравнение позиций из исходного JSON и заказа объясняет расхождение до копейки. +- **[Почему вчерашняя выручка меняется сегодня](../labs/optional/revenue-over-time.md) (#188).** + История одного дня связывает опоздания, оплату и отмены с изменением выручки, + позволяет определить момент закрытия дня и границу знания об оплате. -В четвертой лабораторной учащийся сам собирает историю одного дня оформления -заказов. Перед наблюдением он ставит `world_live` на паузу и дожидается -завершения начатых прогонов. Затем продвигает -мир по одному модельному дню через `world_next_day` и после каждого запуска -выполняет `etl_pipeline`. Только после завершения конвейера сохраняет дату -слепка, состояния отдельных заказов и выручку выбранного дня. Эти результаты -позволяют сравнить наблюдения даже после удаления сырых слепков. Готовые -слепки и сочетание готового примера с собственным наблюдением отклонены: -учащийся может получить нужную историю на стенде и сам увидеть, как новые -данные меняют уже рассчитанный отчет. +В четвертой лабораторной выбрано собственное наблюдение: учащийся продвигает +мир по одному дню через Airflow и сохраняет результаты после обновления витрин. +Готовые слепки и сочетание готового примера с собственным наблюдением +отклонены: нужную историю можно получить на стенде и самому увидеть, +как новые данные меняют уже рассчитанный отчет. День считается закрытым после получения и обработки последнего слепка семидневного окна изменяемости; этот слепок доставляется следующим модельным From bb087f4477907f28d41a62a05a216c3b4e5c89bc Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Tue, 22 Sep 2026 04:18:05 -0400 Subject: [PATCH 5/7] =?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=D0=B2=D0=BE=D0=BF=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=B0=D0=BC=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D1=8F=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D1=80=D0=B0=D1=81=D1=81=D0=BB=D0=B5=D0=B4=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Зачем: учащемуся нужен понятный путь от наблюдений к выводу без готового ответа в задании. Что: уточнены сравнения в четырех расследованиях; готовые объяснения заменены вопросами к строкам, расчетам и сохраненным состояниям. Проверка: проверены 54 локальные ссылки и git diff --check; формулировки сверены с устройством источников и витрин. --- docs/labs/optional/duplicate-purchases.md | 22 +++++++----- docs/labs/optional/missing-purchases.md | 29 ++++++++++----- docs/labs/optional/order-amount-difference.md | 29 +++++++-------- docs/labs/optional/revenue-over-time.md | 36 ++++++++++--------- 4 files changed, 68 insertions(+), 48 deletions(-) diff --git a/docs/labs/optional/duplicate-purchases.md b/docs/labs/optional/duplicate-purchases.md index 38f860e..2e93d7f 100644 --- a/docs/labs/optional/duplicate-purchases.md +++ b/docs/labs/optional/duplicate-purchases.md @@ -26,19 +26,23 @@ Теперь рассчитайте клиентскую сумму, учитывая каждый номер заказа один раз. До выбора одной суммы на заказ проверьте, что во всех его событиях она -одинакова. Объясните, почему удалять одинаковые денежные значения вместо -повторных номеров заказов было бы ошибкой. +одинакова. Проверьте свое решение на случае двух разных заказов с одинаковой +суммой: сколько денег оно учтет? -## Объясните разницу +## Найдите заказы, из-за которых сумма завышена -Для каждого повторившегося заказа определите число лишних событий и их -вклад в завышение. Сложите эти вклады и сверьте с разностью двух расчетов. -Равенство должно выполняться до копейки. +Возьмите один повторившийся номер заказа. Сколько денег он добавил в первый +расчет и сколько — во второй? Подтвердите разность его событиями. + +Повторите расчет для остальных повторившихся номеров. Сохраните таблицу: +номер заказа, число событий и вклад в разность сумм. Проверьте, объясняют ли +эти заказы все расхождение за день до копейки. Слой ODS убирает повторную доставку одного события по его идентификатору; -`dds.event_v` читает уже это состояние. Объясните по своим строкам, почему -повтор покупки сохранился. Какие поля совпадают и какие различаются? -Какой ключ отвечает на вопрос «это то же событие», а какой — «это тот же заказ»? +`dds.event_v` читает уже это состояние. Сравните число разных `event_id` +и число разных номеров заказа в выбранных строках. Почему устранение +повторов по первому ключу не дало тот же результат, что по второму? +Подтвердите ответ конкретными идентификаторами. После собственного расчета найдите эти заказы в `dm.purchase_vs_orders_v`. Если у повторившегося заказа класс отличается от `duplicate_event`, проверьте diff --git a/docs/labs/optional/missing-purchases.md b/docs/labs/optional/missing-purchases.md index 75528d3..7e783f2 100644 --- a/docs/labs/optional/missing-purchases.md +++ b/docs/labs/optional/missing-purchases.md @@ -28,13 +28,18 @@ FROM ods.order_v; ## Найдите разрыв в событиях +При оформлении заказа посетитель видит страницу подтверждения, а трекер +отправляет событие покупки. Просмотр страницы и покупка — отдельные события. + Посчитайте просмотры страницы подтверждения и события покупки: - у просмотра `event_type = 'pageview'`, а `url` заканчивается на `/confirmation`; - у покупки `event_type = 'purchase'`. -Запишите оба числа и разность. Какую потерю она позволяет заподозрить? -Достаточно ли этих двух чисел, чтобы назвать пропавшие покупки? +Вычтите число событий покупки из числа просмотров подтверждения. Сохраните +оба счетчика и разность. Как соотносились бы эти числа, если бы для каждого +заказа пришло ровно по одному событию каждого вида? Что можно предположить +по вашему результату и чего эти два числа еще не доказывают? ## Сверьте с магазином @@ -49,14 +54,22 @@ FROM ods.order_v; - разность просмотров подтверждения и событий покупки равна числу заказов без события. -Первое равенство проверяет полноту сопоставления источников. Второе относится -к устройству этого стенда. Найдите покупку с повторным событием и посмотрите -соседние события того же `client_id` по времени: что произошло со страницей -подтверждения? Объясните, почему повтор не скрыл разрыв воронки. +Первое равенство проверяет полноту сопоставления источников. Второе связывает +разность счетчиков с найденными заказами без события. Проверьте, как на него +влияют повторные покупки. + +Найдите номер заказа с несколькими событиями `purchase`. Для его `client_id` +выведите события рядом с этими покупками по времени: `event_time`, +`event_type` и `url`. Найдите просмотры `/confirmation` перед первой +и повторной покупкой. Посчитайте, сколько повтор добавил просмотров +подтверждения и сколько — покупок. Изменилась ли от этих добавлений разность +счетчиков? Подтвердите ответ найденными строками. Второе равенство относится +к устройству этого стенда; для другого источника его нужно проверять заново. Если в выбранном дне повторов нет, возьмите другой закрытый день. -Для оценки доли потерь используйте число заказов магазина и объясните, -почему число просмотров подтверждения может дать другую долю. +Для оценки доли потерь используйте число заказов магазина. Затем посчитайте +долю с тем же числителем, но с числом просмотров подтверждения в знаменателе. +Совпали ли результаты? Свяжите различие знаменателей с найденными событиями. ## Сопоставьте вывод с готовой сверкой diff --git a/docs/labs/optional/order-amount-difference.md b/docs/labs/optional/order-amount-difference.md index 7a4973b..a728fd4 100644 --- a/docs/labs/optional/order-amount-difference.md +++ b/docs/labs/optional/order-amount-difference.md @@ -16,9 +16,10 @@ `order_day`, `declared_revenue` и `items_total`. Найдите этот заказ в `dds.order_v`. Сопоставьте `items_total`, `discount`, -`delivery` и `total`. Как связаны эти суммы? Объясните, почему клиентскую -`declared_revenue` нужно сравнивать с `items_total`, чтобы искать разницу -именно в составе корзины. +`delivery` и `total`. Запишите равенство, связывающее эти четыре значения, +и проверьте его на заказе. С какой суммой магазина следует сравнивать +`declared_revenue`, если мы ищем расхождение именно в составе корзины? +Проверьте свой выбор после разбора позиций. ## Разберите позиции с двух сторон @@ -37,9 +38,9 @@ к одной позиции; при разворачивании сохраняйте эту связь. Сопоставьте позиции по артикулу так, чтобы остались видны товары, -присутствующие только с одной стороны. Покажите, какие позиции совпали, -а какая объясняет разницу. В результате нужны строки товаров, а не только -две итоговые суммы. +присутствующие только с одной стороны. Для каждой позиции сравните наличие, +цену и количество. Что различается и сколько это добавляет к расхождению +сумм? В результате нужны строки товаров, а не только две итоговые суммы. ## Проверьте объяснение @@ -47,19 +48,19 @@ - сумма стоимостей клиентских позиций равна `declared_revenue`; - сумма стоимостей позиций магазина равна `items_total`; -- стоимость отсутствующей у магазина позиции равна +- сумма расхождений по позициям равна `declared_revenue - items_total`. Затем сравните точные цены из JSON с готовым массивом `productPrice` -события. По [формату выгрузки](../../formats/clickstream-event.md) цены в нем -округлены до целых рублей. Покажите на своей корзине, что потеряется при -расчете по этому массиву. Если все цены выбранной корзины целые, возьмите -еще один заказ с расхождением и дробной ценой. +события. Пересчитайте стоимость клиентской корзины по этому массиву: +сойдется ли она с `declared_revenue` до копейки? Найдите цены, на которых +результаты различаются, и сверьте наблюдение с +[форматом выгрузки](../../formats/clickstream-event.md). Если все цены +выбранной корзины целые, возьмите еще один заказ с расхождением и дробной ценой. В коротком объяснении назовите заказ, товар, количество, цену и разницу. -Отделите то, что подтверждают данные, от причины, которую по ним установить -нельзя: отсутствие позиции в заказе видно, а причину ее удаления источник -отдельным полем не сообщает. +Какие строки подтверждают найденное расхождение? Есть ли в источниках +сведения о том, почему оно возникло? Отделите наблюдения от предположений. ## Что должно получиться diff --git a/docs/labs/optional/revenue-over-time.md b/docs/labs/optional/revenue-over-time.md index a11d017..5fef588 100644 --- a/docs/labs/optional/revenue-over-time.md +++ b/docs/labs/optional/revenue-over-time.md @@ -69,8 +69,8 @@ SELECT отправляет события без пауз; ждать 24 минуты живого дня здесь не нужно. Продолжайте до слепка **D + 7** включительно. У заказов дня D окно состоит -из семи дней, от D до D + 6. Слепок D + 6 содержит окончательное состояние, -а еще один шаг позволит проверить, что оно сохранилось за пределами окна. +из семи дней, от D до D + 6. Последний шаг нужен для сравнения состояния +в конце окна и за его пределами. От исходного наблюдения потребуется восемь однодневных запусков. ## Объясните изменения отчета @@ -86,32 +86,34 @@ SELECT Опоздавшими здесь считаются те, которых не было уже в этом слепке. Покажите на нескольких номерах, как они исчезают из `awaiting_order`. -Для двух соседних наблюдений разложите изменение выручки по заказам. -Вклад заказа — его `items_total`, если статус `paid`, и ноль в остальных -случаях, включая отсутствие заказа. Разность вкладов объясняет, сколько -каждый заказ добавил к отчету или убрал из него. Сумма этих изменений должна -совпасть с изменением `revenue` до копейки. +Выберите два соседних наблюдения, между которыми хотя бы один заказ перешел +из `paid` в `cancelled`. Разложите изменение выручки по заказам. +Составьте таблицу: номер заказа, прежний и новый статусы, вклад в выручку +до и после изменения. Вклад определите по правилу расчета витрины, указанному +выше; учтите и заказы, которых еще не было в первом наблюдении. Сумма +изменений вкладов должна совпасть с изменением `revenue` до копейки. -Запишите даты, между которыми выручка выросла или уменьшилась. Отдельный -отмененный заказ мог убрать деньги, даже если итог дня вырос: покажите это -по его вкладу. Одна общая сумма скрывает встречные изменения. +Для одного из этих отмененных заказов сравните изменение его вклада +с изменением общей выручки между теми же наблюдениями. Совпало ли +направление изменений? Найдите в своей таблице заказы, определившие общий итог. ## Что известно об отменах Возьмите отмененный заказ, у которого в вашей истории был статус `paid`. Укажите два наблюдения, доказывающих, что он был оплачен, а затем отменен. -Для отмененного заказа без наблюдаемой оплаты сформулируйте предел вывода. -Дневные слепки могли не застать промежуточное состояние: оплата и отмена -могли произойти между ними. Поэтому отсутствие `paid` в выгрузках не -доказывает, что оплаты не было. Не заменяйте недостающие данные догадкой. +Теперь возьмите отмененный заказ, для которого ни в одной вашей выгрузке +нет `paid`. Проверьте утверждение «этот заказ никогда не был оплачен»: +что известно о его состоянии между двумя слепками? Достаточно ли сохраненных +наблюдений для такого вывода? Укажите, какие данные позволили бы его проверить. ## Когда день закрыт Сравните сохраненные состояния при S = D + 6 и S = D + 7: состав заказов, -их статусы, суммы и выручку. Объясните, почему отчет за D теперь окончательный. -Основание — получен и обработан последний слепок окна изменяемости; -одно совпадение сумм в соседних наблюдениях этого не доказывает. +их статусы, суммы и выручку. Какой слепок последним мог изменить заказы дня D +и каким прогоном он доставлен? Укажите наблюдение, после которого отчет +можно считать окончательным. Было бы достаточно совпадения двух соседних +сумм, если бы окно еще не закончилось? После собственных выводов сверьтесь с описанием [слепка и его доставки](../../architecture/orders/snapshot.md#слепок-и-его-доставка) From 3e9249933b14159bbef287229046db7d3fa83c83 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Tue, 22 Sep 2026 04:28:27 -0400 Subject: [PATCH 6/7] =?UTF-8?q?docs(labs):=20=D1=80=D0=B0=D1=81=D1=81?= =?UTF-8?q?=D0=BB=D0=B5=D0=B4=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B2?= =?UTF-8?q?=D1=8B=D1=80=D1=83=D1=87=D0=BA=D0=B8=20=D0=BF=D1=80=D0=B8=D0=B2?= =?UTF-8?q?=D1=8F=D0=B7=D0=B0=D0=BD=D1=8B=20=D0=BA=20=D0=B7=D0=B0=D0=B4?= =?UTF-8?q?=D0=B0=D1=87=D0=B0=D0=BC=20DE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Зачем: результатом лабораторных должны быть обоснованные решения о подготовке данных для витрин. Что: добавлены проверяемые результаты сверки полноты, выбора ключа, точного разбора и правила пересчета; обновлены программа и индекс, убран подсчет долей потерь. Проверка: SELECT-запросы под analyst, расчеты по сохраненной истории девяти наблюдений, 54 локальные ссылки, git diff --check и независимое техническое ревью. --- docs/labs/README.md | 10 ++--- docs/labs/optional/duplicate-purchases.md | 37 ++++++++++----- docs/labs/optional/missing-purchases.md | 34 +++++++++----- docs/labs/optional/order-amount-difference.md | 27 ++++++++--- docs/labs/optional/revenue-over-time.md | 45 ++++++++++++------- docs/specs/2026-09-01-labs-program.md | 23 +++++++--- 6 files changed, 119 insertions(+), 57 deletions(-) diff --git a/docs/labs/README.md b/docs/labs/README.md index bc9d870..59a2ff7 100644 --- a/docs/labs/README.md +++ b/docs/labs/README.md @@ -124,16 +124,16 @@ DM](../architecture/dm.md). ### Почему выручка не сходится -Открывается после ступени 1. Четыре расследования: +Открывается после ступени 1. Четыре расследования качества данных: - [Куда пропали покупки](optional/missing-purchases.md): найти заказы без - события в трекере и объяснить разрыв воронки. + события, выбрать источник витрины и условие проверки полноты. - [Почему повтор покупки завышает выручку](optional/duplicate-purchases.md): - посчитать деньги по номеру заказа и объяснить вклад повторов. + выбрать зерно и ключ расчета, проверить его на повторных событиях. - [Какая позиция объясняет разницу в сумме](optional/order-amount-difference.md): - сопоставить корзины и найти причину расхождения до копейки. + разобрать позиции без потери точности и локализовать расхождение источников. - [Почему вчерашняя выручка меняется сегодня](optional/revenue-over-time.md): - проследить опоздания и отмены, затем определить момент закрытия дня. + обосновать окно пересчета витрины и требования к истории заказов. Первые три работы используют готовые данные. Четвертая продвигает мир через Airflow; для нее вы сохраняете состояния заказов после каждого модельного дня. diff --git a/docs/labs/optional/duplicate-purchases.md b/docs/labs/optional/duplicate-purchases.md index 2e93d7f..09686ce 100644 --- a/docs/labs/optional/duplicate-purchases.md +++ b/docs/labs/optional/duplicate-purchases.md @@ -1,14 +1,17 @@ # Почему повтор покупки завышает выручку -**Чему учит:** отличать повторную доставку строки от повторного события об -одной покупке и выбирать ключ, по которому считать деньги. +**Чему учит:** выбрать зерно и ключ расчета, чтобы повторные события +не умножали денежную сумму. Факультатив доступен после ступени 1. Работайте в DBeaver пользователем `analyst` по [инструкции](../../../README.md#подключиться-из-dbeaver). Дождитесь завершения `etl_pipeline`; оставьте `world_live` на паузе и дождитесь уже начатых прогонов, чтобы данные не менялись между запросами. -## Посчитайте клиентскую сумму двумя способами +## Проверьте расчет по событиям + +В ODS уже устранены повторные доставки событий. Достаточно ли этого, +чтобы сложить суммы всех `purchase` для витрины заказов? Проверьте на данных. Возьмите один закрытый день: его дата должна быть не позднее `max(snapshot_date) - 6` из `ods.order_v`. Во всех запросах к `dds.event_v` @@ -24,10 +27,16 @@ Для двух повторившихся покупок выпишите `event_id`, `client_id`, `event_time`, номер заказа и сумму. Сопоставьте число событий и заказов в `dds.order_v`. -Теперь рассчитайте клиентскую сумму, учитывая каждый номер заказа один раз. -До выбора одной суммы на заказ проверьте, что во всех его событиях она -одинакова. Проверьте свое решение на случае двух разных заказов с одинаковой -суммой: сколько денег оно учтет? +## Соберите расчет по заказам + +Напишите запрос с одной строкой на номер заказа. Сохраните в результате +номер, число исходных событий и одну заявленную сумму. До выбора суммы +проверьте, что во всех событиях заказа она одинакова. Затем посчитайте итог +дня по результату этого запроса и сравните с расчетом по событиям. + +Проверьте, что номера в результате уникальны, а число строк совпадает с числом +разных номеров в исходных событиях. Отдельно проверьте свое решение на случае +двух разных заказов с одинаковой суммой: сколько денег оно учтет? ## Найдите заказы, из-за которых сумма завышена @@ -49,7 +58,7 @@ его статус и [порядок присвоения классов](../../architecture/dm.md#dmpurchase_vs_orders_v). Класс витрины не заменяет поиск всех повторов в событиях. -## Сравните с выручкой магазина +## Определите границы расчета Сложите `revenue` из `dm.revenue_daily_v` за тот же `report_date` по всем категориям. Эта витрина считает стоимость позиций оплаченных заказов, @@ -60,10 +69,14 @@ заказ, который объясняет, почему деньги трекера и магазина все еще могут различаться. Для этого сопоставьте события, заказ и готовую сверку. +Рядом со своим запросом укажите его зерно и ключ. Обоснуйте, где нужна одна +строка на событие, а где — одна на заказ: следует ли ради этого расчета +удалять повторные покупки из `dds.event_v`? Используйте найденные `event_id`. + ## Что должно получиться -- [ ] сохранены две клиентские суммы и строки повторившихся покупок; +- [ ] сохранен запрос с одной строкой на заказ, числом событий и суммой; +- [ ] уникальность ключа и полнота набора номеров проверены по источнику; - [ ] разность сумм полностью объяснена вкладом лишних событий; -- [ ] выбор ключа для устранения повторов подтвержден данными; -- [ ] вы можете объяснить, почему устранение повторов не завершает сверку - выручки с магазином. +- [ ] обоснованы ключ расчета и место устранения повторов; +- [ ] найден случай, показывающий границы расчета денег по трекеру. diff --git a/docs/labs/optional/missing-purchases.md b/docs/labs/optional/missing-purchases.md index 7e783f2..ff6333a 100644 --- a/docs/labs/optional/missing-purchases.md +++ b/docs/labs/optional/missing-purchases.md @@ -1,7 +1,7 @@ # Куда пропали покупки -**Чему учит:** обнаружить потерю событий при сверке двух источников и -обосновать, каким данным можно доверять при подсчете покупок. +**Чему учит:** проверить полноту данных по независимому источнику и выбрать +источник для витрины заказов. Факультатив доступен после ступени 1. В DBeaver подключитесь пользователем `analyst` по [инструкции](../../../README.md#подключиться-из-dbeaver). @@ -10,8 +10,9 @@ ## Выберите день -Магазин зарегистрировал заказы, но часть событий покупки не дошла до трекера. -Найдите след этой потери сначала в событиях сайта, затем в заказах. +Можно ли построить полную витрину заказов по событиям `purchase`? +Проверьте это на одном дне: сначала сопоставьте события сайта, затем сверьте +номера заказов с выгрузкой магазина. Возьмите день, для которого уже получен последний слепок семидневного окна. Запрос показывает самую позднюю подходящую дату: @@ -46,6 +47,9 @@ FROM ods.order_v; В событии номер заказа находится в `purchase_id[1]`, в заказах — в `order_id`. Соберите список заказов выбранного дня, для которых нет события покупки. Несколько событий с одним номером заказа должны оставаться одной покупкой. +Сохраните запрос сверки: на каждый такой заказ он должен возвращать дату, +номер и статус. Дату задавайте в одном месте, чтобы запрос можно было +повторить для другого дня. Проверьте два равенства: @@ -67,9 +71,6 @@ FROM ods.order_v; к устройству этого стенда; для другого источника его нужно проверять заново. Если в выбранном дне повторов нет, возьмите другой закрытый день. -Для оценки доли потерь используйте число заказов магазина. Затем посчитайте -долю с тем же числителем, но с числом просмотров подтверждения в знаменателе. -Совпали ли результаты? Свяжите различие знаменателей с найденными событиями. ## Сопоставьте вывод с готовой сверкой @@ -80,13 +81,22 @@ FROM ods.order_v; объясните, почему потеря события не всегда попадает в `lost_event`. Возьмите один заказ из своего списка и сохраните два запроса: первый находит -его в магазине, второй показывает отсутствие покупки в трекере. Сформулируйте, -какой источник нужен для отчета о поведении посетителей, а какой — для денег. +его в магазине, второй показывает отсутствие покупки в трекере. + +## Выберите источник и условие проверки + +По результатам сверки укажите источник для витрины, в которой должен быть +каждый зарегистрированный заказ. Обоснуйте выбор найденными номерами. + +Рядом с запросом запишите, когда его результат можно считать окончательным. +Что изменится, если запустить его до получения всех слепков выбранного дня? +Позволяет ли отсутствие события в `dds.event_v` установить место потери — +до приема в хранилище или при обработке? Какие данные нужны, чтобы это выяснить? ## Что должно получиться -- [ ] сохранены день, счетчики воронки и список заказов без события; +- [ ] сохранен запрос сверки по дате, возвращающий заказы без события; - [ ] оба равенства сходятся, влияние повторного просмотра объяснено; -- [ ] доля потерь посчитана с обоснованным знаменателем; - [ ] один случай подтвержден запросами к обоим источникам; -- [ ] вы различаете отсутствие события и класс строки в готовой сверке. +- [ ] источник витрины выбран по результатам сверки; +- [ ] указаны условие окончательной проверки и границы ее выводов. diff --git a/docs/labs/optional/order-amount-difference.md b/docs/labs/optional/order-amount-difference.md index a728fd4..0f18a45 100644 --- a/docs/labs/optional/order-amount-difference.md +++ b/docs/labs/optional/order-amount-difference.md @@ -1,7 +1,7 @@ # Какая позиция объясняет разницу в сумме -**Чему учит:** перейти от расхождения суммы заказа к конкретному товару и -проверить объяснение с точностью до копейки. +**Чему учит:** разобрать вложенные данные без потери точности и локализовать +расхождение двух источников до товарной позиции. Факультатив доступен после ступени 1. В DBeaver используйте пользователя `analyst` из [инструкции подключения](../../../README.md#подключиться-из-dbeaver). @@ -10,6 +10,9 @@ ## Найдите заказ +Суммы покупки в трекере и заказа в магазине различаются. Разберите их позиции +и проверьте отдельно точность своего запроса и различия между событием и заказом. + В `dm.purchase_vs_orders_v` выберите один заказ с `mismatch_class = 'amount_delta'`. Возьмите закрытый день: `order_day` не позднее `max(snapshot_date) - 6` из `ods.order_v`. Сохраните `order_id`, @@ -40,7 +43,9 @@ Сопоставьте позиции по артикулу так, чтобы остались видны товары, присутствующие только с одной стороны. Для каждой позиции сравните наличие, цену и количество. Что различается и сколько это добавляет к расхождению -сумм? В результате нужны строки товаров, а не только две итоговые суммы. +сумм? +Сохраните запрос сверки для выбранных даты и номера заказа; в результате +оставьте артикул, значения с обеих сторон и разность стоимостей позиций. ## Проверьте объяснение @@ -58,14 +63,22 @@ [форматом выгрузки](../../formats/clickstream-event.md). Если все цены выбранной корзины целые, возьмите еще один заказ с расхождением и дробной ценой. -В коротком объяснении назовите заказ, товар, количество, цену и разницу. Какие строки подтверждают найденное расхождение? Есть ли в источниках сведения о том, почему оно возникло? Отделите наблюдения от предположений. +## Выберите поля для точного расчета + +Укажите рядом с запросом, откуда брать цену, каким типом ее читать и по какому +ключу сопоставлять позиции. Подтвердите выбор результатами обеих сверок: +суммы разобранных позиций с суммой своего источника и позиций двух источников +между собой. Какой результат укажет на ошибку вашего разбора, а какой +потребует искать причину выше — в данных, которые получил запрос? + ## Что должно получиться -- [ ] позиции клиента и магазина сопоставлены по артикулам; +- [ ] сохранен запрос сверки позиций, не теряющий товары без пары; - [ ] все три равенства сходятся до копейки; - [ ] найден пример потери точности в готовом массиве цен; -- [ ] расхождение объяснено конкретной позицией, а скидка и доставка - отделены от сравнения корзин. +- [ ] выбор полей, типа денег и ключа сопоставления подтвержден расчетами; +- [ ] расхождение источников сверки отделено от ошибки своего разбора и различия + между стоимостью корзины и итогом со скидкой и доставкой. diff --git a/docs/labs/optional/revenue-over-time.md b/docs/labs/optional/revenue-over-time.md index 5fef588..2b177a0 100644 --- a/docs/labs/optional/revenue-over-time.md +++ b/docs/labs/optional/revenue-over-time.md @@ -1,13 +1,13 @@ # Почему вчерашняя выручка меняется сегодня -**Чему учит:** объяснять изменения уже рассчитанного отчета поступлением -заказов и сменой их статусов, определять момент закрытия дня и границы -доступной истории. +**Чему учит:** обосновать окно пересчета витрины и требования к истории +заказов по наблюдаемым опозданиям и изменениям статусов. Факультатив доступен после ступени 1. Понадобятся Airflow и DBeaver с подключением `analyst` по [инструкции](../../../README.md#подключиться-из-dbeaver). Вы будете наблюдать один день оформления заказов, продвигая мир по одному -модельному дню. Новые данные останутся в стенде. +модельному дню. Проверьте, можно ли при загрузке выручки каждый раз считать +только новый день. Новые данные останутся в стенде. ## Подготовьте первое наблюдение @@ -73,7 +73,7 @@ SELECT в конце окна и за его пределами. От исходного наблюдения потребуется восемь однодневных запусков. -## Объясните изменения отчета +## Найдите изменения в уже загруженном дне После каждого шага сравнивайте заказы по `order_id`, а не по положению строк в выгрузке. Найдите: @@ -93,11 +93,12 @@ SELECT выше; учтите и заказы, которых еще не было в первом наблюдении. Сумма изменений вкладов должна совпасть с изменением `revenue` до копейки. -Для одного из этих отмененных заказов сравните изменение его вклада -с изменением общей выручки между теми же наблюдениями. Совпало ли -направление изменений? Найдите в своей таблице заказы, определившие общий итог. +Покажите на этих наблюдениях, какой результат остался бы в витрине, +если бы конвейер после первого расчета обрабатывал только новые дни заказов. +Достаточно ли добавлять в старый день только опоздавшие заказы? Проверьте +свой ответ на найденных изменениях статусов. -## Что известно об отменах +## Определите, какую историю нужно хранить Возьмите отмененный заказ, у которого в вашей истории был статус `paid`. Укажите два наблюдения, доказывающих, что он был оплачен, а затем отменен. @@ -105,9 +106,14 @@ SELECT Теперь возьмите отмененный заказ, для которого ни в одной вашей выгрузке нет `paid`. Проверьте утверждение «этот заказ никогда не был оплачен»: что известно о его состоянии между двумя слепками? Достаточно ли сохраненных -наблюдений для такого вывода? Укажите, какие данные позволили бы его проверить. +наблюдений для такого вывода? -## Когда день закрыт +Сформулируйте требования к данным для двух задач: восстановить состояние +витрины на момент прошлого расчета и узнать, был ли заказ когда-либо оплачен. +Какие сведения для каждой задачи должен передавать источник и сохранять +хранилище? Проверьте предложение на своих выгрузках и на промежутке между ними. + +## Задайте правило пересчета Сравните сохраненные состояния при S = D + 6 и S = D + 7: состав заказов, их статусы, суммы и выручку. Какой слепок последним мог изменить заказы дня D @@ -115,6 +121,12 @@ SELECT можно считать окончательным. Было бы достаточно совпадения двух соседних сумм, если бы окно еще не закончилось? +Запишите правило выбора дней для пересчета после очередного слепка. +Считайте, что начальная история уже загружена и ежедневные прогоны не +пропускались. Укажите, по какой дате определяются границы, какие дни нужно +пересчитать и когда день можно перестать включать в расчет. Подтвердите +правило своими наблюдениями, включая опоздание и отмену. + После собственных выводов сверьтесь с описанием [слепка и его доставки](../../architecture/orders/snapshot.md#слепок-и-его-доставка) и [границ истории отмен](../../architecture/orders/fate.md#судьба-заказа). @@ -122,12 +134,13 @@ SELECT ## Что должно получиться - [ ] сохранена последовательность наблюдений одного дня D с датами слепков S; -- [ ] найдены опоздавшие заказы и изменения статусов; -- [ ] изменение выручки между двумя наблюдениями объяснено вкладами заказов; +- [ ] на опоздавших заказах и изменениях статусов показаны ошибки загрузки + только новых дней или только новых заказов; +- [ ] изменение выручки между наблюдениями сходится с изменением вкладов заказов; - [ ] оплата одной из последующих отмен подтверждена двумя наблюдениями; -- [ ] вы различаете доказанную оплату и отсутствие сведений о ней; -- [ ] состояние дня за пределами окна совпало с окончательным, а момент - закрытия обоснован правилом доставки слепков. +- [ ] сформулированы требования к истории расчетов и истории оплаты; +- [ ] правило пересчета и момент закрытия дня обоснованы наблюдениями + и правилом доставки слепков. После работы оставьте `world_live` на паузе либо верните автоматическое продвижение, если пользовались им до наблюдения. diff --git a/docs/specs/2026-09-01-labs-program.md b/docs/specs/2026-09-01-labs-program.md index 2796b44..9a88319 100644 --- a/docs/specs/2026-09-01-labs-program.md +++ b/docs/specs/2026-09-01-labs-program.md @@ -161,15 +161,28 @@ изменение выручки и история отмен используют одну историю заказов, поэтому для них выбрана общая работа вместо трех отдельных. +Результат расследования — запрос сверки и инженерное решение, подтвержденное +данными: выбор источника, ключа, полей для точного расчета или правила +пересчета и хранения истории. Объяснение суммы служит проверкой решения. +Вариант закончить работы только объяснением показателей отклонен: он +смещал акцент в анализ отчетов. Постройка новых DAG и подключение проверок +к конвейеру оставлены основной программе, чтобы факультативы не повторяли +ее задания. + - **[Куда пропали покупки](../labs/optional/missing-purchases.md) (#186).** - Учащийся объясняет разрыв воронки заказами, для которых нет события покупки. + Сверка по номерам заказов обосновывает источник витрины и условие + окончательной проверки полноты. Счетчики событий помогают обнаружить + расхождение, но не устанавливают место потери. - **[Почему повтор покупки завышает выручку](../labs/optional/duplicate-purchases.md) (#187).** - Разность клиентских сумм объясняется повторными событиями одной покупки. + Учащийся собирает расчет с одной строкой на заказ, проверяет ключ и полноту + набора номеров, отличает его зерно от зерна исходных событий. - **[Какая позиция объясняет разницу в сумме](../labs/optional/order-amount-difference.md).** - Сравнение позиций из исходного JSON и заказа объясняет расхождение до копейки. + Сверка позиций обосновывает выбор исходных полей и типа денег, отделяет + ошибку собственного разбора от различия данных события и заказа. - **[Почему вчерашняя выручка меняется сегодня](../labs/optional/revenue-over-time.md) (#188).** - История одного дня связывает опоздания, оплату и отмены с изменением выручки, - позволяет определить момент закрытия дня и границу знания об оплате. + Наблюдение опозданий и смены статусов обосновывает правило пересчета при + ежедневной загрузке без пропусков. Учащийся определяет момент закрытия дня + и требования к истории для воспроизведения расчета и проверки факта оплаты. В четвертой лабораторной выбрано собственное наблюдение: учащийся продвигает мир по одному дню через Airflow и сохраняет результаты после обновления витрин. From bb8605eed7d80e7d3ceef94deee77a0710f7d78e Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Tue, 22 Sep 2026 04:56:45 -0400 Subject: [PATCH 7/7] =?UTF-8?q?feat(labs):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE=20=D1=81=D0=BE=D1=85=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BD=D0=B0=D0=B1=D0=BB=D1=8E?= =?UTF-8?q?=D0=B4=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=BF=D0=BE=D0=B4=20=D1=83?= =?UTF-8?q?=D1=87=D0=B5=D0=B1=D0=BD=D0=BE=D0=B9=20=D1=83=D1=87=D0=B5=D1=82?= =?UTF-8?q?=D0=BA=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Зачем: студенту нужен понятный путь от сохраненного состояния к SQL-сравнению без доступа технической учетной записи к изменению хранилища. Что: добавлен student с чтением источников и записью только в default; лабораторная сохраняет заказы и итоги в две локальные таблицы. Обновлены настройка доступа и программа, уточнено округление productPrice. Проверка: DDL и INSERT SELECT под student, пустое наблюдение, восемь переходов сохраненной истории и запрет вставки в пять прикладных баз на обеих нодах. config-test, lint стенда и генератора, 54 ссылки, независимое ревью. --- .env.example | 1 + README.md | 3 + compose.yaml | 1 + docs/adr/0007-clickhouse-access.md | 12 ++- docs/architecture/stand.md | 8 +- docs/formats/clickstream-event.md | 2 +- docs/labs/optional/revenue-over-time.md | 96 +++++++++++++------ docs/specs/2026-09-01-labs-program.md | 8 +- generator/src/clickstream_generator/schema.py | 2 +- infra/clickhouse/users.d/access.xml | 13 +++ 10 files changed, 114 insertions(+), 32 deletions(-) diff --git a/.env.example b/.env.example index 6469fa9..718dbec 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,7 @@ CLICKHOUSE_ETL_PASSWORD=etl-26-writer CLICKHOUSE_LIFECYCLE_PASSWORD=lifecycle-26-owner CLICKHOUSE_BI_PASSWORD=bi-26-reader CLICKHOUSE_ANALYST_PASSWORD=analyst-26-reader +CLICKHOUSE_STUDENT_PASSWORD=student-26-lab CLICKHOUSE_GRAFANA_PASSWORD=grafana-26-reader KAFKA_EXTERNAL_PORT=29092 PROMETHEUS_PORT=29090 diff --git a/README.md b/README.md index 1cf9769..38cac01 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,9 @@ Kafka, затем заново создаст схему хранилища и Для подключения ко второму узлу укажите порт `28124`. Пользователь `analyst` может читать данные во всех слоях и справочниках. +Для заданий с собственными таблицами наблюдений используйте `student` и пароль +из `CLICKHOUSE_STUDENT_PASSWORD` в `.env`. Он читает те же источники, а создавать, +заполнять и удалять таблицы может только в базе `default`. Для создания учебных витрин используйте подключение, описанное в [инструкции к заданиям](docs/labs/README.md#как-применить-свой-ddl). Нативные порты `29000` и `29001` нужны `clickhouse-client`, но не DBeaver. diff --git a/compose.yaml b/compose.yaml index df87088..16b2a3e 100644 --- a/compose.yaml +++ b/compose.yaml @@ -20,6 +20,7 @@ x-clickhouse-common: &clickhouse-common CLICKHOUSE_LIFECYCLE_PASSWORD: ${CLICKHOUSE_LIFECYCLE_PASSWORD:?Скопируйте .env.example в .env} CLICKHOUSE_BI_PASSWORD: ${CLICKHOUSE_BI_PASSWORD:?Скопируйте .env.example в .env} CLICKHOUSE_ANALYST_PASSWORD: ${CLICKHOUSE_ANALYST_PASSWORD:?Скопируйте .env.example в .env} + CLICKHOUSE_STUDENT_PASSWORD: ${CLICKHOUSE_STUDENT_PASSWORD:?Добавьте CLICKHOUSE_STUDENT_PASSWORD из .env.example в .env} CLICKHOUSE_GRAFANA_PASSWORD: ${CLICKHOUSE_GRAFANA_PASSWORD:?Скопируйте .env.example в .env} depends_on: clickhouse-keeper: diff --git a/docs/adr/0007-clickhouse-access.md b/docs/adr/0007-clickhouse-access.md index e322580..a027322 100644 --- a/docs/adr/0007-clickhouse-access.md +++ b/docs/adr/0007-clickhouse-access.md @@ -3,10 +3,11 @@ Дата: 9 августа 2026 года. Статус: принято; роль `monitoring_reader` и пользователь `grafana` добавлены 21 августа 2026 года (#114), права `bi_reader` расширены до ODS, DDS и DM 25 августа 2026 года (#150). +Пользователь `student` и роль `student_writer` добавлены 22 сентября 2026 года. ## Решение -У кластера семь пользователей и шесть ролей. +У кластера восемь пользователей и семь ролей. - `default` — с паролем, только служебный: проверки здоровья нод и работа изнутри контейнеров. Приложения им не ходят. @@ -20,6 +21,10 @@ шире. - `analyst` с ролью `analyst_reader` — чтение всех слоёв и справочников. Им человек подключается снаружи, из своего клиента. +- `student` с ролями `analyst_reader` и `student_writer` — то же чтение и + работа со своими таблицами в `default`: создание, вставка и удаление. + Сохранение учебных наблюдений не требует прав технической учётной записи + `etl` на изменение слоёв. Таблицы локальные; работа ведётся на одной ноде. - `grafana` с ролью `monitoring_reader` — чтение слоёв, таблиц базы `dic` и системных таблиц, `CLUSTER` и `REMOTE`. Им ходит Grafana, и больше никто. - `dict` с ролью `dict_reader` — чтение одной базы `dic`, пароля нет. Им @@ -148,6 +153,11 @@ DDL для матвью приёма выполняется от имени `lif ## Что проверено +Права `student` сверены 22 сентября 2026 года через MCP Context7 и на стенде +26.3.17.56: создание локальных `MergeTree`, `INSERT SELECT` из DDS и DM, +чтение и удаление своих таблиц работают. На обеих нодах вставка в STG, ODS, +DDS, DM и `dic` завершается `ACCESS_DENIED`. + Сверено 9 августа 2026 года по документации ClickHouse через MCP Context7 и на живом стенде 26.3.17.56. diff --git a/docs/architecture/stand.md b/docs/architecture/stand.md index ada8bff..e1637ce 100644 --- a/docs/architecture/stand.md +++ b/docs/architecture/stand.md @@ -22,7 +22,7 @@ ## Пользователи и роли ClickHouse -В ClickHouse семь пользователей и шесть ролей: +В ClickHouse восемь пользователей и семь ролей: - `etl` — учётная запись рабочих дагов Airflow; роль `etl_writer` читает и пишет слои хранилища; @@ -33,6 +33,9 @@ справочники `dic`, а запросы Superset обращаются к публичным `_v`; - `analyst` — учётная запись аналитика с правом чтения всех слоёв и справочников; +- `student` — учебная учётная запись: роль `analyst_reader` дает чтение + источников, а `student_writer` — создание, заполнение и удаление своих + таблиц в `default`. Записи в слои хранилища у нее нет; - `grafana` — учётная запись Grafana; роль `monitoring_reader` читает все слои, системные таблицы и данные со всех реплик кластера; - `dict` — беспарольная учётная запись словаря товаров с правом чтения базы @@ -49,6 +52,9 @@ Superset получает пароль `bi`, а Grafana — пароль `grafan ## Применение настроек +Для существующего стенда сначала добавьте `CLICKHOUSE_STUDENT_PASSWORD` +из `.env.example` в свой `.env`. + Чтобы применить новые значения ClickHouse из `.env` или изменения файлов в `infra/clickhouse/config.d/` и `infra/clickhouse/users.d/`, пересоздайте ноды: diff --git a/docs/formats/clickstream-event.md b/docs/formats/clickstream-event.md index 0f9028e..bdeeb42 100644 --- a/docs/formats/clickstream-event.md +++ b/docs/formats/clickstream-event.md @@ -98,7 +98,7 @@ DDS складывает свою модель и называет атрибу | 41 | `productID` | `Array(String)` | `object` | `product_id` | id товаров события | | 42 | `productName` | `Array(String)` | `object` | `product_name` | названия тех же товаров | | 43 | `productCategory` | `Array(String)` | `object` | `product_category` | категории тех же товаров | -| 44 | `productPrice` | `Array(Int64)` | `int64` | `product_price` | цена за штуку целым числом: деньги генератор считает целыми | +| 44 | `productPrice` | `Array(Int64)` | `int64` | `product_price` | цена за штуку, округленная до целых рублей | | 45 | `productQuantity` | `Array(UInt64)` | `uint64` | `product_quantity` | количество штук каждого товара | | 46 | `productEventType` | `Array(String)` | `object` | `product_event_type` | действие с товаром: стенд шлёт add и purchase, полный словарь Метрики (detail, remove, impressions) не берём | | 47 | `ecommerce` | `String` | `object` | `ecommerce` | сырой JSON события, как отдаёт Метрика — материал лабы про разбор JSON внутри колонки | diff --git a/docs/labs/optional/revenue-over-time.md b/docs/labs/optional/revenue-over-time.md index 2b177a0..469236e 100644 --- a/docs/labs/optional/revenue-over-time.md +++ b/docs/labs/optional/revenue-over-time.md @@ -4,7 +4,8 @@ заказов по наблюдаемым опозданиям и изменениям статусов. Факультатив доступен после ступени 1. Понадобятся Airflow и DBeaver с -подключением `analyst` по [инструкции](../../../README.md#подключиться-из-dbeaver). +подключением `student` по [инструкции](../../../README.md#подключиться-из-dbeaver). +Он читает данные стенда и записывает ваши наблюдения в базу `default`. Вы будете наблюдать один день оформления заказов, продвигая мир по одному модельному дню. Проверьте, можно ли при загрузке выручки каждый раз считать только новый день. Новые данные останутся в стенде. @@ -32,28 +33,65 @@ SELECT хранилище еще нет. Проверьте это для D в `dds.order_v` и `dm.purchase_vs_orders_v`. -## Подготовьте запросы и сохраняйте результаты - -Напишите запросы для следующих наблюдений: - -| Что сохранить | Откуда взять | -|---|---| -| Дату полученного слепка S | `max(snapshot_date)` из `ods.order_v` | -| Номер, статус и `items_total` каждого заказа дня D | `dds.order_v`, отбор по `order_date` | -| Число заказов в каждом статусе и сумму `items_total` только у `paid` | те же заказы дня D | -| Выручку дня D | сумма `revenue` из `dm.revenue_daily_v`, отбор по `report_date` | -| Номера покупок, еще ожидающих заказ | строки `awaiting_order` из `dm.purchase_vs_orders_v`, отбор по `order_day` | - -Выручку складывайте по всем категориям. В этой витрине она означает стоимость -позиций оплаченных заказов, без скидки и доставки, поэтому должна сходиться -с суммой `items_total` у заказов в статусе `paid`. - -Сохраняйте результаты запросов отдельными файлами, например выгрузками CSV -из DBeaver. Подписывайте D и S. До поступления первого слепка список заказов -пуст, а выручка равна нулю; сохраните и это наблюдение. +## Подготовьте место для наблюдений Публичное представление заказа показывает текущее состояние, а сырые слепки -хранятся ограниченное время. Сохраненные выгрузки — история для расследования. +хранятся ограниченное время. Сохраните историю в двух своих таблицах: потом +вы сможете сравнивать наблюдения обычными SQL-запросами в том же DBeaver. + +Выполните DDL ниже. Это локальные таблицы на узле вашего подключения; +всю работу выполняйте через одно подключение. Если имена уже заняты вашей +прошлой работой, выберите другие и используйте их в дальнейших запросах. + +```sql +CREATE TABLE default.lab_order_history +( + order_date Date, + snapshot_date Date, + order_id String, + status String, + items_total Decimal(18, 2) +) +ENGINE = MergeTree +ORDER BY (order_date, snapshot_date, order_id); + +CREATE TABLE default.lab_revenue_history +( + order_date Date, + snapshot_date Date, + revenue Decimal(18, 2), + awaiting_order_ids Array(String) +) +ENGINE = MergeTree +ORDER BY (order_date, snapshot_date); +``` + +## Сохраните первое наблюдение + +Напишите два `INSERT INTO ... SELECT` с явным списком колонок назначения: + +| Куда записать | Что взять из стенда | +|---|---| +| `default.lab_order_history` | По строке на заказ дня D из `dds.order_v`: `order_date`, `order_id`, `status`, `items_total`; добавьте дату наблюдения S | +| `default.lab_revenue_history` | Одну строку с D, S, суммой `revenue` за `report_date = D` из `dm.revenue_daily_v` и массивом номеров `awaiting_order` за `order_day = D` из `dm.purchase_vs_orders_v` | + +Во втором запросе сумму и массив получите отдельными агрегатами, чтобы +заказы не умножили строки выручки при соединении. Для массива пригодится +`groupArray(order_id)`. Дату S берите из `max(snapshot_date)` в `ods.order_v`. + +Сначала выполните части `SELECT` отдельно и проверьте результаты, затем +сохраните их. До первого слепка заказов дня D еще нет: в таблицу заказов +строки не добавятся, но в таблице итогов должна появиться строка с нулевой +выручкой. Она зафиксирует, что наблюдение сделано. + +Каждую пару D и S сохраняйте один раз: повторный `INSERT` добавит копии строк. +После записи проверьте, что на эту пару приходится одна строка итогов, +а число заказов и число разных `order_id` совпадают с `dds.order_v` за D. + +Выручка этой витрины — стоимость позиций оплаченных заказов без скидки и +доставки. Для каждого сохраненного наблюдения посчитайте число заказов по +статусам и сумму `items_total` только у `paid`. Эта сумма должна сходиться +с сохраненной выручкой, сложенной по всем категориям. ## Продвигайте мир по одному дню @@ -62,7 +100,7 @@ SELECT 1. Запустите `world_next_day` с `days = 1` и дождитесь успеха. 2. Запустите `etl_pipeline` и дождитесь успеха: `world_next_day` сам витрины не обновляет. -3. Повторите свои запросы и сохраните новый комплект результатов с датой S. +3. Повторите два запроса сохранения с новой датой S и проверьте записанные строки. Во всех запросах остается прежний день D. За один запуск проигрывайте один день, чтобы сохранить промежуточные состояния. Пакетный `world_next_day` @@ -75,8 +113,10 @@ SELECT ## Найдите изменения в уже загруженном дне -После каждого шага сравнивайте заказы по `order_id`, а не по положению строк -в выгрузке. Найдите: +Для сравнения прочитайте `default.lab_order_history` дважды: в одном +подзапросе отберите прежнюю S, в другом — новую; день D остается общим. +Соедините состояния по `order_id`, сохранив и заказы без пары. Итоги для этих +же D и S возьмите из `default.lab_revenue_history`. Найдите: - заказы, отсутствовавшие в первом слепке D, но появившиеся в следующем; - переходы из `created` в `paid`; @@ -103,7 +143,7 @@ SELECT Возьмите отмененный заказ, у которого в вашей истории был статус `paid`. Укажите два наблюдения, доказывающих, что он был оплачен, а затем отменен. -Теперь возьмите отмененный заказ, для которого ни в одной вашей выгрузке +Теперь возьмите отмененный заказ, для которого ни в одном вашем наблюдении нет `paid`. Проверьте утверждение «этот заказ никогда не был оплачен»: что известно о его состоянии между двумя слепками? Достаточно ли сохраненных наблюдений для такого вывода? @@ -111,7 +151,7 @@ SELECT Сформулируйте требования к данным для двух задач: восстановить состояние витрины на момент прошлого расчета и узнать, был ли заказ когда-либо оплачен. Какие сведения для каждой задачи должен передавать источник и сохранять -хранилище? Проверьте предложение на своих выгрузках и на промежутке между ними. +хранилище? Проверьте, каких нужных сведений нет в ваших таблицах наблюдений. ## Задайте правило пересчета @@ -143,4 +183,6 @@ SELECT и правилом доставки слепков. После работы оставьте `world_live` на паузе либо верните автоматическое -продвижение, если пользовались им до наблюдения. +продвижение, если пользовались им до наблюдения. Таблицы истории останутся +для повторной проверки выводов. Когда они больше не нужны, удалите только +свои `default.lab_order_history` и `default.lab_revenue_history`. diff --git a/docs/specs/2026-09-01-labs-program.md b/docs/specs/2026-09-01-labs-program.md index 9a88319..369e665 100644 --- a/docs/specs/2026-09-01-labs-program.md +++ b/docs/specs/2026-09-01-labs-program.md @@ -185,7 +185,13 @@ и требования к истории для воспроизведения расчета и проверки факта оплаты. В четвертой лабораторной выбрано собственное наблюдение: учащийся продвигает -мир по одному дню через Airflow и сохраняет результаты после обновления витрин. +мир по одному дню через Airflow и сохраняет результаты после обновления витрин +в две локальные таблицы ClickHouse под `student`: состояния заказов и итоги +наблюдений. Сравнение выполняется SQL-запросами в том же подключении DBeaver. +Учащийся может писать только в `default`; источники доступны ему на чтение. +Отдельная строка итогов фиксирует и наблюдение без заказов. Экспорт в CSV +отклонен как основной способ: он требовал неописанного перехода к другому +инструменту или обратному импорту файлов. Готовые слепки и сочетание готового примера с собственным наблюдением отклонены: нужную историю можно получить на стенде и самому увидеть, как новые данные меняют уже рассчитанный отчет. diff --git a/generator/src/clickstream_generator/schema.py b/generator/src/clickstream_generator/schema.py index d31d3f4..0082f80 100644 --- a/generator/src/clickstream_generator/schema.py +++ b/generator/src/clickstream_generator/schema.py @@ -420,7 +420,7 @@ COLUMNS: tuple[Column, ...] = ( numpy_dtype="int64", normalized_name="product_price", group=ColumnGroup.ECOMMERCE, - comment="цена за штуку целым числом: деньги генератор считает целыми", + comment="цена за штуку, округленная до целых рублей", ), Column( name="productQuantity", diff --git a/infra/clickhouse/users.d/access.xml b/infra/clickhouse/users.d/access.xml index 04cc4a0..5cf2bd5 100644 --- a/infra/clickhouse/users.d/access.xml +++ b/infra/clickhouse/users.d/access.xml @@ -61,6 +61,11 @@ GRANT SELECT ON system.settings + + + GRANT SELECT, INSERT, CREATE TABLE, DROP TABLE ON default.* + + GRANT SELECT ON stg.* @@ -113,6 +118,14 @@ GRANT analyst_reader + + + default + + GRANT analyst_reader + GRANT student_writer + + default