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/AGENTS.md b/AGENTS.md index 594c75e..d8621b2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,15 +46,10 @@ ## Кому что поручать -Читатель здесь не побочный потребитель, а тот, ради кого стенд существует: -код, комментарии и документы и есть продукт. Поэтому всё, что человек будет -читать и разбирать, пишет модель с чувством меры — сейчас это Opus. Codex -берёт техническую работу без вкусовых решений: миграции, разбор журналов и -объёмного вывода, обход репозитория, повторяющиеся правки. Там его запас -лимитов работает на нас, а экономить их на читаемом коде — экономия ложная: -переписывать выйдет дороже, чем сразу написать хорошо. +Автор задачи сам пишет и редактирует код, комментарии и документы, если +владелец не задал другое распределение работы. -Делить работу между ними стоит не целыми задачами, а подзадачами, и решать +Делить работу стоит не целыми задачами, а подзадачами, и решать это при планировании. Вкусовая часть идёт первой: она задаёт форму, имена и границы, под которые механическая потом подстраивается. 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/README.md b/docs/labs/README.md index cf593f1..59a2ff7 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/duplicate-purchases.md b/docs/labs/optional/duplicate-purchases.md new file mode 100644 index 0000000..09686ce --- /dev/null +++ b/docs/labs/optional/duplicate-purchases.md @@ -0,0 +1,82 @@ +# Почему повтор покупки завышает выручку + +**Чему учит:** выбрать зерно и ключ расчета, чтобы повторные события +не умножали денежную сумму. + +Факультатив доступен после ступени 1. Работайте в DBeaver пользователем +`analyst` по [инструкции](../../../README.md#подключиться-из-dbeaver). +Дождитесь завершения `etl_pipeline`; оставьте `world_live` на паузе и +дождитесь уже начатых прогонов, чтобы данные не менялись между запросами. + +## Проверьте расчет по событиям + +В ODS уже устранены повторные доставки событий. Достаточно ли этого, +чтобы сложить суммы всех `purchase` для витрины заказов? Проверьте на данных. + +Возьмите один закрытый день: его дата должна быть не позднее +`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` читает уже это состояние. Сравните число разных `event_id` +и число разных номеров заказа в выбранных строках. Почему устранение +повторов по первому ключу не дало тот же результат, что по второму? +Подтвердите ответ конкретными идентификаторами. + +После собственного расчета найдите эти заказы в `dm.purchase_vs_orders_v`. +Если у повторившегося заказа класс отличается от `duplicate_event`, проверьте +его статус и [порядок присвоения классов](../../architecture/dm.md#dmpurchase_vs_orders_v). +Класс витрины не заменяет поиск всех повторов в событиях. + +## Определите границы расчета + +Сложите `revenue` из `dm.revenue_daily_v` за тот же `report_date` по всем +категориям. Эта витрина считает стоимость позиций оплаченных заказов, +без скидки и доставки. + +Сравните результат с исправленной клиентской суммой. Совпадение не требуется: +удаление повторов устраняет одну причину расхождения. Найдите хотя бы один +заказ, который объясняет, почему деньги трекера и магазина все еще могут +различаться. Для этого сопоставьте события, заказ и готовую сверку. + +Рядом со своим запросом укажите его зерно и ключ. Обоснуйте, где нужна одна +строка на событие, а где — одна на заказ: следует ли ради этого расчета +удалять повторные покупки из `dds.event_v`? Используйте найденные `event_id`. + +## Что должно получиться + +- [ ] сохранен запрос с одной строкой на заказ, числом событий и суммой; +- [ ] уникальность ключа и полнота набора номеров проверены по источнику; +- [ ] разность сумм полностью объяснена вкладом лишних событий; +- [ ] обоснованы ключ расчета и место устранения повторов; +- [ ] найден случай, показывающий границы расчета денег по трекеру. diff --git a/docs/labs/optional/missing-purchases.md b/docs/labs/optional/missing-purchases.md new file mode 100644 index 0000000..ff6333a --- /dev/null +++ b/docs/labs/optional/missing-purchases.md @@ -0,0 +1,102 @@ +# Куда пропали покупки + +**Чему учит:** проверить полноту данных по независимому источнику и выбрать +источник для витрины заказов. + +Факультатив доступен после ступени 1. В DBeaver подключитесь пользователем +`analyst` по [инструкции](../../../README.md#подключиться-из-dbeaver). +Дождитесь завершения `etl_pipeline`; на время расследования оставьте +`world_live` на паузе и дождитесь уже начатых прогонов. + +## Выберите день + +Можно ли построить полную витрину заказов по событиям `purchase`? +Проверьте это на одном дне: сначала сопоставьте события сайта, затем сверьте +номера заказов с выгрузкой магазина. + +Возьмите день, для которого уже получен последний слепок семидневного окна. +Запрос показывает самую позднюю подходящую дату: + +```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`. +Соберите список заказов выбранного дня, для которых нет события покупки. +Несколько событий с одним номером заказа должны оставаться одной покупкой. +Сохраните запрос сверки: на каждый такой заказ он должен возвращать дату, +номер и статус. Дату задавайте в одном месте, чтобы запрос можно было +повторить для другого дня. + +Проверьте два равенства: + +- число заказов магазина равно числу разных заказов из событий плюс число + заказов без события; +- разность просмотров подтверждения и событий покупки равна числу заказов + без события. + +Первое равенство проверяет полноту сопоставления источников. Второе связывает +разность счетчиков с найденными заказами без события. Проверьте, как на него +влияют повторные покупки. + +Найдите номер заказа с несколькими событиями `purchase`. Для его `client_id` +выведите события рядом с этими покупками по времени: `event_time`, +`event_type` и `url`. Найдите просмотры `/confirmation` перед первой +и повторной покупкой. Посчитайте, сколько повтор добавил просмотров +подтверждения и сколько — покупок. Изменилась ли от этих добавлений разность +счетчиков? Подтвердите ответ найденными строками. Второе равенство относится +к устройству этого стенда; для другого источника его нужно проверять заново. + +Если в выбранном дне повторов нет, возьмите другой закрытый день. + +## Сопоставьте вывод с готовой сверкой + +Найдите свои заказы без события в `dm.purchase_vs_orders_v` по `order_id`. +Сравните их `status` и `mismatch_class`. Витрина присваивает заказу один класс, +хотя причин расхождения может быть несколько. По +[правилу классификации](../../architecture/dm.md#dmpurchase_vs_orders_v) +объясните, почему потеря события не всегда попадает в `lost_event`. + +Возьмите один заказ из своего списка и сохраните два запроса: первый находит +его в магазине, второй показывает отсутствие покупки в трекере. + +## Выберите источник и условие проверки + +По результатам сверки укажите источник для витрины, в которой должен быть +каждый зарегистрированный заказ. Обоснуйте выбор найденными номерами. + +Рядом с запросом запишите, когда его результат можно считать окончательным. +Что изменится, если запустить его до получения всех слепков выбранного дня? +Позволяет ли отсутствие события в `dds.event_v` установить место потери — +до приема в хранилище или при обработке? Какие данные нужны, чтобы это выяснить? + +## Что должно получиться + +- [ ] сохранен запрос сверки по дате, возвращающий заказы без события; +- [ ] оба равенства сходятся, влияние повторного просмотра объяснено; +- [ ] один случай подтвержден запросами к обоим источникам; +- [ ] источник витрины выбран по результатам сверки; +- [ ] указаны условие окончательной проверки и границы ее выводов. diff --git a/docs/labs/optional/order-amount-difference.md b/docs/labs/optional/order-amount-difference.md new file mode 100644 index 0000000..0f18a45 --- /dev/null +++ b/docs/labs/optional/order-amount-difference.md @@ -0,0 +1,84 @@ +# Какая позиция объясняет разницу в сумме + +**Чему учит:** разобрать вложенные данные без потери точности и локализовать +расхождение двух источников до товарной позиции. + +Факультатив доступен после ступени 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`, если мы ищем расхождение именно в составе корзины? +Проверьте свой выбор после разбора позиций. + +## Разберите позиции с двух сторон + +Найдите событие покупки в `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` +события. Пересчитайте стоимость клиентской корзины по этому массиву: +сойдется ли она с `declared_revenue` до копейки? Найдите цены, на которых +результаты различаются, и сверьте наблюдение с +[форматом выгрузки](../../formats/clickstream-event.md). Если все цены +выбранной корзины целые, возьмите еще один заказ с расхождением и дробной ценой. + +Какие строки подтверждают найденное расхождение? Есть ли в источниках +сведения о том, почему оно возникло? Отделите наблюдения от предположений. + +## Выберите поля для точного расчета + +Укажите рядом с запросом, откуда брать цену, каким типом ее читать и по какому +ключу сопоставлять позиции. Подтвердите выбор результатами обеих сверок: +суммы разобранных позиций с суммой своего источника и позиций двух источников +между собой. Какой результат укажет на ошибку вашего разбора, а какой +потребует искать причину выше — в данных, которые получил запрос? + +## Что должно получиться + +- [ ] сохранен запрос сверки позиций, не теряющий товары без пары; +- [ ] все три равенства сходятся до копейки; +- [ ] найден пример потери точности в готовом массиве цен; +- [ ] выбор полей, типа денег и ключа сопоставления подтвержден расчетами; +- [ ] расхождение источников сверки отделено от ошибки своего разбора и различия + между стоимостью корзины и итогом со скидкой и доставкой. diff --git a/docs/labs/optional/revenue-over-time.md b/docs/labs/optional/revenue-over-time.md new file mode 100644 index 0000000..469236e --- /dev/null +++ b/docs/labs/optional/revenue-over-time.md @@ -0,0 +1,188 @@ +# Почему вчерашняя выручка меняется сегодня + +**Чему учит:** обосновать окно пересчета витрины и требования к истории +заказов по наблюдаемым опозданиям и изменениям статусов. + +Факультатив доступен после ступени 1. Понадобятся Airflow и DBeaver с +подключением `student` по [инструкции](../../../README.md#подключиться-из-dbeaver). +Он читает данные стенда и записывает ваши наблюдения в базу `default`. +Вы будете наблюдать один день оформления заказов, продвигая мир по одному +модельному дню. Проверьте, можно ли при загрузке выручки каждый раз считать +только новый день. Новые данные останутся в стенде. + +## Подготовьте первое наблюдение + +Поставьте `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`. + +## Подготовьте место для наблюдений + +Публичное представление заказа показывает текущее состояние, а сырые слепки +хранятся ограниченное время. Сохраните историю в двух своих таблицах: потом +вы сможете сравнивать наблюдения обычными 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`. Эта сумма должна сходиться +с сохраненной выручкой, сложенной по всем категориям. + +## Продвигайте мир по одному дню + +Для каждого следующего наблюдения: + +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. Последний шаг нужен для сравнения состояния +в конце окна и за его пределами. +От исходного наблюдения потребуется восемь однодневных запусков. + +## Найдите изменения в уже загруженном дне + +Для сравнения прочитайте `default.lab_order_history` дважды: в одном +подзапросе отберите прежнюю S, в другом — новую; день D остается общим. +Соедините состояния по `order_id`, сохранив и заказы без пары. Итоги для этих +же D и S возьмите из `default.lab_revenue_history`. Найдите: + +- заказы, отсутствовавшие в первом слепке D, но появившиеся в следующем; +- переходы из `created` в `paid`; +- отмены заказов, для которых раньше был виден статус `paid`. + +Первый слепок, полученный на следующий день, — обычная доставка заказов. +Опоздавшими здесь считаются те, которых не было уже в этом слепке. +Покажите на нескольких номерах, как они исчезают из `awaiting_order`. + +Выберите два соседних наблюдения, между которыми хотя бы один заказ перешел +из `paid` в `cancelled`. Разложите изменение выручки по заказам. +Составьте таблицу: номер заказа, прежний и новый статусы, вклад в выручку +до и после изменения. Вклад определите по правилу расчета витрины, указанному +выше; учтите и заказы, которых еще не было в первом наблюдении. Сумма +изменений вкладов должна совпасть с изменением `revenue` до копейки. + +Покажите на этих наблюдениях, какой результат остался бы в витрине, +если бы конвейер после первого расчета обрабатывал только новые дни заказов. +Достаточно ли добавлять в старый день только опоздавшие заказы? Проверьте +свой ответ на найденных изменениях статусов. + +## Определите, какую историю нужно хранить + +Возьмите отмененный заказ, у которого в вашей истории был статус `paid`. +Укажите два наблюдения, доказывающих, что он был оплачен, а затем отменен. + +Теперь возьмите отмененный заказ, для которого ни в одном вашем наблюдении +нет `paid`. Проверьте утверждение «этот заказ никогда не был оплачен»: +что известно о его состоянии между двумя слепками? Достаточно ли сохраненных +наблюдений для такого вывода? + +Сформулируйте требования к данным для двух задач: восстановить состояние +витрины на момент прошлого расчета и узнать, был ли заказ когда-либо оплачен. +Какие сведения для каждой задачи должен передавать источник и сохранять +хранилище? Проверьте, каких нужных сведений нет в ваших таблицах наблюдений. + +## Задайте правило пересчета + +Сравните сохраненные состояния при S = D + 6 и S = D + 7: состав заказов, +их статусы, суммы и выручку. Какой слепок последним мог изменить заказы дня D +и каким прогоном он доставлен? Укажите наблюдение, после которого отчет +можно считать окончательным. Было бы достаточно совпадения двух соседних +сумм, если бы окно еще не закончилось? + +Запишите правило выбора дней для пересчета после очередного слепка. +Считайте, что начальная история уже загружена и ежедневные прогоны не +пропускались. Укажите, по какой дате определяются границы, какие дни нужно +пересчитать и когда день можно перестать включать в расчет. Подтвердите +правило своими наблюдениями, включая опоздание и отмену. + +После собственных выводов сверьтесь с описанием +[слепка и его доставки](../../architecture/orders/snapshot.md#слепок-и-его-доставка) +и [границ истории отмен](../../architecture/orders/fate.md#судьба-заказа). + +## Что должно получиться + +- [ ] сохранена последовательность наблюдений одного дня D с датами слепков S; +- [ ] на опоздавших заказах и изменениях статусов показаны ошибки загрузки + только новых дней или только новых заказов; +- [ ] изменение выручки между наблюдениями сходится с изменением вкладов заказов; +- [ ] оплата одной из последующих отмен подтверждена двумя наблюдениями; +- [ ] сформулированы требования к истории расчетов и истории оплаты; +- [ ] правило пересчета и момент закрытия дня обоснованы наблюдениями + и правилом доставки слепков. + +После работы оставьте `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 87f2627..369e665 100644 --- a/docs/specs/2026-09-01-labs-program.md +++ b/docs/specs/2026-09-01-labs-program.md @@ -149,28 +149,59 @@ ## 4. Факультативы — пять разделов -Ниже перечислены только названия сюжетов и их уроки. Материал, формы и способы -проверки остаются в [инвентаризации](../research/2026-08-31-labs-inventory.md). +Ниже перечислены сюжеты, их уроки и принятые решения по форме заданий. Исходный +материал и способы проверки — в +[инвентаризации](../research/2026-08-31-labs-inventory.md). ### Почему выручка не сходится -Этот раздел рекомендован первым из факультативов. Он открывается после ступени -1 основной программы: материал живет на стенде с этапа 6. +Этот раздел рекомендован первым из факультативов и открывается после ступени +1 основной программы. Шесть сюжетов собраны в четыре лабораторные: три +расследования готовых данных и одно наблюдение во времени. Опоздания, +изменение выручки и история отмен используют одну историю заказов, поэтому +для них выбрана общая работа вместо трех отдельных. -- **Почему трекер недосчитывает покупки (#186).** Разрыв воронки подтверждений - показывает потерянные события и границу доверия к двум источникам. -- **Почему повтор покупки завышает выручку (#187).** Движок убирает технический - дубль строки, а повтор одной покупки приходится находить по деловому ключу. -- **Какая позиция объясняет разницу в сумме.** Расхождение суммы нужно - разложить до конкретной вычеркнутой позиции и объяснить до копейки. -- **Был ли отмененный заказ оплачен.** Текущее состояние не заменяет историю, а - некоторые различия после истечения срока хранения сырых данных уже не - восстановить. -- **Когда отчет за день можно считать окончательным (#188).** Отчет за день - становится окончательным лишь после того, как источник больше не может - изменить этот день. -- **Почему выручка за день еще меняется.** Внутри окна изменяемости выручка - движется, а после закрытия дня фиксируется. +Результат расследования — запрос сверки и инженерное решение, подтвержденное +данными: выбор источника, ключа, полей для точного расчета или правила +пересчета и хранения истории. Объяснение суммы служит проверкой решения. +Вариант закончить работы только объяснением показателей отклонен: он +смещал акцент в анализ отчетов. Постройка новых DAG и подключение проверок +к конвейеру оставлены основной программе, чтобы факультативы не повторяли +ее задания. + +- **[Куда пропали покупки](../labs/optional/missing-purchases.md) (#186).** + Сверка по номерам заказов обосновывает источник витрины и условие + окончательной проверки полноты. Счетчики событий помогают обнаружить + расхождение, но не устанавливают место потери. +- **[Почему повтор покупки завышает выручку](../labs/optional/duplicate-purchases.md) (#187).** + Учащийся собирает расчет с одной строкой на заказ, проверяет ключ и полноту + набора номеров, отличает его зерно от зерна исходных событий. +- **[Какая позиция объясняет разницу в сумме](../labs/optional/order-amount-difference.md).** + Сверка позиций обосновывает выбор исходных полей и типа денег, отделяет + ошибку собственного разбора от различия данных события и заказа. +- **[Почему вчерашняя выручка меняется сегодня](../labs/optional/revenue-over-time.md) (#188).** + Наблюдение опозданий и смены статусов обосновывает правило пересчета при + ежедневной загрузке без пропусков. Учащийся определяет момент закрытия дня + и требования к истории для воспроизведения расчета и проверки факта оплаты. + +В четвертой лабораторной выбрано собственное наблюдение: учащийся продвигает +мир по одному дню через 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