feat(labs): добавлено сохранение наблюдений под учебной учеткой

Зачем: студенту нужен понятный путь от сохраненного состояния к SQL-сравнению без доступа технической учетной записи к изменению хранилища.

Что: добавлен student с чтением источников и записью только в default; лабораторная сохраняет заказы и итоги в две локальные таблицы. Обновлены настройка доступа и программа, уточнено округление productPrice.

Проверка: DDL и INSERT SELECT под student, пустое наблюдение, восемь переходов сохраненной истории и запрет вставки в пять прикладных баз на обеих нодах. config-test, lint стенда и генератора, 54 ссылки, независимое ревью.
This commit is contained in:
ddadmin committed 2026-09-22 04:56:45 -04:00
1 parent 3e9249933b
commit bb8605eed7
10 files changed
+114 -32

No files matched your search

+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 внутри колонки |
+69 -27
View File
@@ -4,7 +4,8 @@
заказов по наблюдаемым опозданиям и изменениям статусов.
Факультатив доступен после ступени 1. Понадобятся Airflow и DBeaver с
подключением `analyst` по [инструкции](../../../README.md#подключиться-из-dbeaver).
подключением `student` по [инструкции](../../../README.md#подключиться-из-dbeaver).
Он читает данные стенда и записывает ваши наблюдения в базу `default`.
Вы будете наблюдать один день оформления заказов, продвигая мир по одному
модельному дню. Проверьте, можно ли при загрузке выручки каждый раз считать
только новый день. Новые данные останутся в стенде.
@@ -32,28 +33,65 @@ SELECT
хранилище еще нет. Проверьте это для D в `dds.order_v` и
`dm.purchase_vs_orders_v`.
## Подготовьте запросы и сохраняйте результаты
Напишите запросы для следующих наблюдений:
| Что сохранить | Откуда взять |
|---|---|
| Дату полученного слепка S | `max(snapshot_date)` из `ods.order_v` |
| Номер, статус и `items_total` каждого заказа дня D | `dds.order_v`, отбор по `order_date` |
| Число заказов в каждом статусе и сумму `items_total` только у `paid` | те же заказы дня D |
| Выручку дня D | сумма `revenue` из `dm.revenue_daily_v`, отбор по `report_date` |
| Номера покупок, еще ожидающих заказ | строки `awaiting_order` из `dm.purchase_vs_orders_v`, отбор по `order_day` |
Выручку складывайте по всем категориям. В этой витрине она означает стоимость
позиций оплаченных заказов, без скидки и доставки, поэтому должна сходиться
с суммой `items_total` у заказов в статусе `paid`.
Сохраняйте результаты запросов отдельными файлами, например выгрузками CSV
из DBeaver. Подписывайте D и S. До поступления первого слепка список заказов
пуст, а выручка равна нулю; сохраните и это наблюдение.
## Подготовьте место для наблюдений
Публичное представление заказа показывает текущее состояние, а сырые слепки
хранятся ограниченное время. Сохраненные выгрузки — история для расследования.
хранятся ограниченное время. Сохраните историю в двух своих таблицах: потом
вы сможете сравнивать наблюдения обычными SQL-запросами в том же DBeaver.
Выполните DDL ниже. Это локальные таблицы на узле вашего подключения;
всю работу выполняйте через одно подключение. Если имена уже заняты вашей
прошлой работой, выберите другие и используйте их в дальнейших запросах.
```sql
CREATE TABLE default.lab_order_history
(
order_date Date,
snapshot_date Date,
order_id String,
status String,
items_total Decimal(18, 2)
)
ENGINE = MergeTree
ORDER BY (order_date, snapshot_date, order_id);
CREATE TABLE default.lab_revenue_history
(
order_date Date,
snapshot_date Date,
revenue Decimal(18, 2),
awaiting_order_ids Array(String)
)
ENGINE = MergeTree
ORDER BY (order_date, snapshot_date);
```
## Сохраните первое наблюдение
Напишите два `INSERT INTO ... SELECT` с явным списком колонок назначения:
| Куда записать | Что взять из стенда |
|---|---|
| `default.lab_order_history` | По строке на заказ дня D из `dds.order_v`: `order_date`, `order_id`, `status`, `items_total`; добавьте дату наблюдения S |
| `default.lab_revenue_history` | Одну строку с D, S, суммой `revenue` за `report_date = D` из `dm.revenue_daily_v` и массивом номеров `awaiting_order` за `order_day = D` из `dm.purchase_vs_orders_v` |
Во втором запросе сумму и массив получите отдельными агрегатами, чтобы
заказы не умножили строки выручки при соединении. Для массива пригодится
`groupArray(order_id)`. Дату S берите из `max(snapshot_date)` в `ods.order_v`.
Сначала выполните части `SELECT` отдельно и проверьте результаты, затем
сохраните их. До первого слепка заказов дня D еще нет: в таблицу заказов
строки не добавятся, но в таблице итогов должна появиться строка с нулевой
выручкой. Она зафиксирует, что наблюдение сделано.
Каждую пару D и S сохраняйте один раз: повторный `INSERT` добавит копии строк.
После записи проверьте, что на эту пару приходится одна строка итогов,
а число заказов и число разных `order_id` совпадают с `dds.order_v` за D.
Выручка этой витрины — стоимость позиций оплаченных заказов без скидки и
доставки. Для каждого сохраненного наблюдения посчитайте число заказов по
статусам и сумму `items_total` только у `paid`. Эта сумма должна сходиться
с сохраненной выручкой, сложенной по всем категориям.
## Продвигайте мир по одному дню
@@ -62,7 +100,7 @@ SELECT
1. Запустите `world_next_day` с `days = 1` и дождитесь успеха.
2. Запустите `etl_pipeline` и дождитесь успеха: `world_next_day` сам витрины
не обновляет.
3. Повторите свои запросы и сохраните новый комплект результатов с датой S.
3. Повторите два запроса сохранения с новой датой S и проверьте записанные строки.
Во всех запросах остается прежний день D. За один запуск проигрывайте один
день, чтобы сохранить промежуточные состояния. Пакетный `world_next_day`
@@ -75,8 +113,10 @@ SELECT
## Найдите изменения в уже загруженном дне
После каждого шага сравнивайте заказы по `order_id`, а не по положению строк
в выгрузке. Найдите:
Для сравнения прочитайте `default.lab_order_history` дважды: в одном
подзапросе отберите прежнюю S, в другом — новую; день D остается общим.
Соедините состояния по `order_id`, сохранив и заказы без пары. Итоги для этих
же D и S возьмите из `default.lab_revenue_history`. Найдите:
- заказы, отсутствовавшие в первом слепке D, но появившиеся в следующем;
- переходы из `created` в `paid`;
@@ -103,7 +143,7 @@ SELECT
Возьмите отмененный заказ, у которого в вашей истории был статус `paid`.
Укажите два наблюдения, доказывающих, что он был оплачен, а затем отменен.
Теперь возьмите отмененный заказ, для которого ни в одной вашей выгрузке
Теперь возьмите отмененный заказ, для которого ни в одном вашем наблюдении
нет `paid`. Проверьте утверждение «этот заказ никогда не был оплачен»:
что известно о его состоянии между двумя слепками? Достаточно ли сохраненных
наблюдений для такого вывода?
@@ -111,7 +151,7 @@ SELECT
Сформулируйте требования к данным для двух задач: восстановить состояние
витрины на момент прошлого расчета и узнать, был ли заказ когда-либо оплачен.
Какие сведения для каждой задачи должен передавать источник и сохранять
хранилище? Проверьте предложение на своих выгрузках и на промежутке между ними.
хранилище? Проверьте, каких нужных сведений нет в ваших таблицах наблюдений.
## Задайте правило пересчета
@@ -143,4 +183,6 @@ SELECT
и правилом доставки слепков.
После работы оставьте `world_live` на паузе либо верните автоматическое
продвижение, если пользовались им до наблюдения.
продвижение, если пользовались им до наблюдения. Таблицы истории останутся
для повторной проверки выводов. Когда они больше не нужны, удалите только
свои `default.lab_order_history` и `default.lab_revenue_history`.
+7 -1
View File
@@ -185,7 +185,13 @@
и требования к истории для воспроизведения расчета и проверки факта оплаты.
В четвертой лабораторной выбрано собственное наблюдение: учащийся продвигает
мир по одному дню через Airflow и сохраняет результаты после обновления витрин.
мир по одному дню через Airflow и сохраняет результаты после обновления витрин
в две локальные таблицы ClickHouse под `student`: состояния заказов и итоги
наблюдений. Сравнение выполняется SQL-запросами в том же подключении DBeaver.
Учащийся может писать только в `default`; источники доступны ему на чтение.
Отдельная строка итогов фиксирует и наблюдение без заказов. Экспорт в CSV
отклонен как основной способ: он требовал неописанного перехода к другому
инструменту или обратному импорту файлов.
Готовые слепки и сочетание готового примера с собственным наблюдением
отклонены: нужную историю можно получить на стенде и самому увидеть,
как новые данные меняют уже рассчитанный отчет.