feat(labs): добавлены четыре расследования выручки (#255)

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

Что: добавлены четыре лабораторные и учебная учетная запись для сохранения наблюдений.

Проверка: SQL и права student на стенде, восемь модельных дней через Airflow, config-test, lint и ссылки.
This commit was merged in pull request #255.
This commit is contained in:
ddmitry committed 2026-09-22 12:13:33 +03:00
commit c1d3a6db9b
15 files changed
+559 -32

No files matched your search

+1
View File
@@ -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
+3 -8
View File
@@ -46,15 +46,10 @@
## Кому что поручать
Читатель здесь не побочный потребитель, а тот, ради кого стенд существует:
код, комментарии и документы и есть продукт. Поэтому всё, что человек будет
читать и разбирать, пишет модель с чувством меры — сейчас это Opus. Codex
берёт техническую работу без вкусовых решений: миграции, разбор журналов и
объёмного вывода, обход репозитория, повторяющиеся правки. Там его запас
лимитов работает на нас, а экономить их на читаемом коде — экономия ложная:
переписывать выйдет дороже, чем сразу написать хорошо.
Автор задачи сам пишет и редактирует код, комментарии и документы, если
владелец не задал другое распределение работы.
Делить работу между ними стоит не целыми задачами, а подзадачами, и решать
Делить работу стоит не целыми задачами, а подзадачами, и решать
это при планировании. Вкусовая часть идёт первой: она задаёт форму, имена и
границы, под которые механическая потом подстраивается.
+3
View File
@@ -121,6 +121,9 @@ Kafka, затем заново создаст схему хранилища и
Для подключения ко второму узлу укажите порт `28124`.
Пользователь `analyst` может читать данные во всех слоях и справочниках.
Для заданий с собственными таблицами наблюдений используйте `student` и пароль
из `CLICKHOUSE_STUDENT_PASSWORD` в `.env`. Он читает те же источники, а создавать,
заполнять и удалять таблицы может только в базе `default`.
Для создания учебных витрин используйте подключение, описанное
в [инструкции к заданиям](docs/labs/README.md#как-применить-свой-ddl).
Нативные порты `29000` и `29001` нужны `clickhouse-client`, но не DBeaver.
+1
View File
@@ -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:
+11 -1
View File
@@ -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.
+7 -1
View File
@@ -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/`, пересоздайте ноды:
+1 -1
View File
@@ -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 внутри колонки |
+13 -2
View File
@@ -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; для нее вы сохраняете состояния заказов после каждого модельного дня.
### Тонкий анализ данных
+82
View File
@@ -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`.
## Что должно получиться
- [ ] сохранен запрос с одной строкой на заказ, числом событий и суммой;
- [ ] уникальность ключа и полнота набора номеров проверены по источнику;
- [ ] разность сумм полностью объяснена вкладом лишних событий;
- [ ] обоснованы ключ расчета и место устранения повторов;
- [ ] найден случай, показывающий границы расчета денег по трекеру.
+102
View File
@@ -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` установить место потери —
до приема в хранилище или при обработке? Какие данные нужны, чтобы это выяснить?
## Что должно получиться
- [ ] сохранен запрос сверки по дате, возвращающий заказы без события;
- [ ] оба равенства сходятся, влияние повторного просмотра объяснено;
- [ ] один случай подтвержден запросами к обоим источникам;
- [ ] источник витрины выбран по результатам сверки;
- [ ] указаны условие окончательной проверки и границы ее выводов.
@@ -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). Если все цены
выбранной корзины целые, возьмите еще один заказ с расхождением и дробной ценой.
Какие строки подтверждают найденное расхождение? Есть ли в источниках
сведения о том, почему оно возникло? Отделите наблюдения от предположений.
## Выберите поля для точного расчета
Укажите рядом с запросом, откуда брать цену, каким типом ее читать и по какому
ключу сопоставлять позиции. Подтвердите выбор результатами обеих сверок:
суммы разобранных позиций с суммой своего источника и позиций двух источников
между собой. Какой результат укажет на ошибку вашего разбора, а какой
потребует искать причину выше — в данных, которые получил запрос?
## Что должно получиться
- [ ] сохранен запрос сверки позиций, не теряющий товары без пары;
- [ ] все три равенства сходятся до копейки;
- [ ] найден пример потери точности в готовом массиве цен;
- [ ] выбор полей, типа денег и ключа сопоставления подтвержден расчетами;
- [ ] расхождение источников сверки отделено от ошибки своего разбора и различия
между стоимостью корзины и итогом со скидкой и доставкой.
+188
View File
@@ -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`.
+49 -18
View File
@@ -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
отклонен как основной способ: он требовал неописанного перехода к другому
инструменту или обратному импорту файлов.
Готовые слепки и сочетание готового примера с собственным наблюдением
отклонены: нужную историю можно получить на стенде и самому увидеть,
как новые данные меняют уже рассчитанный отчет.
День считается закрытым после получения и обработки последнего слепка
семидневного окна изменяемости; этот слепок доставляется следующим модельным
днем. Одного совпадения сумм в соседних наблюдениях для такого вывода
недостаточно. История отмен тоже имеет границу: оплата и отмена между двумя
дневными слепками могут остаться невидимыми. Отсутствие наблюдаемой оплаты
не доказывает, что оплаты не было.
### Тонкий анализ данных
@@ -420,7 +420,7 @@ COLUMNS: tuple[Column, ...] = (
numpy_dtype="int64",
normalized_name="product_price",
group=ColumnGroup.ECOMMERCE,
comment="цена за штуку целым числом: деньги генератор считает целыми",
comment="цена за штуку, округленная до целых рублей",
),
Column(
name="productQuantity",
+13
View File
@@ -61,6 +61,11 @@
<query>GRANT SELECT ON system.settings</query>
</grants>
</analyst_reader>
<student_writer>
<grants>
<query>GRANT SELECT, INSERT, CREATE TABLE, DROP TABLE ON default.*</query>
</grants>
</student_writer>
<monitoring_reader>
<grants>
<query>GRANT SELECT ON stg.*</query>
@@ -113,6 +118,14 @@
<query>GRANT analyst_reader</query>
</grants>
</analyst>
<student>
<password from_env="CLICKHOUSE_STUDENT_PASSWORD"/>
<quota>default</quota>
<grants>
<query>GRANT analyst_reader</query>
<query>GRANT student_writer</query>
</grants>
</student>
<grafana>
<password from_env="CLICKHOUSE_GRAFANA_PASSWORD"/>
<quota>default</quota>