Author SHA1 Message Date
ddadminandClaude Fable 5 453950a639 docs(specs): итог тикета #203 внесен в раздел 9 спеки программы лаб
- Зачем:
  - раздел 9 отсылал к открытому тикету #203; решение принято, указатель
    должен вести на итог, а не на вопрос.
- Что:
  - фраза «докуда ведет карта #182» заменена итогом: карта закрыта приемкой
    спеки, исполнение ведет карта #206.
- Проверка:
  - вычитка раздела 9 файла docs/specs/2026-09-01-labs-program.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 09:32:27 -04:00
ddmitry c26b4530f3 Merge pull request 'docs(specs): спека программы учебных лаб' (#205) from 202-labs-program-spec into main
Reviewed-on: #205
2026-09-01 16:13:49 +03:00
ddadmin eed5b4b4e6 docs(specs): словарь программы лаб очеловечен по ревью владельца
- Зачем:
  - термины спеки перейдут в задания, а «хребет», «ветки» и «точка
    прицепа» с калькированными заголовками сюжетов смущали бы студентов.
- Что:
  - хребет → основная программа, шаги → ступени, ветки → факультативы,
    «точка прицепа» заменена формулой «открывается после ступени N»;
  - заголовки разделов и сюжетов переписаны из позиции студента
    («Почему выручка не сходится», «Куда попадают битые строки»);
  - переносы выровнены по ~80 колонок по всему файлу.
- Проверка:
  - текст без учета пробелов не изменился на шаге переносов (SHA-256);
    решения, состав и порядок программы не менялись.
2026-09-01 09:07:40 -04:00
ddadmin 4fd872f296 docs(specs): спека программы учебных лаб — хребет и ветки
- Зачем:
  - тикет #202 карты #182: собрать из инвентаризации #201 программу лаб
    целиком — состав, порядок, формы и судьбу идей-кандидатов.
- Что:
  - создан docs/specs/2026-09-01-labs-program.md: хребет из семи
    обязательных шагов и пять тематических разделов необязательных лаб;
  - узаконены границы вычитания по потокам и поименные резолюции идей
    #143, #145, #148, #149, #186, #187, #188;
  - записаны отклоненные варианты оси прогрессии и границы спеки (#203).
- Проверка:
  - вычитка владельцем (спека принята), слепое ревью фактов против
    инвентаризации и доков — все находки закрыты.
2026-09-01 08:45:43 -04:00
ddmitry 22bf09af9d Merge pull request 'docs(research): инвентаризация сюжетов лаб по слоям стенда' (#204) from research/labs-inventory into main 2026-08-31 18:39:20 +03:00
ddadminandClaude Fable 5 502d705ca1 docs(research): инвентаризация сюжетов лаб по слоям стенда
- Зачем:
  - спеке программы лаб (#202) нужен полный веер сюжетов вместо семи
    случайно накопленных идей; резолюция тикета #201 карты #182.
- Что:
  - заметка docs/research/2026-08-31-labs-inventory.md: 32 сюжета по
    слоям STG, ODS, DDS, DM, BI и поперек слоев, три формы заданий,
    разбор гипотезы «вычитание + DQ» с таблицей по потокам.
- Проверка:
  - фактура (пути, имена таблиц, номера тикетов) сверена с репозиторием
    при вычитке; кода изменение не касается.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 11:38:54 -04:00
ddmitry d22156a7da Merge pull request 'docs(labs): ТЗ необязательной лабы «свой дашборд в Superset»' (#199) from 197-superset-lab into main
Reviewed-on: #199
2026-08-31 11:07:53 +03:00
ddadminandClaude Fable 5 6174536cc1 docs(specs): BI-лаба узаконена в исключении раздела 10
Зачем: раздел 10 выносил все лабы v2 в отдельную работу учебного пути,
делая исключение только для домашек DM, — необязательная лаба Superset
из #197 формально в него не попадала.

Что: исключение расширено фразой про лабу «свой дашборд в Superset» с
меркой владельца: домашки DM и эта лаба продуманы до готовых ТЗ,
остальные лабы пока на уровне идей.

Проверка: вычитка; решение владельца репозитория из обсуждения #197.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 04:05:43 -04:00
ddadminandClaude Fable 5 989ff71495 docs(labs): ТЗ необязательной лабы «свой дашборд в Superset»
Зачем: владеть хотя бы одним BI-инструментом на базовом уровне — полезный
навык дата-инженера, но стенд в первую очередь про ClickHouse, поэтому
лаба необязательная. Обещание «отдельная домашка после этапа 8» в доке DM
висело без адреса.

Что: ТЗ docs/labs/superset-dashboard.md — пять датасетов поверх публичных
представлений заданий 1-5, свой дашборд отдельно от стендового, проверка
глазами без homework_check; шестая строка и разведение «пяти заданий по
витринам» в docs/labs/README.md; абзац о сроке жизни объектов менти в
docs/architecture/superset.md; ссылка на лабу из доки DM; правило слага в
AGENTS.md расширено до «предмет задания».

Проверка: кода и DDL нет; слепое ревью текста (9 находок закрыто, 2
дополнительные с перепроверки) и финальная вычитка.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 03:55:40 -04:00
ddmitry cd82911906 Merge pull request 'feat(superset): датасеты и дашборд «Бизнес-сюжеты» (этап 8)' (#198) from 196-superset-dashboard into main
Reviewed-on: #198
2026-08-31 10:26:41 +03:00
ddadminandClaude Fable 5 3ffce6387e feat(superset): датасеты и дашборд «Бизнес-сюжеты» деревом импорта
- Зачем:
  - этап 8 (#196): три сюжета спеки — выручка, сверка, посетители /
    известные — должны приезжать в Superset при каждой инициализации
    стенда, без ручной сборки в интерфейсе.
- Что:
  - дерево infra/superset/import/ дополнено датасетами трех витрин dm,
    тремя графиками и дашбордом «Бизнес-сюжеты»; uuid розданы рукой,
    русские строки читаемы, metadata.yaml сокращен до версии формата.
  - написан справочник docs/architecture/superset.md: устройство зоны,
    дерево импорта, маршрут свежего экспорта, ловушки импорта.
  - в docs/architecture/dm.md добавлена ссылка на справочник зоны.
- Проверка:
  - docker compose run --rm superset-init — импорт начисто, затем три
    графика отдают данные через API (54/48/10 строк).
  - прогон рецепта «чистая установка» из справочника: сброс базы superset
    в postgres-metadata, импорт, активы на месте.
  - make lint и make check-services зеленые (7/7).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 18:42:01 -04:00
16 changed files with 1431 additions and 24 deletions

No files matched your search

+1 -1
View File
@@ -160,7 +160,7 @@ Airflow) и названия из кода. Если для понятия ес
- Имена файлов в `docs/labs/` — слаг строчными латинскими буквами через дефис - Имена файлов в `docs/labs/` — слаг строчными латинскими буквами через дефис
(`top-pages-daily.md`), индекс папки — `README.md`. Здесь живут ТЗ учебных (`top-pages-daily.md`), индекс папки — `README.md`. Здесь живут ТЗ учебных
заданий. Ни даты, ни номера: задание — не событие истории, а живой документ, заданий. Ни даты, ни номера: задание — не событие истории, а живой документ,
и правится он вместе с тем, о чем говорит. Слаг называет витрину задания. и правится он вместе с тем, о чем говорит. Слаг называет предмет задания.
- Имена файлов в `docs/adr/` — `NNNN-краткое-имя.md`: сквозной номер из четырёх - Имена файлов в `docs/adr/` — `NNNN-краткое-имя.md`: сквозной номер из четырёх
цифр и слаг (`0001-stand-services.md`). Решения нумеруются подряд, дата цифр и слаг (`0001-stand-services.md`). Решения нумеруются подряд, дата
в имени не нужна. в имени не нужна.
+5 -3
View File
@@ -46,8 +46,9 @@
пересобирать нечего. Тексты заданий — в [`docs/labs/`](../labs/README.md). пересобирать нечего. Тексты заданий — в [`docs/labs/`](../labs/README.md).
Дашборд этапа 8 стоит только на витринах стенда: приёмка этапа не зависит Дашборд этапа 8 стоит только на витринах стенда: приёмка этапа не зависит
от выполненных домашек. Расширение дашборда визуализациями по своим от выполненных домашек. Свой дашборд по витринам менти —
витринам — отдельная домашка после этапа 8. [необязательная лаба](../labs/superset-dashboard.md) после этапа 8. Устройство
стендового дашборда — в [доке зоны Superset](superset.md).
## Ритм пересборки ## Ритм пересборки
@@ -214,7 +215,8 @@ business_key`.
## Даг самопроверки ## Даг самопроверки
Домашки контролирует `homework_check` — отдельный даг без расписания: менти Домашки контролирует `homework_check` — отдельный даг без расписания: менти
запускает его рукой. По задаче на каждое из пяти [заданий](../labs/README.md). запускает его рукой. По задаче на каждое из
[пяти заданий по витринам](../labs/README.md).
Задача красная, пока витрины нет или счёт не сходится, и зелёная, когда Задача красная, пока витрины нет или счёт не сходится, и зелёная, когда
построено верно; сообщение об ошибке говорит, что не так и куда смотреть. Проверки — инварианты против источников витрины, построено верно; сообщение об ошибке говорит, что не так и куда смотреть. Проверки — инварианты против источников витрины,
золотые константы не прибиваются. В `dq_summary` даг не пишет: он инструмент золотые константы не прибиваются. В `dq_summary` даг не пишет: он инструмент
+165
View File
@@ -0,0 +1,165 @@
# Superset: витрины глазами
Документ описывает зону BI: куда Superset ходит за данными и за своими
метаданными, из чего собран стендовый дашборд, как снять свежий экспорт после
правки в интерфейсе и почему правка руками живет только до перезапуска.
Состав сюжетов и колонок витрин здесь не повторяется: он живет в
[спеке](../specs/2026-07-30-stand-v2-realism.md), раздел 7, и в
[доке слоя DM](dm.md).
## Устройство зоны
Superset читает ClickHouse пользователем `bi` **на ноде 2** — так задано
подключением `ClickHouse` (`clickhousedb://bi@clickhouse-02:8123`). Нода 2
здесь — ловушка правильных ошибок из
[спеки](../specs/2026-07-30-stand-v2-realism.md), раздел «Роли нод»: DDL идет
с ноды 1, и забытый `ON CLUSTER` виден в дашборде.
Пароля в подключении нет. Его отдает штатное хранилище паролей Superset
(`SQLALCHEMY_CUSTOM_PASSWORD_STORE` в `infra/superset/superset_config.py`),
которое читает `CLICKHOUSE_BI_PASSWORD` из окружения. Поэтому файл подключения
безопасно лежит в git, и экспорт из интерфейса не должен возвращать пароль
обратно в YAML.
Свои метаданные — подключения, датасеты, графики, дашборды, пользователи —
Superset держит в базе `superset` служебного `postgres-metadata`, рядом с базой
Airflow. В ClickHouse он не пишет ничего.
## Дерево импорта
`infra/superset/import/` — это дашборд стенда в виде файлов. Каталог
смонтирован в контейнер как `/app/import`.
| Файл | Что описывает |
|---|---|
| `metadata.yaml` | версия формата, `1.0.0`; больше в нем ничего нет |
| `databases/clickhouse.yaml` | подключение к ClickHouse: URI без пароля и постоянный uuid |
| `datasets/ClickHouse/*.yaml` | три витрины: `revenue_daily_v`, `purchase_vs_orders_v`, `daily_traffic_v` |
| `charts/*.yaml` | три графика: столбики выручки, сводная таблица сверки, линии трафика |
| `dashboards/business_stories.yaml` | дашборд «Бизнес-сюжеты» и раскладка панелей |
Связаны файлы не путями, а uuid. Датасет называет `database_uuid`, график —
`dataset_uuid`, а дашборд не называет графики вовсе: единственная связь с ними
лежит в его `position`, где узел `CHART-*` несет `meta.uuid` нужного графика.
График, которого нет в `position`, импортируется молча и не появляется нигде.
Uuid у стенда розданы рукой: общее начало
`19600000-0000-4000-8000-`, дальше номер объекта в последнем блоке — `1`–`3`
у датасетов, `101`–`103` у графиков, `201` у дашборда
(`…-000000000101`). Экспорт из интерфейса приносит свои, сгенерированные;
их заменяют на эти, иначе повторный импорт заведет вторую копию объекта вместо
обновления первой.
Датасет несет русские имена колонок, формат денег и метрику — а график только
ссылается на них по имени. Значит, подпись или формат правятся в одном месте, а
не в каждом графике.
Датасеты объявлены физическими (`schema: dm`, `sql: null`), хотя за именем стоит
представление ClickHouse. Импорт в источник не ходит и разницы между таблицей и
представлением не знает, а виртуальный датасет со своим SQL просто повторил бы
запрос, который уже живет в `sql/ddl/60-dm-views.sql`.
## Импорт при каждой инициализации
Последняя строка `infra/superset/init.sh`:
```
superset import-directory /app/import --overwrite
```
Служба `superset-init` отрабатывает перед подъемом самого Superset, то есть при
каждом `make up`. Без `--overwrite` второй прогон падал бы на первом же
знакомом uuid; с ним объект обновляется на месте, и повторная инициализация
проходит без ошибок.
Отсюда правило зоны: **стендовый дашборд не расширяют руками в интерфейсе.**
Правка живет до следующей инициализации, а потом молча заменяется тем, что
лежит в git. Пробовать интерфейс менти никто не мешает — но все, что должно
пережить перезапуск, приезжает файлом.
Это правило относится только к стендовым объектам. Свои датасеты, графики и
дашборды менти живут в той же базе метаданных Superset, а импорт трогает
только объекты с известными ему uuid. Поэтому свои объекты переживают
`make up` и `make rebuild-storage`. Их стирает только `make clean` или
адресный сброс базы метаданных по рецепту ниже.
## Как снять свежий экспорт
Маршрут один: правишь на живом стенде, забираешь экспорт, кладешь дерево в git.
1. Собрать или поправить график и дашборд в интерфейсе — на стенде, а не в
файлах. Ключи `params` графиков велики, недокументированы и меняются от
версии к версии; неверный ключ не ошибается, а рисует пустоту. Интерфейс
здесь единственный надежный оракул.
2. Снять экспорт и достать его из контейнера:
```
docker compose exec superset superset export-dashboards -f /tmp/export.zip
docker compose cp superset:/tmp/export.zip /tmp/export.zip
unzip -d /tmp/superset-export /tmp/export.zip
```
Внутри архива — один каталог `dashboard_export_<дата>` ровно той же формы,
что и дерево импорта. Команда выгружает все дашборды сразу; на стенде он
один.
3. Вычистить экспортный сор перед коммитом:
- выбросить экспортный `databases/` — наш `clickhouse.yaml` остается как
есть, с постоянным uuid и без пароля;
- оставить в `metadata.yaml` одну строку `version: 1.0.0`;
- убрать из графиков `params.datasource`, `params.slice_id` и
`params.dashboards` — это идентификаторы строк того Superset, где
снимали, а импорт выводит связи из uuid;
- убрать `query_context` — снимок запроса оттуда же. Импорт его не
требует и обратно не заполняет: после переимпорта он остается пустым, на
отрисовку это не влияет;
- перезаписать русские строки читаемыми. Экспорт кодирует их
`\uXXXX`-последовательностями, и после каждого круга их приходится
раскодировать заново: имена графиков и подписи колонок читает менти, и в
файле, и в диффе. Годится любой повторный дамп YAML с
`allow_unicode=True`;
- вернуть постоянные uuid из схемы выше.
4. Скопировать вычищенное дерево в `infra/superset/import/` и проверить
импортом **на пустых метаданных** — иначе прогон проверит только обновление
уже стоящих объектов, а молчаливый пропуск нового спрячется за старым.
Активы Superset лежат в базе `superset` службы `postgres-metadata`, поэтому
адресный сброс — эта база:
```
docker compose stop superset
docker compose exec postgres-metadata sh -c \
'psql -U "$POSTGRES_USER" -d postgres \
-c "DROP DATABASE superset WITH (FORCE)" \
-c "CREATE DATABASE superset OWNER \"$SUPERSET_METADATA_USER\""'
docker compose run --rm superset-init
docker compose up -d superset
```
Базу приходится создавать тут же: скрипт `infra/postgres/init/` заводит ее
один раз, при первой инициализации тома Postgres. `make rebuild-storage`
для этой проверки не годится — он гоняет `world_recreate` в ClickHouse и
метаданных Superset не касается. `make clean` сбросит и их, но заодно снесет
весь стенд, включая метаданные Airflow в той же службе.
После импорта дашборд смотрят глазами: это единственное доказательство —
см. ловушку ниже.
## Ловушки
**Импорт молчит.** График, чей `dataset_uuid` не разрешился, отбрасывается без
единого слова; дашборд, у которого не сошлись ссылки на графики,
пропускается. Ошибки уходят в журнал, и `init.sh`
завершается нулем в любом случае. Поэтому импорт проверяют глазами или
запросом к API — но не тем, что команда отработала.
**`--overwrite` синхронизирует датасет разрушительно.** Колонки и метрики
берутся из YAML целиком: добавленная в интерфейсе метрика или подпись колонки
исчезнет при следующем старте контейнера. Правка датасета — это правка файла.
## Что проверяет стенд
`make check-services` входит в Superset, находит подключение `ClickHouse`,
сверяет его uuid и безопасный URI и требует схему `dm` — то есть подтверждает
живое подключение и хранилище пароля. Сами графики и дашборд ни одна цель не
проверяет: их смотрят глазами после переинициализации. Полная карта целей — в
[доке проверок](testing.md).
+22 -17
View File
@@ -1,9 +1,13 @@
# Учебные задания: витрины слоя DM # Учебные задания: витрины слоя DM и дашборд в Superset
Пять домашек. Стенд привозит слой DM наполовину: витрины трех сюжетов Пять домашних заданий — про витрины слоя DM. Стенд привозит слой
дашборда и контроль качества построены, остальные витрины строит менти. наполовину: витрины трех сюжетов дашборда и контроль качества построены,
Каждое задание — самостоятельное ТЗ: что за витрина, какое у нее зерно, остальные витрины строит менти. Каждое из них — самостоятельное ТЗ: что за
из чего она собирается и как проверить, что собралась верно. витрина, какое у нее зерно, из чего она собирается и как проверить, что
собралась верно.
Шестое задание — необязательная лаба в Superset. В ней менти собирает свой
дашборд поверх пяти витрин.
Общее устройство слоя — [дока DM](../architecture/dm.md). Она отвечает на Общее устройство слоя — [дока DM](../architecture/dm.md). Она отвечает на
вопросы «почему так», задание отвечает на вопрос «что сделать». вопросы «почему так», задание отвечает на вопрос «что сделать».
@@ -13,18 +17,19 @@
Задания идут по нарастанию: каждое следующее опирается на то, что уже Задания идут по нарастанию: каждое следующее опирается на то, что уже
усвоено. усвоено.
| № | Витрина | Чему учит | | № | Результат | Чему учит |
|---|---|---| |---|---|---|
| 1 | [`events_enriched_v`](events-enriched.md) | слой — это контракт, а не копия данных | | 1 | [`events_enriched_v`](events-enriched.md) | слой — это контракт, а не копия данных |
| 2 | [`top_pages_daily`](top-pages-daily.md) | зерно сменилось — витрина материализуется и встает в конвейер | | 2 | [`top_pages_daily`](top-pages-daily.md) | зерно сменилось — витрина материализуется и встает в конвейер |
| 3 | [`session_overview`](session-overview.md) | зерно решает, на какие вопросы витрина уже не ответит | | 3 | [`session_overview`](session-overview.md) | зерно решает, на какие вопросы витрина уже не ответит |
| 4 | [`dq_errors_daily`](dq-errors-daily.md) | у брака свой день и свой срок жизни | | 4 | [`dq_errors_daily`](dq-errors-daily.md) | у брака свой день и свой срок жизни |
| 5 | [`utm_effectiveness`](utm-effectiveness.md) | `declared_` в имени — предупреждение, а не украшение | | 5 | [`utm_effectiveness`](utm-effectiveness.md) | `declared_` в имени — предупреждение, а не украшение |
| 6 | [Свой дашборд в Superset](superset-dashboard.md) — необязательная лаба | свои объекты BI живут отдельно от стендовых |
## Общие правила ## Общие правила
Они одни на все пять заданий, поэтому записаны здесь, а не повторяются в Они одни для пяти заданий по витринам, поэтому записаны здесь, а не
каждом ТЗ. повторяются в каждом ТЗ.
**Читать — через `_v`.** Публичный договор сущности DDS — представление с **Читать — через `_v`.** Публичный договор сущности DDS — представление с
суффиксом `_v`: `dds.event_v`, `dds.session_v`. Витрина называет только суффиксом `_v`: `dds.event_v`, `dds.session_v`. Витрина называет только
@@ -74,18 +79,18 @@ DDS, и задания остаются независимыми. Исключе
собрано верно. В сводку качества конвейера `dm.dq_summary` он не пишет: собрано верно. В сводку качества конвейера `dm.dq_summary` он не пишет:
это инструмент лабы, а не история качества стенда. это инструмент лабы, а не история качества стенда.
Проверка у всех заданий устроена одинаково и золотых констант не содержит: Проверка пяти заданий по витринам устроена одинаково и золотых констант не
рядом с витриной независимым запросом собирается тот же ответ — прямо из содержит: рядом с витриной независимым запросом собирается тот же ответ —
источника витрины, минуя ее саму. У четырех заданий источник эталона тот прямо из источника витрины, минуя ее саму. У четырех заданий источник эталона
же, что у витрины, `dds.session_v` или `dds.event_v`; у задания 4 это тот же, что у витрины, `dds.session_v` или `dds.event_v`; у задания 4 это
таблицы брака ODS, потому что в DDS у брака представительства нет. Обе таблицы брака ODS, потому что в DDS у брака представительства нет. Обе стороны
стороны соединяются по деловому ключу через `FULL OUTER JOIN`, и считается соединяются по деловому ключу через `FULL OUTER JOIN`, и считается число строк,
число строк, где стороны разошлись или одной стороны нет. Ноль — зеленый. где стороны разошлись или одной стороны нет. Ноль — зеленый.
Полное соединение здесь обязательно: при обычном пропавший день или Полное соединение здесь обязательно: при обычном пропавший день или
пропавший ключ просто не попал бы в результат и остался бы незамечен. пропавший ключ просто не попал бы в результат и остался бы незамечен.
Раздел «Как проверить себя» каждого ТЗ называет деловой ключ и сравниваемые Раздел «Как проверить себя» каждого ТЗ по витрине называет деловой ключ и
величины. Тот же прием в стендовом исполнении — [даг сравниваемые величины. Тот же прием в стендовом исполнении — [даг
`dq_check`](../../dags/dq_check.py) и запросы в [`sql/dq/`](../../sql/dq). `dq_check`](../../dags/dq_check.py) и запросы в [`sql/dq/`](../../sql/dq).
**Предупреждение.** Запросы самой проверки лежат в `sql/dq/` открыто, и **Предупреждение.** Запросы самой проверки лежат в `sql/dq/` открыто, и
+87
View File
@@ -0,0 +1,87 @@
# Задание 6 (необязательное): свой дашборд в Superset
**Чему учит:** довести свои витрины до потребителя — завести в Superset
датасеты поверх публичных представлений, подобрать графики под вопросы и
собрать из них один связный экран.
Эта лаба идет после заданий 1–5 и опирается на их результат. Ее можно
пропустить: основной предмет стенда — ClickHouse, а здесь вся работа — в
интерфейсе BI.
## Роль дашборда
Собери обзор трафика и поведения посетителей. Он должен отвечать на три
вопроса: что происходило в магазине, какие страницы, устройства и рекламные
каналы дали результат, были ли ошибки приема данных.
Стендовый дашборд «Бизнес-сюжеты» собран из готовых витрин. Свой дашборд
собери из пяти витрин, которые ты построил в заданиях. Копировать раскладку
стендового дашборда не нужно: выбери форму, в которой ответы читаются без
пояснений рядом с экраном.
## Свои объекты — отдельно
Не добавляй графики в стендовый дашборд и не правь его датасеты. При
инициализации Superset импорт с `--overwrite` заменит ручные правки стендовых
объектов файлами из репозитория. `make up` свой дашборд не стирает; срок жизни
таких объектов описан в [доке Superset](../architecture/superset.md), «Импорт
при каждой инициализации».
Заведи отдельные датасеты и графики, собери из них свой дашборд. Назови его,
например, «Мои витрины DM». Это то же правило, по которому DDL домашек лежит
в `sql/ddl/70-labs.sql`: стенд привозит свои объекты, менти создает свои.
## Датасеты
Заведи в подключении `ClickHouse` пять физических датасетов схемы `dm`:
- `events_enriched_v`;
- `top_pages_daily_v`;
- `session_overview_v`;
- `dq_errors_daily_v`;
- `utm_effectiveness_v`.
## Состав дашборда
Каждая из пяти витрин должна появиться хотя бы в одном графике. Точный тип
графика выбери сам. На дашборде покажи следующее:
| Витрина | Что показать |
|---|---|
| `events_enriched_v` | число событий по часу и типу события |
| `top_pages_daily_v` | страницы с просмотрами, посетителями и добавлениями в корзину |
| `session_overview_v` | сравнение устройств по визитам, отказам и визитам с покупкой |
| `dq_errors_daily_v` | число ошибок по дню загрузки, источнику и классу |
| `utm_effectiveness_v` | трафик, покупки и заявленную выручку по рекламным каналам |
Для долей используй слагаемые из витрин. Например, доля отказов считается
как сумма `bounce_sessions`, деленная на сумму `sessions`. Среднее по готовым
долям для такого графика не подходит — урок задания 3 остается в силе.
Названия графиков, осей и показателей должны объяснять экран без чтения SQL.
## Как проверить себя
Эта лаба проверяется глазами. У расположения панелей и понятных подписей нет
счетного инварианта, поэтому задачи для нее в `homework_check` нет.
Открой список дашбордов и проверь:
- «Бизнес-сюжеты» остался отдельным стендовым дашбордом;
- твой дашборд открывается без ошибок и содержит график от каждого из пяти
датасетов;
- подписи, единицы измерения и легенды читаются без обращения к SQL;
- прямой трафик не исчез из графика UTM, а заявленная выручка не выдана за
серверную;
- пустой график ошибок нормален, если в таблицах брака сейчас нет строк.
Последний проход сделай как читатель: найди на экране самый заметный канал,
страницу и устройство, затем проверь, понятно ли из подписей, что именно
сравнивается. Если ответ приходится угадывать, поправь график или его название.
## Приемка
- [ ] пять датасетов читают публичные представления витрин заданий 1–5;
- [ ] создан отдельный дашборд, стендовые объекты не изменены;
- [ ] каждая витрина представлена хотя бы одним графиком;
- [ ] дашборд проверен глазами по разделу выше.
+279
View File
@@ -0,0 +1,279 @@
# Инвентаризация сюжетов лаб по слоям стенда
Это материал для спеки программы лаб (#202): полный список сюжетов без отбора. Спека решит, что оставить и в каком порядке поставить. Здесь для каждого участка стенда записано, чему на нем научится менти. Тикет — #201, карта — #182.
Каждый сюжет описан четырьмя строками: **урок**, **материал**, **форма** и **проверка**. Возможны три формы:
- *построить свое по ТЗ* — как в пяти домашних заданиях DM: рядом есть образец, но нет готового ответа;
- *вычитание* — удалить SQL готовой трансформации и написать его заново; даги остаются без изменений (уточнение владельца в комментарии к #148);
- *расследование* — данные уже лежат на стенде или стенд намеренно ломается, а менти ищет причину и объясняет ее.
Существующие шесть заданий из `docs/labs/` входят в список как готовое ядро слоя DM. Идеи из тикетов #143, #145, #148, #149, #186, #187, #188 разнесены по подходящим участкам. В конце отдельно проверяется гипотеза «вычитание + DQ» для каждого потока нижних слоев.
## STG — прием и сырье
**1. Push против pull: два способа принять топик.**
Урок: способ приема зависит от источника. Поток постоянно читает подписанная матвью, а слепок забирает отдельный запрос по команде.
Материал: оба чтеца в `sql/ddl/10-stg-tables.sql` (у `orders_raw_kafka` нет ни матвью, ни `ON CLUSTER`, зато есть `kafka_commit_on_select`), матвью приема `sql/ddl/40-stg-views.sql`, забор `sql/stg/orders_raw_load.sql`, `dags/orders_ingest.py`; сравнение цены и наблюдаемости — заготовка спеки (`docs/specs/2026-07-30-stand-v2-realism.md`, раздел 12).
Форма: расследование двух живых путей на одном стенде.
Проверка: офсеты групп `clickstream_hits` и `clickstream_orders`, счет строк сырья по `_load_id`.
**2. Кто читал топик и куда легли данные.**
Урок: нода, которая читает топик, может меняться от прогона к прогону. Раскладка данных не меняется: ее определяет ключ шардирования — свойство самих данных.
Материал: колонка `consumer_host` в `stg.hits_raw_dist`; `docs/architecture/storage.md`, «Раскладка по шардам».
Форма: расследование данных.
Проверка: пересчет ключа шардирования по `hostName()` — строк на чужом шарде нет.
**3. Вторая consumer group — дубли.**
Урок: на кластер нужна одна группа потребителей. Если завести вторую, каждая нода получит полную копию топика.
Материал: `kafka_group_name` в `sql/ddl/10-stg-tables.sql`; сцена названа спекой (раздел 12).
Форма: расследование-опыт «сломай — увидь — верни» на переигранном дне.
Проверка: счет событий против описи мира (`make check-clickhouse`); уборка — снятием партиции `_load_ts` (правило из `docs/architecture/testing.md`).
**4. Дырка в офсетах.**
Урок: сырье может сохранить не все сообщения, которые пришли из источника. Пустое сообщение или зазор после пересоздания матвью видны только по разрыву в `kafka_offset`, поэтому пропажу не нужно искать глазами.
Материал: `docs/architecture/storage.md`, «Что проверено» (опыты 5 и 7 августа); колонка `kafka_offset` в сырье.
Форма: расследование.
Проверка: непрерывность офсетов внутри партиции.
**5. Две оси времени.**
Урок: переобработка данных и очистка диска живут по разным часам. Сырье разбивается на части и удаляется по `_load_ts`, а модельный день появляется только в ODS. Поэтому TTL по модельной дате работал бы неверно.
Материал: `sql/ddl/10-stg-tables.sql` (TTL трое суток, `ttl_only_drop_parts`), `docs/architecture/storage.md`, «Срок жизни сырья».
Форма: расследование-вопрос (почему нельзя нарезать сырье по `EventDate`).
Проверка: ответ сверяется с доводом доки; счетного инварианта нет.
## ODS — типизация и брак
**6. Предикат и его буквальное отрицание.**
Урок: годные строки и брак должны делить поток без пропусков и пересечений. NULL в предикате незаметно теряет строку, а исключение роняет вставку и останавливает чтение топика.
Материал: `sql/ddl/30-ods-views.sql` (пара матвью событий), `sql/ods/order_parse.sql` (макрос `order_wire_contract`), ADR 0005.
Форма: расследование — подложить в топик по строке каждого класса брака и найти их в `ods.event_errors_dist` / `ods.order_errors_dist`.
Проверка: «сырье среза = годные + брак» на управляемой пачке; строки опыта убрать за собой.
**7. Три времени и версия-наблюдение.**
Урок: у строки заказа есть четыре временные координаты. Версию сущности задает дата слепка источника, а не время загрузки. `FINAL` живет ровно в одном месте — за представлением `_v`.
Материал: `sql/ddl/20-ods-tables.sql`, `sql/ddl/30-ods-views.sql`, ADR 0010 и 0014, `docs/architecture/orders/ingestion.md` («Роль ODS»).
Форма: расследование — физические версии `ods.order_dist` против `ods.order_v`.
Проверка: эталон `argMax` по `updated_at` прямо из сырья (прием #165).
**8. Часовой пояс как способ смотреть на время.**
Урок: у ночных событий `toDate(UTCEventTime)` может не совпадать с `EventDate`. Это нормальное свойство данных. День нужно считать по готовой колонке и явно называть часовой пояс, в котором его рассматривают.
Материал: `docs/architecture/storage.md`, «Часовые пояса»; ADR 0015.
Форма: расследование данных.
Проверка: менти сам находит ночные события и объясняет расхождение.
**9. Сырое против разобранного: `ecommerce`.**
Урок: типизированный разбор JSON может молча подменить ошибку значением. Например, `qty` со значением «oops» превратится в 0. Поэтому правильность разбора нужно подтверждать инвариантом источника.
Материал: колонка `ecommerce` рядом с массивами `purchase_*` / `product_*` в `ods.event`; разбор `items` в `sql/dds/order_rebuild.sql`; спека, раздел 1.2 («кандидат будущей лабы»).
Форма: построить свое по ТЗ — разобрать `ecommerce` запросом и сверить с готовыми массивами того же события.
Проверка: построчная сверка с колонками-массивами.
## DDS — модель
**10. Лаба сессий: своя нарезка против эталона.**
Урок: сессия — расчетная сущность, границы которой определяет логика в хранилище. Самостоятельная нарезка может разойтись с эталоном. Если расхождение невозможно, задание сводится к `GROUP BY` по готовому полю.
Материал: `docs/architecture/dds/session.md`, `sql/dds/session_rebuild.sql` (два окна, полный порядок `(event_time, event_id)`, детерминированный чекан), эталон `VisitID` в `ods.event_v`.
Форма: построить свое — менти собирает нарезку сам до знакомства со стендовым кодом; либо вычитание `session_rebuild.sql` (раздел о гипотезе ниже).
Проверка: `dq_check` / `sessions_vs_reference` (зерно кука × день против `uniqExact(VisitID)`); затем сравнение со стендовой нарезкой по `session_id`.
**11. Поштучный разбор расхождения нарезки.**
Урок: счетная сверка показывает, сколько сессий разошлось. Чтобы понять, где именно отличаются границы, нужно вручную соединить свою сессию с эталонной по времени.
Материал: `sql/dq/sessions_vs_reference.sql`, `docs/architecture/dds/session.md`, «Сверка с эталоном» («поштучное сопоставление границ — материал лабы»).
Форма: расследование.
Проверка: найденная кука, день и пауза, на которой решения разошлись.
**12. Склейка идентичностей во времени (#145).**
Урок: текущее знание о пользователе и знание на выбранную дату дают разные отчеты. Если использовать сведения из будущего, исторический график известной аудитории изменится.
Материал: `dds.identity_map_v` (пары без временной колонки — решение `docs/architecture/dds/identity.md`), момент связи выводится как минимум времени `purchase`-события, соединенного с заказом.
Форма: построить свое (запрос, без изменения стенда).
Проверка: два способа на одном наборе данных; расхождение объясняется письменно.
**13. Справочник регионов (#143).**
Урок: справочник содержит данные, которыми источник владеет вне хранилища. Если собрать словарь руками, можно разобраться в механике, которую готовый каталог стенда позволяет только использовать.
Материал: образец `sql/ddl/05-dic-catalog.sql` и ADR 0012; зацепка в контракте — `region_city_id` / `region_country_id` отдаются как есть (`dds.event_v`).
Форма: построить свое по образцу (свой CSV, подложка, словарь, `dictGet` в отчете).
Проверка: обогащенный отчет; неатомарность обновления словаря на двух нодах — дополнительное наблюдение (спека, раздел 12).
**14. Инвариант позиций: недостающая проверка.**
Урок: равенство «цена × количество = items_total» проверяет, не исказил ли молчаливый разбор данные. Сейчас это равенство проверяют вручную, хотя ему нужно постоянное место в контуре DQ.
Материал: комментарий в `sql/dds/order_rebuild.sql` («постоянное место такой проверки — контур DQ этапа 4; пока она делается руками»), образцы `sql/dq/*_vs_dds.sql`, `dags/dq_check.py`.
Форма: построить свое — новая проверка по образцу существующих пяти.
Проверка: проверка обязана уметь краснеть — испортить позицию руками и увидеть красное (`docs/architecture/testing.md`).
**15. Сверка слоя с источником по дню.**
Урок: трансформация выполняет заданное правило, но сама не замечает, что часть данных не приехала. Это обнаруживает отдельная сверка слоя с источником.
Материал: `docs/architecture/etl.md`, «Чего конвейер о себе не знает» (три слепых пятна и лекарство: счет заказов и поздний `updated_at` с обеих сторон).
Форма: построить свое — вторая самодельная DQ-проверка.
Проверка: снести часть дня в ODS-окне — проверка краснеет, конвейер сам не замечал.
## DM — витрины
**16. Пять домашек DM (готовое ядро).**
Урок: готовые ТЗ уже содержат пять отдельных уроков (`docs/labs/README.md`): `events_enriched_v` показывает, что слой задает контракт, а не копирует данные; `top_pages_daily` меняет зерно, материализует витрину и включает ее в конвейер; `session_overview` учит хранить слагаемые вместо средних и не складывать уникальные значения, даже если на текущих данных результат совпадает; `dq_errors_daily` показывает, что у брака есть собственный день и срок жизни, а витрина может пережить источник; `utm_effectiveness` объясняет, зачем в имени нужно предупреждение `declared_`.
Материал: пять готовых ТЗ в `docs/labs/README.md`.
Форма: построить свое по ТЗ.
Проверка: `dags/homework_check.py` + `sql/dq/*_vs_dds.sql` (полное внешнее соединение с независимым ответом).
**17. Свой дашборд в Superset (готовая необязательная лаба).**
Урок: пользовательские объекты BI хранятся отдельно от стендовых и переживают перезапуск. Стендовые объекты при импорте перезаписываются.
Материал: `docs/labs/superset-dashboard.md`, `docs/architecture/superset.md`.
Форма: построить свое.
Проверка: глазами — счетного инварианта у раскладки панелей нет.
**18. Инкремент против пересборки (#149, повышенная сложность).**
Урок: чтобы обновлять меньше данных, приходится хранить историю изменений. Полная перезагрузка эту историю стирает.
Материал: `sql/dm/daily_traffic_rebuild.sql` (полный пересчет), `docs/architecture/dm.md`, «Ритм пересборки» и отклоненный вариант; момент `first_matched_at` — из сюжета 12.
Форма: построить свое (инкрементальная альтернатива рядом со стендовой).
Проверка: бесплатный контроль — результат обязан совпасть с эталонной витриной `dm.daily_traffic_v`.
**19. Вычитание стендовых витрин.**
Урок: при сверке двух источников нужен `GLOBAL FULL OUTER JOIN` с подзапросами с обеих сторон, `join_use_nulls` и явный приоритет классов. Значение с отсутствующей стороны должно остаться `NULL`, а не превратиться в ноль.
Материал: `sql/dm/purchase_vs_orders_rebuild.sql` (и соседние `revenue_daily_*`, `daily_traffic_*`), `docs/architecture/dm.md`.
Форма: вычитание — с оговоркой: запросы `sql/dq/*_vs_dds.sql` лежат открыто и почти дословно повторяют рецепт витрины. Предупреждение из `docs/labs/README.md` («открыв их до того, как задание сделано, ты обменяешь урок на готовый SQL») здесь особенно важно.
Проверка: `dq_check`, группа `marts_vs_dds` + `classes_vs_inventory`.
## BI — Superset (сюжеты сверх готовой лабы)
**20. Ловушка ноды 2.**
Урок: забытый `ON CLUSTER` и представление над локальной таблицей могут проявиться только в дашборде. Superset намеренно читает ноду 2, а DDL запускается с ноды 1.
Материал: `docs/architecture/superset.md`, «Устройство зоны»; спека, «Роли нод».
Форма: расследование-опыт (создать объект без `ON CLUSTER`, открыть дашборд).
Проверка: своя же находка и починка.
## Поперек слоев
### Airflow-переобработка по дневным партициям
**21. Переиграть день X.**
Урок: объем повторного прогона определяется состоянием хранилища. После падения конвейеру не нужно помнить параметры прошлого запуска: он ищет первую несобранную дату, а не последнюю собранную.
Материал: `sql/dds/order_scope.sql` и `session_scope.sql` (окно + хвост от первой дыры, `GLOBAL NOT IN`), `dags/dds_transform.py` (день — отдельная размноженная задача замены), `docs/architecture/etl.md`.
Форма: расследование-опыт — снести партицию дня в `dds.session_rep`, запустить `etl_pipeline` без параметров, увидеть, как объем сам нашел дыру.
Проверка: `dq_check` зеленый, день вернулся тем же (детерминированный чекан).
**22. Атомарность — пошардовая.**
Урок: `REPLACE PARTITION` заменяет данные в локальных таблицах по одному шарду. Если одна из двух замен завершится ошибкой, половина дня останется старой. Сам конвейер этого не заметит, поэтому нужна отдельная сверка.
Материал: `sql/dds/order_replace.sql` (комментарий о границе обещания), `docs/architecture/etl.md`, «Чего конвейер о себе не знает»; правило донора — `sql/ddl/45-dds-tables.sql`.
Форма: расследование (в паре с сюжетом 15).
Проверка: `purchase_vs_orders_vs_dds` или самодельная сверка по дню.
### Сверка purchase vs orders (классы A–D)
**23. Воронка подтверждений и недосчет трекера (#186, класс B).**
Урок: со стороны трекера потерянное событие можно обнаружить только по разрыву воронки между `/confirmation` и `purchase`. Отсюда правило: поведение считаем по трекеру, деньги — по бэкенду.
Материал: `lost_event` в `dm.purchase_vs_orders_v`, события дня в `dds.event_v`; механика — `docs/architecture/orders/fate.md` («потеря точечная»).
Форма: расследование.
Проверка: оцененный недосчет сходится с долей `lost_event` закрытых дней.
**24. Бизнес-дубль и наивная выручка (#187, класс D).**
Урок: движок удаляет технические дубли, а бизнес-дубли приходится определять самим — по `purchaseID`, а не по совпадению строк. `ReplacingMergeTree` не удалит дубль с новым `WatchID`, и это правильное поведение.
Материал: `duplicate_event`, свертка по `purchase_id[1]` в `sql/dm/purchase_vs_orders_rebuild.sql`; `fate.md` («дубль сюжетный»).
Форма: расследование — наивный `SUM` против дедуплицированного.
Проверка: разница равна вкладу дублей; сверка с `dm.revenue_daily_v`.
**25. Дельта суммы объясняется сравнением позиций (класс C).**
Урок: `amount_delta` обозначает конкретное расхождение, а не случайный шум. Его можно до копейки объяснить вычеркнутой позицией, если разобрать вложенный JSON и применить `ARRAY JOIN`. Ради этого навыка позиции и разбираются.
Материал: `amount_delta` в сверке, массивы `product_*` события против `item_*` заказа; `fate.md` («дельта — вычеркнутая позиция»).
Форма: расследование.
Проверка: найденная позиция и ее полная стоимость равны дельте.
**26. Две дороги отмены и история версий (класс A).**
Урок: текущее состояние заказа не сохраняет его историю. Различить случаи «оплачен и отменен» и «отменен неоплаченным» можно только по следам: физическим версиям ODS до слияния и сырью STG, пока оно хранится трое суток.
Материал: `fate.md`, «Что из двух дорог видно»; `ods.order_dist`, `stg.orders_raw_dist`; выручка дня, которая выросла и убыла.
Форма: расследование с ограничением по времени — часть случаев неразличима принципиально, и признать это тоже урок.
Проверка: списки заказов по дорогам, сверенные с историей версий.
### Опоздания и закрытие дня
**27. Опоздавшие заказы и закрытое окно (#188).**
Урок: отчет за день зависит от момента расчета. День можно считать закрытым, когда источник уже не способен его изменить. Поэтому опись хранит счетчики классов только для закрытых дней.
Материал: `awaiting_order` в `dm.purchase_vs_orders_v`, задержка слепков D+1/D+2 (`fate.md`, «Опоздание»), `data/world-inventory.json` (счетчики `orders` только у дней с закрытым окном), возврат `awaiting_order`-дней в объем — `sql/dm/purchase_vs_orders_scope.sql`.
Форма: расследование по ходу проживаемых дней.
Проверка: вчерашний отчет, не сошедшийся утром, сходится завтра; разность слепков называет опоздавших.
**28. Дыхание выручки.**
Урок: значение выручки за день меняется, пока день внутри окна изменяемости, а после выхода из окна фиксируется. Эти изменения видны как повторные пересборки партиции.
Материал: `dm.revenue_daily_v` по ходу дней, окно из ярлыка `snapshot_date` (`sql/dm/revenue_daily_scope.sql`), K = 7 — константа мира.
Форма: расследование-наблюдение (снять один день несколько раз).
Проверка: за окном значение больше не меняется.
### Мониторинг и runbook
В репозитории пока нет runbook для случаев «keeper упал / DDL повис». Спека оставляет его в хвосте этапа 9 вместе с `system.text_log` (ADR 0002), потому что такой документ должен опираться на реальный опыт. Поэтому менти может написать runbook сам: по шагам сломать сценарий, записать наблюдения и оставить стенду документ, которого сейчас не хватает.
**29. Keeper — общая память кластера.**
Урок: без keeper останавливаются DDL и репликация. На дашборде это бывает видно раньше, чем в ошибках запросов. Читаемый путь в keeper помогает понять, какие данные он хранит и за что отвечает.
Материал: панель keeper и очередь DDL в `docs/architecture/monitoring/cluster.md`, `system.zookeeper` (читаемый путь — `docs/architecture/storage.md`, «Путь реплицированных таблиц»), `make smoke` / `make check-clickhouse`.
Форма: расследование-опыт «посмотрели — сломали — увидели — вернули» (остановить keeper), итог — свой runbook.
Проверка: возврат к зеленым целям `make`; шаги записаны.
**30. Повисший DDL.**
Урок: каждый запрос `ON CLUSTER` проходит через keeper. Очередь распределенных DDL показывает, дошел ли запрос до обеих нод и что ответила каждая из них.
Материал: `system.distributed_ddl_queue`, панель «Распределенный DDL» (`cluster.md`: единственная панель без `clusterAllReplicas` — и почему).
Форма: расследование-опыт (DDL при недоступной второй ноде, таймаут `ON CLUSTER`).
Проверка: запись в очереди со статусом по нодам; починка доводит до `Finished`.
**31. Мониторинг сам стоит денег.**
Урок: запросы Grafana попадают в `system.query_log` вместе с запросами людей. Самый частый клиент хранилища — сам мониторинг. Если отфильтровать его запросы, стоимость наблюдаемости станет невидимой.
Материал: `docs/architecture/monitoring/queries.md` (панель «Кто спрашивает», строки начала и конца, куски шардов в панели ошибок).
Форма: расследование.
Проверка: собственное объяснение верха списка тяжелых запросов.
**32. Свежесть второго источника — пила, а не линия.**
Урок: возраст заказов растет в течение дня и резко уменьшается после нового слепка. Так устроен источник; это не признак деградации. Для панелей свежести, потока и брака выбираются разные слои данных. Если выбрать неверный слой, панель даст правдоподобный, но ложный ответ.
Материал: `docs/architecture/monitoring/data.md` (почему свежесть — ODS, поток и брак — сырье, окно доли брака).
Форма: расследование.
Проверка: предсказать форму графика до взгляда на него.
### За границей лаб — опорные точки лекций
Спека (раздел 12) оставляет несколько сюжетов для лекций и не включает их в лабораторные работы: `Sign` и CollapsingMergeTree (почему на стенде `sum(Sign)` равен `count()`, а в бою нет), «как выбирают топологии в бою», идентичность «как в бою» (`setUserID`, вероятностная склейка, identity graph), анти-паттерны ключа шардирования. Последний сюжет уже входит в домашние задания DM как требование защитить выбранный ключ (`docs/labs/README.md`).
## Гипотеза «вычитание + DQ» для нижних слоев
В #148 предложено удалить SQL трансформаций потока и написать его заново. Владелец уточнил, что даги менять не нужно: это несущая инфраструктура. Обратную связь дают инварианты, которые не зависят от реализации слоя. Ниже каждый поток проверяется отдельно: что можно отдать на переписывание, что должно остаться, чем проверять результат и где заканчивается эталон.
На всю таблицу влияют два общих факта:
- `homework_check` покрывает только пять витрин DM. `dq_check` содержит пять проверок, но внешний эталон есть лишь у двух: `sessions_vs_reference` использует чужой чекан `VisitID` из `ods.event_v`, а `classes_vs_inventory` — опись мира. Три проверки `*_vs_dds` пересчитывают витрину из DDS по тому же рецепту. Они находят ошибку в сборке витрины, но не ошибку в DDS, потому что обе стороны читают один слой.
- Все сверки читают таблицы `_dist`, поэтому их агрегаты не зависят от раскладки (спека, раздел 6). Если строка попала не на свой шард, счетные проверки останутся зелеными. Это ловит только пересчет ключа шардирования по `hostName()` (прием из `docs/architecture/storage.md`, замер #137). Такой пересчет нужно явно включать в ТЗ каждого вычитания.
### По потокам
**События STG (прием).** На переписывание этот участок не отдается. Здесь почти нет трансформации: `stg.hits_raw_mv` перекладывает колонки (`sql/ddl/40-stg-views.sql`). При этом пересоздание матвью на работающем стенде незаметно пропускает ближайшие сообщения (замер в `storage.md`, «Что проверено»). Чтец, таблицы сырья и матвью остаются несущей стеной. Урок этого участка раскрывают расследования из сюжетов 1–5.
**События STG → ODS (разбор).** Вся логика — в DDL, в паре матвью `sql/ddl/30-ods-views.sql`; отдельных файлов событий в `sql/ods/` нет. Удаление и пересоздание этих матвью остановит прием и снова создаст зазор с пропущенными сообщениями. Поэтому обычное вычитание здесь не подходит. Рабочая форма — **«собери рядом»**: выполнить пакетный `INSERT SELECT` из сырья в собственную таблицу по срезу `_load_ts`, а затем построчно сверить результат с нетронутым `ods.event_v`. Эталоном служат стендовый ODS и подневный счет против описи (`make check-clickhouse`). Однако опись проверяет только количество и только для покрытых ею дней. Содержимое неопорных колонок остается без независимой проверки: неверно разобранные UTM или браузер нигде не покраснеют, потому что витрины сверяются с DDS, а DDS читает ODS. Срок работы тоже ограничен: сырье хранится трое суток.
**Заказы STG (забор, `sql/stg/orders_raw_load.sql`).** Формально трансформация вынесена в отдельный файл, но ее учебный смысл сводится к перекладке колонок, а цена ошибки велика. Офсеты коммитятся при чтении, поэтому неверный повтор унесет слепок до следующего переигрыша дня через пульт. Этот участок остается стеной. Эталон описи — хеш байтов слепка — проверяет топик, но не сам забор.
**Заказы STG → ODS (`sql/ods/order_parse.sql`).** Вычитание возможно с оговоркой. Это пакетный шаг, идемпотентный по `_load_id`: повтор не меняет `ods.order_v` («Что проверено» в `orders/ingestion.md`). Брак обрабатывается отдельной целью, а слепок можно вернуть переигрышем дня. Обратную связь менти собирает сам: проверяет равенство «сырье среза = годные + брак» по `_load_id`, а текущее состояние сверяет с `argMax` по `updated_at` из сырья. Ниже по течению результат проверяет `classes_vs_inventory` через DDS и витрину. Слабое место — ветвь брака. Чистый мир не производит брак, поэтому отрицание предиката останется непроверенным, пока менти сам не подложит по строке каждого класса. Готовый рецепт есть в `orders/ingestion.md`, «Риски и проверка»; после опыта данные нужно обязательно убрать.
**DDS сессия (`session_scope` / `rebuild` / `replace`).** Это лучший поток для вычитания: здесь гипотеза подтверждается без дополнительных условий. Эталон `VisitID` внешний, а `sessions_vs_reference.sql` считает чужой чекан и не повторяет алгоритм нарезки. Ошибиться можно в важных местах: в полном порядке окон, границе суток в `PARTITION BY`, детерминированном чекане и расчете объема из позиции мира. Эталон заканчивается на атрибутах строки. Сверка работает на зерне кука × день и проверяет только число сессий. Поэтому неверные границы, длительность, вход, выход или UTM могут пройти зеленым. `session_overview` из `homework_check` тоже согласится, потому что сверяет витрину с тем же слоем. Вторая ступень обратной связи — поштучная сверка границ по `VisitID` из сюжета 11, которую менти пишет сам.
**DDS заказ (`order_scope` / `rebuild` / `replace`).** Вычитание возможно при условии: сначала нужно построить недостающую проверку. Косвенный эталон уже есть. `classes_vs_inventory` ловит потерянные заказы и неверные статусы закрытых дней, а `purchase_vs_orders` проверяет `items_total`. Но независимо не проверяются разбор массивов `item_*` и время в поясе `Europe/Samara`. Выручка сойдется сама с собой, а счетчиков выручки в описи намеренно нет (спека, раздел 8). Стенд уже подсказывает решение: до вычитания нужно реализовать инвариант «цена × количество = items_total» из сюжета 14. Последовательность «сначала сторож, потом ломать» сама становится частью урока.
**DDS карта идентичностей (`identity_map_rebuild.sql`).** Для чистого вычитания этот поток не подходит: независимого эталона нет. `daily_traffic_vs_dds` читает карту с обеих сторон, поэтому пустая или задвоенная карта пройдет проверку. Счетчики идентичности в опись не входят (спека, раздел 8). Менти может сам написать слабые инварианты: `known_users <= visitors` и «каждая пара подтверждена покупкой и заказом». Они проверят допустимость пар, но не их полноту. Подходящие формы — «собери рядом и сверь со стендовой картой» либо вычитание вместе с самодельной проверкой, которая честно названа неполной.
**DM стендовые витрины.** Формально это самый защищенный участок: `dq_check` независимо пересчитывает каждую витрину. На деле проверка содержит готовый ответ: `revenue_daily_vs_dds.sql` дословно повторяет рецепт витрины. Вычитание возможно как сюжет 19, но только с предупреждением из `docs/labs/README.md` о цене подглядывания.
### Сводная таблица
| Поток | Отдать на переписывание | Несущая стена | Обратная связь | Где кончается эталон |
|---|---|---|---|---|
| События STG | ничего | чтец, сырье, `hits_raw_mv` | опись (счет) | все, кроме счета |
| События STG→ODS | «собери рядом», не вычитание | обе матвью, `*_errors` | стендовый ODS рядом, опись | содержимое неопорных колонок; окно сырья 3 суток |
| Заказы STG | ничего | чтец, забор | хеш слепка в описи | сам забор |
| Заказы STG→ODS | `order_parse.sql` | `*_errors`, чтец | баланс среза по `_load_id`, `argMax(updated_at)`, опись классов | ветвь брака — пока не подложен брак руками |
| DDS сессия | `session_*` (все три) | донор, DDL, даг | `sessions_vs_reference` (внешний эталон) | атрибуты строки — нужна поштучная сверка |
| DDS заказ | `order_*` (все три) | донор, DDL, даг | опись классов, `items_total` в сверке | разбор позиций и пояс — сначала сюжет 14 |
| DDS карта | только с оговорками | DDL, даг | нет независимой | почти сразу; форма — «собери рядом» |
| DM витрины | `revenue/pvo/traffic` | доноры, DDL, даг | `dq_check` целиком | ответ лежит в самой проверке |
### Вывод
Гипотеза «вычитание + DQ» подходит не для всех нижних слоев. Без оговорок она работает только для сессий DDS: там есть внешний эталон, настоящие места для ошибок и проверка, которая не содержит готового решения.
Для заказов DDS и разбора заказов ODS нужна последовательность «сначала построй сторожа, потом ломай». Недостающие инварианты — сюжеты 14 и 15, а также баланс среза — сами входят в учебный материал. Их нужно построить до вычитания.
STG и прием событий нельзя отдавать в форме «удали и перепиши»: код там служит несущей частью стенда. Для этих участков подходят расследования и форма «собери рядом».
У всех счетных проверок остается общая дыра — раскладка по шардам. Поэтому пересчет ключа по `hostName()` должен входить в ТЗ каждого вычитания отдельной строкой.
+4 -1
View File
@@ -660,7 +660,10 @@ ADR 0002 с приёмочными совпадают почти целиком.
работой после этой спеки; редизайн лаб v1 (#7) остаётся в v1 и сюда не работой после этой спеки; редизайн лаб v1 (#7) остаётся в v1 и сюда не
переносится. Исключение — домашки слоя DM: ТЗ витрин менти и даг переносится. Исключение — домашки слоя DM: ТЗ витрин менти и даг
самопроверки забраны в этап 4 резолюцией #135, потому что витрина менти и самопроверки забраны в этап 4 резолюцией #135, потому что витрина менти и
конвейер, в который она встает, проектируются вместе. Единый стиль блока конвейер, в который она встает, проектируются вместе. Сюда же —
необязательная лаба «свой дашборд в Superset» (#197): она стоит на
витринах домашек DM и продумана до готового ТЗ в этапе 8, тогда как
остальные лабы пока на уровне идей. Единый стиль блока
домашек — за работой учебного пути. Спека даёт будущим лабам только домашек — за работой учебного пути. Спека даёт будущим лабам только
опорные точки — контрольные числа описи (сверка, идентичность). Явное опорные точки — контрольные числа описи (сверка, идентичность). Явное
следствие: после этапа 9 стенд работает, но учебного пути на нём ещё нет. следствие: после этапа 9 стенд работает, но учебного пути на нём ещё нет.
+293
View File
@@ -0,0 +1,293 @@
# Программа учебных лаб
Эта спека собирает программу лаб из
[инвентаризации сюжетов](../research/2026-08-31-labs-inventory.md), выполненной
в тикете #201. Тикет спеки — #202, карта — #182.
Статус: спека принята владельцем; судьба идей-кандидатов решена здесь поименно.
## 1. Кому и зачем
Менти уже прошел большой теоретический курс ClickHouse объемом около 60 часов.
Он знает устройство системы в теории, но почти ничего не делал своими руками.
Поэтому в программе нет вводного блока с теорией. Здесь теория становится
ремеслом: менти руками осваивает слои хранилища, DQ, оркестрацию и
идемпотентность.
Обещание программы: менти проходит конвейер стенда автором, а не читателем.
## 2. Устройство программы: основная программа и факультативы
У частей программы два статуса, третьего нет.
**Основная программа** — обязательная часть. Менти проходит ее целиком и по
порядку, потому что каждая следующая ступень опирается на предыдущую. От
ступени к ступени растет самостоятельность: сначала менти получает готовые ТЗ с
готовыми сторожами (DQ-проверками), а затем строит сторожей сам.
**Факультативы** — необязательные лабы. Они собраны в тематические разделы,
чтобы менти мог одним взглядом выбрать интересное. Для каждого раздела указано,
после какой ступени основной программы он открывается.
В программе используются четыре формы заданий:
- *построить свое по ТЗ* — рядом есть образец, но готового ответа нет;
- *вычитание* — SQL готовой трансформации удаляется и пишется заново, а даги
остаются как несущая инфраструктура;
- *собери рядом* — менти строит свою копию результата рядом с нетронутым
стендовым и сверяет их построчно;
- *расследование* — данные уже лежат на стенде или стенд намеренно ломается, а
менти ищет причину и объясняет ее.
## 3. Основная программа — семь ступеней
### Ступень 1. Построй пять витрин слоя DM
Первая часть программы опирается на шесть существующих заданий из
[`docs/labs/`](../labs/README.md). Пять ТЗ по витринам входят в первую ступень
основной программы и выполняются как есть. Их точечный пересмотр — работа
реализации. Шестое задание, про свой дашборд в Superset, остается факультативом
в разделе «Superset и BI». Набор уроков пяти витрин уже записан в их общем
описании: от слоя как договора до честных имен показателей.
Форма — построить свое по ТЗ. Обратную связь дает даг `homework_check`.
### Ступень 2. Свой отчет в Superset
Менти строит один график в Superset поверх витрины, которую сделал на первой
ступени. Урок — увидеть, как собственный результат из хранилища становится
отчетом в BI, не превращая знакомство с инструментом в отдельный большой
проект.
Форма — построить свое по ТЗ. Результат проверяется глазами. ТЗ для этой новой
ступени предстоит написать при реализации программы. Полный собственный дашборд
остается факультативом в разделе «Superset и BI».
### Ступень 3. Нарежь сессии сам
До того как открыть стендовый SQL нарезки сессий в DDS, менти самостоятельно
делит события на визиты. Сначала он сравнивает число своих сессий с эталоном
`VisitID`. Затем вручную соединяет свою сессию с эталонной и поштучно разбирает
хотя бы одно расхождение границ. Урок — счет называет проблему, но причину
приходится искать в отдельных строках.
Форма — построить свое по ТЗ и провести расследование. Обратная связь состоит
из счетной сверки с `VisitID` и ручного сопоставления границ.
### Ступень 4. Перепиши нарезку стенда
Менти удаляет и заново пишет SQL нарезки сессий DDS: расчет объема, пересборку
и замену. Урок — изменить расчетную сущность внутри готового конвейера и
проверить ее внешним эталоном. Это единственный поток, для которого
инвентаризация подтвердила вычитание без оговорок: здесь есть настоящие места
для ошибок, а проверка не содержит готового ответа.
Форма — вычитание. Обратную связь дают `dq_check` и поштучная сверка,
построенная на третьей ступени.
### Ступень 5. Сначала сторож, потом ломай: заказы
Сначала менти строит две недостающие DQ-проверки. Первая утверждает равенство
«цена × количество = items_total». Вторая сверяет слой с источником по дням. Их
менти строит по образцу пяти стендовых проверок в `sql/dq/`. Обе проверки
должны покраснеть на управляемой ошибке. Затем менти вручную подкладывает по
строке каждого класса брака, проверяет разбор и убирает эти строки за собой.
Только после этого он вычитает разбор заказов ODS `order_parse` и трансформацию
заказа DDS. Урок — сторожа строят до рискованного изменения, а не после первой
поломки.
Форма — построить свои проверки по ТЗ, провести управляемое расследование и
выполнить вычитание. Обратную связь дают две новые проверки, баланс годных и
ошибочных строк по срезу и существующие сверки заказов.
### Ступень 6. Разбери сырые события и сверь результат со стендом
Менти пакетно разбирает срез сырья событий в собственную таблицу и построчно
сверяет результат с нетронутым стендовым ODS. Урок — изучить разбор потока
отдельно от живого приема и использовать стендовый результат как образец для
сверки. Матвью разбора остаются несущей стеной: их пересоздание молча теряет
сообщения.
Форма — «собери рядом». Обратная связь — построчная сверка двух разборов. Ее
границы называются прямо: срок работы ограничен окном хранения сырья, а
содержимое неопорных колонок можно сравнить только со стендовым разбором, не с
независимым эталоном.
### Ступень 7. Мониторинг: куда смотреть
Сначала менти знакомится с панелями стенда. Затем проводит два безопасных
расследования. В первом он находит в `query_log`, что верх списка тяжелых
запросов занимает сам мониторинг. Во втором объясняет, почему свежесть второго
источника выглядит как пила, а не как линия, и выбирает правильный слой данных
для панели.
Форма — расследование. Обратная связь — найденные запросы и верное объяснение
формы графика и выбора слоя. Ломательные опыты остаются в факультативе «Что
делать, когда стенд сломался». Урок ступени — куда и на что смотреть в живой
системе.
**Общее правило вычитания:** ТЗ каждого вычитания на ступенях 4 и 5 включает
пересчет ключа шардирования по `hostName()`. Счетные сверки читают `_dist`,
поэтому учитывают строку даже на чужом шарде и остаются зелеными: они не видят
саму ошибку раскладки.
## 4. Факультативы — пять разделов
Ниже перечислены только названия сюжетов и их уроки. Материал, формы и способы
проверки остаются в [инвентаризации](../research/2026-08-31-labs-inventory.md).
### Почему выручка не сходится
Этот раздел рекомендован первым из факультативов. Он открывается после ступени
1 основной программы: материал живет на стенде с этапа 6.
- **Почему трекер недосчитывает покупки (#186).** Разрыв воронки подтверждений
показывает потерянные события и границу доверия к двум источникам.
- **Почему повтор покупки завышает выручку (#187).** Движок убирает технический
дубль строки, а повтор одной покупки приходится находить по деловому ключу.
- **Какая позиция объясняет разницу в сумме.** Расхождение суммы нужно
разложить до конкретной вычеркнутой позиции и объяснить до копейки.
- **Был ли отмененный заказ оплачен.** Текущее состояние не заменяет историю, а
некоторые различия после истечения срока сырья уже не восстановить.
- **Когда отчет за день можно считать окончательным (#188).** Отчет за день
становится окончательным лишь после того, как источник больше не может
изменить этот день.
- **Почему выручка за день еще меняется.** Внутри окна изменяемости выручка
движется, а после закрытия дня фиксируется.
### Тонкий анализ данных
Открывается после ступени 1 основной программы.
- **Что мы знали о пользователе в тот день (#145).** Текущее знание и знание на
дату дают разные отчеты; сведения из будущего меняют прошлое.
- **Добавь названия регионов в отчет (#143).** Менти собирает свой словарь по
образцу каталога и обогащает отчет через `dictGet`.
### Superset и BI
Открывается после ступени 2 основной программы.
- **Свой дашборд.** Готовая лаба
[`docs/labs/superset-dashboard.md`](../labs/superset-dashboard.md) учит
хранить пользовательские объекты отдельно от стендовых.
- **Почему дашборд не видит объект на второй ноде.** Забытый `ON CLUSTER` может
остаться незаметным до тех пор, пока дашборд не обратится ко второй ноде.
### Инженерия стенда
Открывается после ступени 1 основной программы. Сюжеты про переигрывание дня и
пошардовую атомарность раскроются полнее после ступени 4, когда менти уже
спускался в DDS.
- **Как принимать данные из Kafka: потоком или слепком.** Поток и слепок
требуют разных способов приема и дают разную наблюдаемость.
- **Почему данные попадают на свои шарды независимо от ноды чтения.**
Нода-потребитель может меняться, а раскладку строк определяет ключ
шардирования.
- **Почему вторая группа потребителей дублирует поток.** Еще одна группа
потребителей создает еще одну полную копию потока.
- **Как заметить пропущенные сообщения по офсетам.** Разрыв в офсетах
показывает потерю, которую трудно заметить по самим данным.
- **Почему сырье удаляется по времени загрузки.** Очистка сырья живет по
времени загрузки, а не по модельному дню.
- **Куда попадают битые строки.** Годные строки и брак делят поток без
пропусков и пересечений; менти подкладывает по строке каждого класса брака
событий и находит их в `ods.event_errors_dist`.
- **Какая версия заказа считается текущей.** Версию заказа задает слепок
источника, а `FINAL` скрывается за представлением.
- **Почему один момент попадает в разные дни.** Один момент дает разные дни в
разных поясах, поэтому день и пояс нужно называть явно.
- **Проверь, как стенд разобрал JSON в `ecommerce`.** Типизированный разбор
может молча подменить ошибку значением, поэтому менти сам разбирает сырой
JSON и сравнивает результат с массивами стенда.
- **Как конвейер восстанавливает пропущенный день.** Конвейер сам находит дыру
и определяет объем повторной обработки по состоянию хранилища.
- **Что будет, если день заменился не на всех шардах.** Замена дня завершается
отдельно на каждом шарде, поэтому целостность результата подтверждает
отдельная сверка.
- **Как обновлять витрину без полной пересборки (#149) — повышенная
сложность.** Экономия на пересчете требует хранить историю изменений, которую
полная пересборка стирает.
### Что делать, когда стенд сломался
Открывается после ступени 7 основной программы; весь раздел — повышенная
сложность.
- **Что ломается вместе с keeper.** Остановка keeper связывает состояние DDL и
репликации с тем, что видно на панелях и в системных таблицах.
- **Где искать запрос DDL, который не завершился.** Очередь распределенного DDL
показывает путь запроса и ответ каждой ноды.
Итог раздела — runbook, которого сейчас не хватает стенду. Менти пишет его по
результатам опытов, и документ остается в проекте.
## 5. Границы вычитания по потокам
Подробное сравнение потоков и таблица доказательств остаются в
[инвентаризации](../research/2026-08-31-labs-inventory.md). Спека берет это
сравнение за основу и проводит свои границы. Без оговорок вычитанию отдаются
только сессии DDS. Заказы можно вычитать лишь после самодельных сторожей. Для
событий ODS выбрана форма «собери рядом»; вычитание здесь отклонено. Прием STG
и забор заказов остаются несущей стеной и доступны только для расследований.
Карту идентичностей спека решает не отдавать вычитанию; причины этого выбора
названы в следующем разделе.
## 6. Что не вошло
Вычитание стендовых витрин DM не входит в программу. Оно повторяет урок
готового ядра DM, а открытые сверочные запросы в `sql/dq/` почти дословно
содержат ответ.
Форма «собери рядом» для карты идентичностей тоже не входит. Независимого
эталона нет, а слабые инварианты проверяют допустимость связей, но не полноту.
Аналитический урок идентичности уже несет склейка во времени (#145) в
факультативе «Тонкий анализ данных».
Сюжеты, которые инвентаризация относит к лекциям, остаются лекциями: `Sign` и
CollapsingMergeTree, топологии в бою и идентичность «как в бою». Эта спека
оставляет их в
[списке опорных точек раздела 12 спеки v2](2026-07-30-stand-v2-realism.md#опорные-точки-для-будущих-лекций-и-лаб).
## 7. Судьба идей-кандидатов
Эти решения закрывают тикеты после приемки спеки:
- #143, справочник регионов — вошла в факультатив «Тонкий анализ данных».
- #145, склейка во времени — вошла в факультатив «Тонкий анализ данных».
- #148, домашки ниже DM и вычитание — вошла в основную программу как ступени
4–6. Форму для каждого потока уточнила инвентаризация #201: вычитание,
«сначала сторож» или «собери рядом».
- #149, инкрементальный `daily_traffic` — вошла в факультатив «Инженерия
стенда» с пометкой «повышенная сложность».
- #186, воронка подтверждений — вошла в факультатив «Почему выручка не
сходится».
- #187, бизнес-дубль — вошла в факультатив «Почему выручка не сходится».
- #188, опоздавшие заказы — вошла в факультатив «Почему выручка не сходится».
## 8. Отклоненные варианты оси
- **Чистое «снизу вверх»: STG → ODS → DDS → DM.** Оно начинает программу с
самого тяжелого по инфраструктуре участка. Внизу быстро кончается эталон
обратной связи, а вычитание вслепую заставляет переписывать слой, которым
менти еще не пользовался.
- **Два прохода: сначала расследования снизу вверх, потом постройка сверху
вниз.** Пролог из восьми расследований откладывает первый собственный
результат. Вместо этого расследования разнесены по факультативам и
открываются по мере прохождения основной программы.
- **Сюжетами поперек слоев.** Такой порядок ломает принятое решение, по
которому готовые задания открывают программу, и превращает путь в сетку
зависимостей.
## 9. Границы спеки и что дальше
Спека не переписывает существующие ТЗ в `docs/labs/` и общий README; точечный
пересмотр относится к реализации.
Реализация программы тоже находится за границей спеки. В нее входят новые ТЗ
для ступеней 2–7 основной программы и факультативов, а также отражение порядка
программы в `docs/labs/README.md`. Карту #182 приемка этой спеки закрыла;
исполнение ведет отдельная карта — тикет #206 (решение тикета #203).
@@ -0,0 +1,31 @@
slice_name: Сверка purchase и заказов
description: null
certified_by: null
certification_details: null
viz_type: pivot_table_v2
params:
viz_type: pivot_table_v2
groupbyRows:
- order_day
groupbyColumns:
- mismatch_class
metrics:
- order_count
adhoc_filters:
- expressionType: SIMPLE
subject: order_day
operator: TEMPORAL_RANGE
comparator: No filter
clause: WHERE
aggregateFunction: Sum
metricsLayout: COLUMNS
rowOrder: key_a_to_z
colOrder: key_a_to_z
order_desc: true
row_limit: 10000
valueFormat: SMART_NUMBER
date_format: smart_date
cache_timeout: null
uuid: 19600000-0000-4000-8000-000000000102
version: 1.0.0
dataset_uuid: 19600000-0000-4000-8000-000000000002
@@ -0,0 +1,30 @@
slice_name: Выручка по дням и категориям
description: null
certified_by: null
certification_details: null
viz_type: echarts_timeseries_bar
params:
viz_type: echarts_timeseries_bar
x_axis: report_date
metrics:
- revenue
groupby:
- product_category
adhoc_filters:
- expressionType: SIMPLE
subject: report_date
operator: TEMPORAL_RANGE
comparator: No filter
clause: WHERE
row_limit: 10000
stack: Stack
orientation: vertical
show_legend: true
rich_tooltip: true
show_value: false
y_axis_format: ',.2f'
x_axis_time_format: smart_date
cache_timeout: null
uuid: 19600000-0000-4000-8000-000000000101
version: 1.0.0
dataset_uuid: 19600000-0000-4000-8000-000000000001
@@ -0,0 +1,29 @@
slice_name: Посетители и известные пользователи
description: null
certified_by: null
certification_details: null
viz_type: echarts_timeseries_line
params:
viz_type: echarts_timeseries_line
x_axis: report_date
metrics:
- visitors
- known_users
groupby: []
adhoc_filters:
- expressionType: SIMPLE
subject: report_date
operator: TEMPORAL_RANGE
comparator: No filter
clause: WHERE
row_limit: 10000
seriesType: echarts_timeseries_line
show_legend: true
rich_tooltip: true
show_value: false
y_axis_format: ',d'
x_axis_time_format: smart_date
cache_timeout: null
uuid: 19600000-0000-4000-8000-000000000103
version: 1.0.0
dataset_uuid: 19600000-0000-4000-8000-000000000003
@@ -0,0 +1,105 @@
dashboard_title: Бизнес-сюжеты
description: null
css: null
slug: business-stories
certified_by: null
certification_details: null
published: true
uuid: 19600000-0000-4000-8000-000000000201
position:
DASHBOARD_VERSION_KEY: v2
ROOT_ID:
id: ROOT_ID
type: ROOT
children:
- GRID_ID
GRID_ID:
id: GRID_ID
type: GRID
parents:
- ROOT_ID
children:
- ROW-revenue
- ROW-pvo
- ROW-traffic
ROW-revenue:
id: ROW-revenue
type: ROW
parents:
- ROOT_ID
- GRID_ID
children:
- CHART-revenue
meta:
background: BACKGROUND_TRANSPARENT
CHART-revenue:
id: CHART-revenue
type: CHART
parents:
- ROOT_ID
- GRID_ID
- ROW-revenue
children: []
meta:
chartId: 1
height: 50
width: 12
sliceName: Выручка по дням и категориям
uuid: 19600000-0000-4000-8000-000000000101
ROW-pvo:
id: ROW-pvo
type: ROW
parents:
- ROOT_ID
- GRID_ID
children:
- CHART-pvo
meta:
background: BACKGROUND_TRANSPARENT
CHART-pvo:
id: CHART-pvo
type: CHART
parents:
- ROOT_ID
- GRID_ID
- ROW-pvo
children: []
meta:
chartId: 2
height: 50
width: 12
sliceName: Сверка purchase и заказов
uuid: 19600000-0000-4000-8000-000000000102
ROW-traffic:
id: ROW-traffic
type: ROW
parents:
- ROOT_ID
- GRID_ID
children:
- CHART-traffic
meta:
background: BACKGROUND_TRANSPARENT
CHART-traffic:
id: CHART-traffic
type: CHART
parents:
- ROOT_ID
- GRID_ID
- ROW-traffic
children: []
meta:
chartId: 3
height: 50
width: 12
sliceName: Посетители и известные пользователи
uuid: 19600000-0000-4000-8000-000000000103
metadata:
color_scheme: supersetColors
cross_filters_enabled: false
chart_configuration: {}
expanded_slices: {}
default_filters: '{}'
native_filter_configuration: []
theme_uuid: null
version: 1.0.0
@@ -0,0 +1,106 @@
table_name: daily_traffic_v
main_dttm_col: report_date
currency_code_column: null
description: null
default_endpoint: null
offset: 0
cache_timeout: null
catalog: null
schema: dm
sql: null
params: null
template_params: null
filter_select_enabled: true
fetch_values_predicate: null
extra: null
normalize_columns: false
always_filter_main_dttm: false
folders: null
uuid: 19600000-0000-4000-8000-000000000003
metrics:
- metric_name: known_users
verbose_name: Известные пользователи
metric_type: null
expression: SUM(known_users)
description: null
d3format: ',d'
currency: null
extra: null
warning_text: null
- metric_name: visitors
verbose_name: Посетители
metric_type: null
expression: SUM(visitors)
description: null
d3format: ',d'
currency: null
extra: null
warning_text: null
columns:
- column_name: _load_ts
verbose_name: Время загрузки
is_dttm: true
is_active: true
type: DATETIME64(3, 'UTC')
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
datetime_format: null
extra: null
- column_name: known_users
verbose_name: Известные пользователи
is_dttm: false
is_active: true
type: UINT64
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
datetime_format: null
extra: null
- column_name: _load_id
verbose_name: Запуск загрузки
is_dttm: false
is_active: true
type: STRING
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
datetime_format: null
extra: null
- column_name: visitors
verbose_name: Посетители
is_dttm: false
is_active: true
type: UINT64
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
datetime_format: null
extra: null
- column_name: report_date
verbose_name: Дата
is_dttm: true
is_active: true
type: DATE
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
datetime_format: null
extra: null
version: 1.0.0
database_uuid: 4b8f2c6e-1d3a-4f5b-9c7d-2e8a1f0b3c5d
@@ -0,0 +1,136 @@
table_name: purchase_vs_orders_v
main_dttm_col: order_day
currency_code_column: null
description: null
default_endpoint: null
offset: 0
cache_timeout: null
catalog: null
schema: dm
sql: null
params: null
template_params: null
filter_select_enabled: true
fetch_values_predicate: null
extra: null
normalize_columns: false
always_filter_main_dttm: false
folders: null
uuid: 19600000-0000-4000-8000-000000000002
metrics:
- metric_name: order_count
verbose_name: Количество заказов
metric_type: null
expression: COUNT(order_id)
description: null
d3format: ',d'
currency: null
extra: null
warning_text: null
columns:
- column_name: items_total
verbose_name: Сумма позиций заказа
is_dttm: false
is_active: true
type: NULLABLE(DECIMAL(18, 2))
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
datetime_format: null
extra: null
- column_name: _load_ts
verbose_name: Время загрузки
is_dttm: true
is_active: true
type: DATETIME64(3, 'UTC')
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
datetime_format: null
extra: null
- column_name: declared_revenue
verbose_name: Выручка purchase
is_dttm: false
is_active: true
type: NULLABLE(DECIMAL(18, 2))
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
datetime_format: null
extra: null
- column_name: status
verbose_name: Статус заказа
is_dttm: false
is_active: true
type: LOWCARDINALITY(NULLABLE(STRING))
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
datetime_format: null
extra: null
- column_name: mismatch_class
verbose_name: Класс расхождения
is_dttm: false
is_active: true
type: LOWCARDINALITY(STRING)
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
datetime_format: null
extra: null
- column_name: order_day
verbose_name: День заказа
is_dttm: true
is_active: true
type: DATE
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
datetime_format: null
extra: null
- column_name: _load_id
verbose_name: Запуск загрузки
is_dttm: false
is_active: true
type: STRING
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
datetime_format: null
extra: null
- column_name: order_id
verbose_name: Заказ
is_dttm: false
is_active: true
type: STRING
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
datetime_format: null
extra: null
version: 1.0.0
database_uuid: 4b8f2c6e-1d3a-4f5b-9c7d-2e8a1f0b3c5d
@@ -0,0 +1,138 @@
table_name: revenue_daily_v
main_dttm_col: report_date
currency_code_column: null
description: null
default_endpoint: null
offset: 0
cache_timeout: null
catalog: null
schema: dm
sql: null
params: null
template_params: null
filter_select_enabled: true
fetch_values_predicate: null
extra: null
normalize_columns: false
always_filter_main_dttm: false
folders: null
uuid: 19600000-0000-4000-8000-000000000001
metrics:
- metric_name: revenue
verbose_name: Выручка
metric_type: null
expression: SUM(revenue)
description: null
d3format: ',.2f'
currency:
symbol: ₽
symbolPosition: suffix
extra: null
warning_text: null
columns:
- column_name: _load_ts
verbose_name: Время загрузки
is_dttm: true
is_active: true
type: DATETIME64(3, 'UTC')
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
datetime_format: null
extra: null
- column_name: aov
verbose_name: Средний чек
is_dttm: false
is_active: true
type: DECIMAL(18, 2)
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
datetime_format: null
extra: null
- column_name: revenue
verbose_name: Выручка
is_dttm: false
is_active: true
type: DECIMAL(18, 2)
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
datetime_format: null
extra: null
- column_name: product_category
verbose_name: Категория товара
is_dttm: false
is_active: true
type: LOWCARDINALITY(STRING)
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
datetime_format: null
extra: null
- column_name: _load_id
verbose_name: Запуск загрузки
is_dttm: false
is_active: true
type: STRING
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
datetime_format: null
extra: null
- column_name: orders
verbose_name: Заказы
is_dttm: false
is_active: true
type: UINT64
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
datetime_format: null
extra: null
- column_name: units
verbose_name: Товары
is_dttm: false
is_active: true
type: UINT64
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
datetime_format: null
extra: null
- column_name: report_date
verbose_name: Дата
is_dttm: true
is_active: true
type: DATE
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
datetime_format: null
extra: null
version: 1.0.0
database_uuid: 4b8f2c6e-1d3a-4f5b-9c7d-2e8a1f0b3c5d
-2
View File
@@ -1,3 +1 @@
version: 1.0.0 version: 1.0.0
type: Database
timestamp: '2026-07-30T00:00:00+00:00'