feat(labs): добавлены четыре расследования выручки (#255)
Зачем: дать учащемуся практику проверки полноты, повторов и изменений данных для витрин. Что: добавлены четыре лабораторные и учебная учетная запись для сохранения наблюдений. Проверка: SQL и права student на стенде, восемь модельных дней через Airflow, config-test, lint и ссылки.
This commit was merged in pull request #255.
This commit is contained in:
commit
c1d3a6db9b
15 files changed
+559
-32
No files matched your search
@@ -10,6 +10,7 @@ CLICKHOUSE_ETL_PASSWORD=etl-26-writer
|
|||||||
CLICKHOUSE_LIFECYCLE_PASSWORD=lifecycle-26-owner
|
CLICKHOUSE_LIFECYCLE_PASSWORD=lifecycle-26-owner
|
||||||
CLICKHOUSE_BI_PASSWORD=bi-26-reader
|
CLICKHOUSE_BI_PASSWORD=bi-26-reader
|
||||||
CLICKHOUSE_ANALYST_PASSWORD=analyst-26-reader
|
CLICKHOUSE_ANALYST_PASSWORD=analyst-26-reader
|
||||||
|
CLICKHOUSE_STUDENT_PASSWORD=student-26-lab
|
||||||
CLICKHOUSE_GRAFANA_PASSWORD=grafana-26-reader
|
CLICKHOUSE_GRAFANA_PASSWORD=grafana-26-reader
|
||||||
KAFKA_EXTERNAL_PORT=29092
|
KAFKA_EXTERNAL_PORT=29092
|
||||||
PROMETHEUS_PORT=29090
|
PROMETHEUS_PORT=29090
|
||||||
|
|||||||
@@ -46,15 +46,10 @@
|
|||||||
|
|
||||||
## Кому что поручать
|
## Кому что поручать
|
||||||
|
|
||||||
Читатель здесь не побочный потребитель, а тот, ради кого стенд существует:
|
Автор задачи сам пишет и редактирует код, комментарии и документы, если
|
||||||
код, комментарии и документы и есть продукт. Поэтому всё, что человек будет
|
владелец не задал другое распределение работы.
|
||||||
читать и разбирать, пишет модель с чувством меры — сейчас это Opus. Codex
|
|
||||||
берёт техническую работу без вкусовых решений: миграции, разбор журналов и
|
|
||||||
объёмного вывода, обход репозитория, повторяющиеся правки. Там его запас
|
|
||||||
лимитов работает на нас, а экономить их на читаемом коде — экономия ложная:
|
|
||||||
переписывать выйдет дороже, чем сразу написать хорошо.
|
|
||||||
|
|
||||||
Делить работу между ними стоит не целыми задачами, а подзадачами, и решать
|
Делить работу стоит не целыми задачами, а подзадачами, и решать
|
||||||
это при планировании. Вкусовая часть идёт первой: она задаёт форму, имена и
|
это при планировании. Вкусовая часть идёт первой: она задаёт форму, имена и
|
||||||
границы, под которые механическая потом подстраивается.
|
границы, под которые механическая потом подстраивается.
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,9 @@ Kafka, затем заново создаст схему хранилища и
|
|||||||
Для подключения ко второму узлу укажите порт `28124`.
|
Для подключения ко второму узлу укажите порт `28124`.
|
||||||
|
|
||||||
Пользователь `analyst` может читать данные во всех слоях и справочниках.
|
Пользователь `analyst` может читать данные во всех слоях и справочниках.
|
||||||
|
Для заданий с собственными таблицами наблюдений используйте `student` и пароль
|
||||||
|
из `CLICKHOUSE_STUDENT_PASSWORD` в `.env`. Он читает те же источники, а создавать,
|
||||||
|
заполнять и удалять таблицы может только в базе `default`.
|
||||||
Для создания учебных витрин используйте подключение, описанное
|
Для создания учебных витрин используйте подключение, описанное
|
||||||
в [инструкции к заданиям](docs/labs/README.md#как-применить-свой-ddl).
|
в [инструкции к заданиям](docs/labs/README.md#как-применить-свой-ddl).
|
||||||
Нативные порты `29000` и `29001` нужны `clickhouse-client`, но не DBeaver.
|
Нативные порты `29000` и `29001` нужны `clickhouse-client`, но не DBeaver.
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ x-clickhouse-common: &clickhouse-common
|
|||||||
CLICKHOUSE_LIFECYCLE_PASSWORD: ${CLICKHOUSE_LIFECYCLE_PASSWORD:?Скопируйте .env.example в .env}
|
CLICKHOUSE_LIFECYCLE_PASSWORD: ${CLICKHOUSE_LIFECYCLE_PASSWORD:?Скопируйте .env.example в .env}
|
||||||
CLICKHOUSE_BI_PASSWORD: ${CLICKHOUSE_BI_PASSWORD:?Скопируйте .env.example в .env}
|
CLICKHOUSE_BI_PASSWORD: ${CLICKHOUSE_BI_PASSWORD:?Скопируйте .env.example в .env}
|
||||||
CLICKHOUSE_ANALYST_PASSWORD: ${CLICKHOUSE_ANALYST_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}
|
CLICKHOUSE_GRAFANA_PASSWORD: ${CLICKHOUSE_GRAFANA_PASSWORD:?Скопируйте .env.example в .env}
|
||||||
depends_on:
|
depends_on:
|
||||||
clickhouse-keeper:
|
clickhouse-keeper:
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
Дата: 9 августа 2026 года. Статус: принято; роль `monitoring_reader`
|
Дата: 9 августа 2026 года. Статус: принято; роль `monitoring_reader`
|
||||||
и пользователь `grafana` добавлены 21 августа 2026 года (#114), права
|
и пользователь `grafana` добавлены 21 августа 2026 года (#114), права
|
||||||
`bi_reader` расширены до ODS, DDS и DM 25 августа 2026 года (#150).
|
`bi_reader` расширены до ODS, DDS и DM 25 августа 2026 года (#150).
|
||||||
|
Пользователь `student` и роль `student_writer` добавлены 22 сентября 2026 года.
|
||||||
|
|
||||||
## Решение
|
## Решение
|
||||||
|
|
||||||
У кластера семь пользователей и шесть ролей.
|
У кластера восемь пользователей и семь ролей.
|
||||||
|
|
||||||
- `default` — с паролем, только служебный: проверки здоровья нод и работа
|
- `default` — с паролем, только служебный: проверки здоровья нод и работа
|
||||||
изнутри контейнеров. Приложения им не ходят.
|
изнутри контейнеров. Приложения им не ходят.
|
||||||
@@ -20,6 +21,10 @@
|
|||||||
шире.
|
шире.
|
||||||
- `analyst` с ролью `analyst_reader` — чтение всех слоёв и справочников. Им
|
- `analyst` с ролью `analyst_reader` — чтение всех слоёв и справочников. Им
|
||||||
человек подключается снаружи, из своего клиента.
|
человек подключается снаружи, из своего клиента.
|
||||||
|
- `student` с ролями `analyst_reader` и `student_writer` — то же чтение и
|
||||||
|
работа со своими таблицами в `default`: создание, вставка и удаление.
|
||||||
|
Сохранение учебных наблюдений не требует прав технической учётной записи
|
||||||
|
`etl` на изменение слоёв. Таблицы локальные; работа ведётся на одной ноде.
|
||||||
- `grafana` с ролью `monitoring_reader` — чтение слоёв, таблиц базы `dic` и
|
- `grafana` с ролью `monitoring_reader` — чтение слоёв, таблиц базы `dic` и
|
||||||
системных таблиц, `CLUSTER` и `REMOTE`. Им ходит Grafana, и больше никто.
|
системных таблиц, `CLUSTER` и `REMOTE`. Им ходит Grafana, и больше никто.
|
||||||
- `dict` с ролью `dict_reader` — чтение одной базы `dic`, пароля нет. Им
|
- `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 и
|
Сверено 9 августа 2026 года по документации ClickHouse через MCP Context7 и
|
||||||
на живом стенде 26.3.17.56.
|
на живом стенде 26.3.17.56.
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
## Пользователи и роли ClickHouse
|
## Пользователи и роли ClickHouse
|
||||||
|
|
||||||
В ClickHouse семь пользователей и шесть ролей:
|
В ClickHouse восемь пользователей и семь ролей:
|
||||||
|
|
||||||
- `etl` — учётная запись рабочих дагов Airflow; роль `etl_writer` читает и
|
- `etl` — учётная запись рабочих дагов Airflow; роль `etl_writer` читает и
|
||||||
пишет слои хранилища;
|
пишет слои хранилища;
|
||||||
@@ -33,6 +33,9 @@
|
|||||||
справочники `dic`, а запросы Superset обращаются к публичным `_v`;
|
справочники `dic`, а запросы Superset обращаются к публичным `_v`;
|
||||||
- `analyst` — учётная запись аналитика с правом чтения всех слоёв и
|
- `analyst` — учётная запись аналитика с правом чтения всех слоёв и
|
||||||
справочников;
|
справочников;
|
||||||
|
- `student` — учебная учётная запись: роль `analyst_reader` дает чтение
|
||||||
|
источников, а `student_writer` — создание, заполнение и удаление своих
|
||||||
|
таблиц в `default`. Записи в слои хранилища у нее нет;
|
||||||
- `grafana` — учётная запись Grafana; роль `monitoring_reader` читает все слои,
|
- `grafana` — учётная запись Grafana; роль `monitoring_reader` читает все слои,
|
||||||
системные таблицы и данные со всех реплик кластера;
|
системные таблицы и данные со всех реплик кластера;
|
||||||
- `dict` — беспарольная учётная запись словаря товаров с правом чтения базы
|
- `dict` — беспарольная учётная запись словаря товаров с правом чтения базы
|
||||||
@@ -49,6 +52,9 @@ Superset получает пароль `bi`, а Grafana — пароль `grafan
|
|||||||
|
|
||||||
## Применение настроек
|
## Применение настроек
|
||||||
|
|
||||||
|
Для существующего стенда сначала добавьте `CLICKHOUSE_STUDENT_PASSWORD`
|
||||||
|
из `.env.example` в свой `.env`.
|
||||||
|
|
||||||
Чтобы применить новые значения ClickHouse из `.env` или изменения файлов в
|
Чтобы применить новые значения ClickHouse из `.env` или изменения файлов в
|
||||||
`infra/clickhouse/config.d/` и `infra/clickhouse/users.d/`, пересоздайте ноды:
|
`infra/clickhouse/config.d/` и `infra/clickhouse/users.d/`, пересоздайте ноды:
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ DDS складывает свою модель и называет атрибу
|
|||||||
| 41 | `productID` | `Array(String)` | `object` | `product_id` | id товаров события |
|
| 41 | `productID` | `Array(String)` | `object` | `product_id` | id товаров события |
|
||||||
| 42 | `productName` | `Array(String)` | `object` | `product_name` | названия тех же товаров |
|
| 42 | `productName` | `Array(String)` | `object` | `product_name` | названия тех же товаров |
|
||||||
| 43 | `productCategory` | `Array(String)` | `object` | `product_category` | категории тех же товаров |
|
| 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` | количество штук каждого товара |
|
| 45 | `productQuantity` | `Array(UInt64)` | `uint64` | `product_quantity` | количество штук каждого товара |
|
||||||
| 46 | `productEventType` | `Array(String)` | `object` | `product_event_type` | действие с товаром: стенд шлёт add и purchase, полный словарь Метрики (detail, remove, impressions) не берём |
|
| 46 | `productEventType` | `Array(String)` | `object` | `product_event_type` | действие с товаром: стенд шлёт add и purchase, полный словарь Метрики (detail, remove, impressions) не берём |
|
||||||
| 47 | `ecommerce` | `String` | `object` | `ecommerce` | сырой JSON события, как отдаёт Метрика — материал лабы про разбор JSON внутри колонки |
|
| 47 | `ecommerce` | `String` | `object` | `ecommerce` | сырой JSON события, как отдаёт Метрика — материал лабы про разбор JSON внутри колонки |
|
||||||
+13
-2
@@ -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; для нее вы сохраняете состояния заказов после каждого модельного дня.
|
||||||
|
|
||||||
### Тонкий анализ данных
|
### Тонкий анализ данных
|
||||||
|
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|
||||||
|
## Что должно получиться
|
||||||
|
|
||||||
|
- [ ] сохранен запрос с одной строкой на заказ, числом событий и суммой;
|
||||||
|
- [ ] уникальность ключа и полнота набора номеров проверены по источнику;
|
||||||
|
- [ ] разность сумм полностью объяснена вкладом лишних событий;
|
||||||
|
- [ ] обоснованы ключ расчета и место устранения повторов;
|
||||||
|
- [ ] найден случай, показывающий границы расчета денег по трекеру.
|
||||||
@@ -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). Если все цены
|
||||||
|
выбранной корзины целые, возьмите еще один заказ с расхождением и дробной ценой.
|
||||||
|
|
||||||
|
Какие строки подтверждают найденное расхождение? Есть ли в источниках
|
||||||
|
сведения о том, почему оно возникло? Отделите наблюдения от предположений.
|
||||||
|
|
||||||
|
## Выберите поля для точного расчета
|
||||||
|
|
||||||
|
Укажите рядом с запросом, откуда брать цену, каким типом ее читать и по какому
|
||||||
|
ключу сопоставлять позиции. Подтвердите выбор результатами обеих сверок:
|
||||||
|
суммы разобранных позиций с суммой своего источника и позиций двух источников
|
||||||
|
между собой. Какой результат укажет на ошибку вашего разбора, а какой
|
||||||
|
потребует искать причину выше — в данных, которые получил запрос?
|
||||||
|
|
||||||
|
## Что должно получиться
|
||||||
|
|
||||||
|
- [ ] сохранен запрос сверки позиций, не теряющий товары без пары;
|
||||||
|
- [ ] все три равенства сходятся до копейки;
|
||||||
|
- [ ] найден пример потери точности в готовом массиве цен;
|
||||||
|
- [ ] выбор полей, типа денег и ключа сопоставления подтвержден расчетами;
|
||||||
|
- [ ] расхождение источников сверки отделено от ошибки своего разбора и различия
|
||||||
|
между стоимостью корзины и итогом со скидкой и доставкой.
|
||||||
@@ -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`.
|
||||||
@@ -149,28 +149,59 @@
|
|||||||
|
|
||||||
## 4. Факультативы — пять разделов
|
## 4. Факультативы — пять разделов
|
||||||
|
|
||||||
Ниже перечислены только названия сюжетов и их уроки. Материал, формы и способы
|
Ниже перечислены сюжеты, их уроки и принятые решения по форме заданий. Исходный
|
||||||
проверки остаются в [инвентаризации](../research/2026-08-31-labs-inventory.md).
|
материал и способы проверки — в
|
||||||
|
[инвентаризации](../research/2026-08-31-labs-inventory.md).
|
||||||
|
|
||||||
### Почему выручка не сходится
|
### Почему выручка не сходится
|
||||||
|
|
||||||
Этот раздел рекомендован первым из факультативов. Он открывается после ступени
|
Этот раздел рекомендован первым из факультативов и открывается после ступени
|
||||||
1 основной программы: материал живет на стенде с этапа 6.
|
1 основной программы. Шесть сюжетов собраны в четыре лабораторные: три
|
||||||
|
расследования готовых данных и одно наблюдение во времени. Опоздания,
|
||||||
|
изменение выручки и история отмен используют одну историю заказов, поэтому
|
||||||
|
для них выбрана общая работа вместо трех отдельных.
|
||||||
|
|
||||||
- **Почему трекер недосчитывает покупки (#186).** Разрыв воронки подтверждений
|
Результат расследования — запрос сверки и инженерное решение, подтвержденное
|
||||||
показывает потерянные события и границу доверия к двум источникам.
|
данными: выбор источника, ключа, полей для точного расчета или правила
|
||||||
- **Почему повтор покупки завышает выручку (#187).** Движок убирает технический
|
пересчета и хранения истории. Объяснение суммы служит проверкой решения.
|
||||||
дубль строки, а повтор одной покупки приходится находить по деловому ключу.
|
Вариант закончить работы только объяснением показателей отклонен: он
|
||||||
- **Какая позиция объясняет разницу в сумме.** Расхождение суммы нужно
|
смещал акцент в анализ отчетов. Постройка новых DAG и подключение проверок
|
||||||
разложить до конкретной вычеркнутой позиции и объяснить до копейки.
|
к конвейеру оставлены основной программе, чтобы факультативы не повторяли
|
||||||
- **Был ли отмененный заказ оплачен.** Текущее состояние не заменяет историю, а
|
ее задания.
|
||||||
некоторые различия после истечения срока хранения сырых данных уже не
|
|
||||||
восстановить.
|
- **[Куда пропали покупки](../labs/optional/missing-purchases.md) (#186).**
|
||||||
- **Когда отчет за день можно считать окончательным (#188).** Отчет за день
|
Сверка по номерам заказов обосновывает источник витрины и условие
|
||||||
становится окончательным лишь после того, как источник больше не может
|
окончательной проверки полноты. Счетчики событий помогают обнаружить
|
||||||
изменить этот день.
|
расхождение, но не устанавливают место потери.
|
||||||
- **Почему выручка за день еще меняется.** Внутри окна изменяемости выручка
|
- **[Почему повтор покупки завышает выручку](../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",
|
numpy_dtype="int64",
|
||||||
normalized_name="product_price",
|
normalized_name="product_price",
|
||||||
group=ColumnGroup.ECOMMERCE,
|
group=ColumnGroup.ECOMMERCE,
|
||||||
comment="цена за штуку целым числом: деньги генератор считает целыми",
|
comment="цена за штуку, округленная до целых рублей",
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
name="productQuantity",
|
name="productQuantity",
|
||||||
|
|||||||
@@ -61,6 +61,11 @@
|
|||||||
<query>GRANT SELECT ON system.settings</query>
|
<query>GRANT SELECT ON system.settings</query>
|
||||||
</grants>
|
</grants>
|
||||||
</analyst_reader>
|
</analyst_reader>
|
||||||
|
<student_writer>
|
||||||
|
<grants>
|
||||||
|
<query>GRANT SELECT, INSERT, CREATE TABLE, DROP TABLE ON default.*</query>
|
||||||
|
</grants>
|
||||||
|
</student_writer>
|
||||||
<monitoring_reader>
|
<monitoring_reader>
|
||||||
<grants>
|
<grants>
|
||||||
<query>GRANT SELECT ON stg.*</query>
|
<query>GRANT SELECT ON stg.*</query>
|
||||||
@@ -113,6 +118,14 @@
|
|||||||
<query>GRANT analyst_reader</query>
|
<query>GRANT analyst_reader</query>
|
||||||
</grants>
|
</grants>
|
||||||
</analyst>
|
</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>
|
<grafana>
|
||||||
<password from_env="CLICKHOUSE_GRAFANA_PASSWORD"/>
|
<password from_env="CLICKHOUSE_GRAFANA_PASSWORD"/>
|
||||||
<quota>default</quota>
|
<quota>default</quota>
|
||||||
|
|||||||
Reference in new issue
Block a user