Compare commits

Author SHA1 Message Date
ddadmin 086007fbd4 docs(warehouse): объяснены расчет строк и замена дневных разделов
Зачем: выражения «собираются в донор» и «публикуются дни» требовали
знать устройство конвейера до чтения объяснения.

Что: описаны назначение промежуточных таблиц и последовательность
расчета, записи и замены разделов. Перечитаны остальные объяснения:
выбор дней, разворачивание позиций заказа, проверка сессий и повтор запуска.

Проверка: make lint, git diff --check, локальные ссылки и якоря.
SQL без комментариев и AST обоих дагов без строк документации не изменились.

Refs: #249
2026-09-13 15:16:26 -04:00
ddadmin 5f317ba8d2 docs(warehouse): упрощены объяснения витрин, сессий и пересчета
Зачем: читателю ступеней 1–4 нужно проследить выбор дней, расчет строки
и публикацию результата без расшифровки метафор.

Что: согласованы справочники DM, сессий, ETL и два раздела хранилища
с комментариями шести SQL-примеров. Уточнены ограничения повторного
запуска и выбора атрибутов при равном времени событий.

Проверка: make lint, git diff --check, локальные Markdown-ссылки и якоря.
SQL без комментариев совпадает; AST Python без строк документации совпадает.
Исторические разделы и материалы вне границ задачи сохранены.

Refs: #249
2026-09-13 14:33:39 -04:00
ddmitry 84dc718f62 docs(context): влита редактура словаря (#252) 2026-09-13 21:00:12 +03:00
ddadmin 5a3e3f92d9 docs(context): упрощен словарь понятий для начинающего
Зачем: понимать термины стенда без изучения устройства генератора.
Что: понятия сгруппированы по учебным темам, определения упрощены;
уточнены идентификаторы, модельное время и повторный запуск стенда.
Проверка: сохранены все 43 прежних термина; проверены 11 входящих
и 6 внутренних ссылок, якорь «Язык» и git diff --check.
2026-09-13 13:59:37 -04:00
ddadmin f3373f1e8f docs(map): упрощен язык интерактивной карты хранилища
Зачем: помочь учащемуся понять назначение объектов и обработку данных по стрелкам карты.

Что: вычитаны карточки и подписи, пояснены термины, удалены повторы. Описания обработки уточнены по SQL, HTML пересобран.

Проверка: сохранены 17 объектов и 17 стрелок, проверены 85 ссылок. В Chromium проверены карточки, фильтры, подсветка, Escape и отображение при ширине 1920 и 1280 пикселей. make lint и git diff --cached --check прошли.
2026-09-13 13:20:58 -04:00
ddadmin 915e976fc2 docs(handbook): упрощен язык путеводителя по стенду
Зачем: помочь учащемуся понять путь данных и работу служб перед выполнением заданий.

Что: вычитаны оглавление и пять глав, пояснены термины, убраны внутренние метафоры и повторы. Уточнены пояснения к SQL и показателям мониторинга.

Проверка: 62 локальные ссылки и 6 входящих ссылок на разделы проверены; 8 блоков SQL и схем сохранены. git diff --cached --check прошел.
2026-09-13 13:02:40 -04:00
ddadmin ce15836093 docs(readme): упрощен язык вводной инструкции
Зачем: помочь учащемуся понять назначение стенда и порядок работы с ним.

Что: убраны внутренние метафоры, пояснены термины, настройка перенесена перед запуском. Обновлена ссылка из путеводителя и удалена ссылка на предыдущий стенд.

Проверка: команды, адреса и пароли сохранены; 24 ссылки README и входящие ссылки на разделы проверены. git diff --cached --check прошел.
2026-09-13 12:47:16 -04:00
ddadmin 8c9c10a875 docs(labs): влита редактура учебных заданий
Зачем: сделать задания понятнее учащемуся и сохранить правила для будущих работ.

Что: объединены три коммита ветки docs/labs-human-language с сохранением истории через merge --no-ff.

Проверка: рабочее дерево чистое перед слиянием, origin/main входит в локальную main, итоговый diff прошел git diff --check. Ссылки и сохранность технических примеров проверены при редактуре.
2026-09-13 11:30:12 -04:00
ddadmin 1b95ce456c docs(labs): сокращены правила написания заданий
Зачем: оставить только необходимые ориентиры для автора учебного текста.

Что: раздел сокращен до трех согласованных абзацев, ссылки на оба образца сохранены.

Проверка: git diff --check прошел; проверен diff единственного измененного файла.
2026-09-13 11:26:55 -04:00
ddadmin 3bfe9ca4ee docs(labs): уточнены самопроверка и формулировки заданий
Зачем: убрать механические повторы в итогах и избежать ложных расхождений при проверке графика Superset.

Что: уточнены правила итоговых списков и название задания о проверках заказов. В разборе событий разделены учет повторных доставок и выбор событий ODS для сравнения. Первый график Superset и проверочный SQL используют весь период витрины.

Проверка: git diff --check, локальные ссылки и переходы к измененным заголовкам прошли. Блоки кода и таблицы требований сохранены. Согласованность периода графика с SQL проверена по тексту; повторный проход через интерфейс Superset не выполнялся.
2026-09-13 11:20:03 -04:00
ddadmin 4058e18864 docs(labs): упрощен язык учебных заданий
Зачем: помочь менти понять действия, проверить результат и оценить освоенное умение.

Что: вычитаны 13 заданий основной программы и общий индекс. Итоговые списки обращены к учащемуся, убраны история разработки и повторы. Правила учебного текста записаны в программе, из AGENTS.md добавлена ссылка.

Проверка: git diff --check, локальные ссылки и ссылки на измененные заголовки прошли. Блоки кода и таблицы требований совпадают с исходными. Упражнения на стенде повторно не выполнялись: изменен только текст.
2026-09-13 11:01:35 -04:00
ddadmin a03838b953 docs(labs): оформлен факультативный раздел Superset и BI
Зачем: дать менти два самостоятельных задания по BI после ступени 2.

Что: обновлено ТЗ дашборда, добавлен опыт с объектом на одной ноде, уточнены программа и индекс. Формулировки обращены к учащемуся.

Проверка: локальные ссылки и git diff --check прошли. На ClickHouse 26.3.17.56 воспроизведены ошибка чтения со второй ноды и исправление; учебный объект удален. Полный проход через интерфейс Superset не выполнялся.
2026-09-13 10:34:48 -04:00
ddmitry 3ac3911e95 Merge pull request 'feat(superset): подача дашборда «Бизнес-сюжеты»' (#247) from 200-superset-dashboard-polish into main
Reviewed-on: #247
2026-09-08 12:28:57 +03:00
ddadminandClaude Opus 5 b6c10ecf21 feat(superset): подача дашборда «Бизнес-сюжеты»
- Зачем:
  - экран отвечал только графиками: итога за период не было, доли категорий
    читались по высоте столбиков, а известные пользователи лежали на нуле
    рядом с посетителями.
- Что:
  - добавлены ряд крупных чисел (выручка, заказы, средний чек, посетители),
    кольцо долей категорий и линия доли известных пользователей;
  - средний чек и доля известных заведены метриками датасетов, знаменатель
    через `nullIf`: на пустом периоде деление Decimal на ноль в ClickHouse
    роняет панель;
  - фильтр «Период» сужает все панели, включая сверку по `order_day`;
  - `label_colors` закрепляет цвет за категорией и за рядами трафика;
  - деньги без копеек, единица в названии панели; подписи дат читаются как
    даты; сводная сверки развернута классами по строкам и уместилась без
    прокрутки;
  - обновлены справочник Superset и глава путеводителя.
- Проверка:
  - `make lint`;
  - `superset import-directory /app/import --overwrite` на живом стенде, экран
    снят в браузере: девять панелей отрисованы, цвет категории одинаков на
    столбиках и в кольце;
  - период 03.06–06.06 сузил все панели, включая сводную по `order_day`;
  - `SUM(revenue) / nullIf(SUM(orders), 0)` на периоде без заказов возвращает
    NULL, без `nullIf` — исключение 153.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 03:35:00 -04:00
ddadminandClaude Opus 5 3ad377e02e docs(superset): записаны отклоненные варианты подачи дашборда
- Зачем:
  - карту регионов и кросс-фильтрацию предложат снова, а доводы против них
    стоили двух линий ревью и замеров на живом стенде.
- Что:
  - в docs/architecture/superset.md добавлен раздел «Отклоненные варианты»:
    карта регионов, регион как общее измерение витрин, кросс-фильтрация между
    панелями, пороги на панелях.
  - к каждому отказу записаны проверенные факты и поводы вернуться к разговору.
- Проверка:
  - `git diff --check`.
  - `grep -rn "0018" docs` — ссылок на неоставленный ADR нет.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 16:48:24 -04:00
ddadmin 10c94b206a docs(agents): добавлены требования к читабельности
- Зачем:
  - сохранить ясный и естественный стиль текстов проекта.
- Что:
  - в CLAUDE.md добавлены требования к лёгкости чтения и отказу от манерной прозы.
- Проверка:
  - проверен diff CLAUDE.md.
2026-09-07 16:34:09 -04:00
ddadmin f438e91d80 docs(labs): уточнен запуск первой учебной витрины
- Зачем:
  - дать учащемуся воспроизводимый путь от своего DDL до первой проверки витрины.
- Что:
  - уточнено применение `sql/ddl/70-labs.sql` в работающем стенде через подключение `etl`.
  - добавлены указания запуска `homework_check` и чтения журнала для `events_enriched`.
- Проверка:
  - `git diff --check`.
  - `make config-test` — зелёный.
2026-09-07 09:12:26 -04:00
55 changed files with 2292 additions and 1663 deletions

No files matched your search

+3
View File
@@ -81,6 +81,9 @@ Airflow) и названия из кода. Если для понятия ес
писал: простые слова, короткие фразы, сложную мысль поясняй при первом писал: простые слова, короткие фразы, сложную мысль поясняй при первом
упоминании. Если понятность и буквальная точность спорят — выбирай понятность. упоминании. Если понятность и буквальная точность спорят — выбирай понятность.
При создании или редактировании лабораторных следуй
[правилам учебного текста](docs/specs/2026-09-01-labs-program.md#как-писать-задания).
Внутренние рассуждения и промежуточные пометки по ходу работы веди на Внутренние рассуждения и промежуточные пометки по ходу работы веди на
английском — он экономнее по токенам. На русском остаётся всё, что видит и английском — он экономнее по токенам. На русском остаётся всё, что видит и
хранит проект: итоговые ответы, документы, комментарии в коде и SQL, сообщения хранит проект: итоговые ответы, документы, комментарии в коде и SQL, сообщения
+7 -1
View File
@@ -1 +1,7 @@
@AGENTS.md @AGENTS.md
# Читабельность языка
Пиши легко для чтения. Сокращай количество мыслей, а не количество слов, которыми выражена каждая мысль.
Please remove all mannered prose.
+247 -201
View File
@@ -1,229 +1,275 @@
# Кликстрим-платформа (стенд v2) # Кликстрим-платформа (стенд v2)
Словарь понятий проекта: одни и те же слова для одних и тех же вещей — Словарь понятий, которые встречаются в учебных заданиях, документации и коде
у владельца, кода, документов и агентов. Только язык, никаких решений. стенда.
## Язык ## Язык
**Хранилище**: - [Люди и события](#люди-и-события)
Аналитическая база стенда — кластер ClickHouse со слоями STG/ODS/DDS/DM. - [Хранилище и форматы данных](#хранилище-и-форматы-данных)
Принимающая сторона границы «трекер | хранилище»: нормализует имена и стили - [Заказы](#заказы)
источников, строится по их документации. - [Модельное время и запуск стенда](#модельное-время-и-запуск-стенда)
_Избегать_: склад, склад данных - [Устройство генератора](#устройство-генератора) — для чтения его кода
и технических справочников.
**Сырые данные**: ### Люди и события
Слой STG хранилища и его содержимое: сообщение топика, положенное строкой,
как пришло, без разбора. В прозе — сырые данные и сырой слой; про
конкретную таблицу или базу — STG.
_Избегать_: сырьё
**Состав мира**:
Постоянная часть мира генератора — популяция посетителей, их привычки,
двухкуковые пары. По дням его выдаёт план состава.
_Избегать_: состояние мира
**План состава**:
Способ спросить состав мира: функция зерна, выдающая его по дням —
когорту новых кук, их возвраты, назначенные заказы двухкуковых пар.
**Подпоток**:
Ветвь дерева случайности генератора: своё зерно у состава мира, у каждого
дня и у каждого компонента дня. Подпоток задан позицией в дереве, а не
порядком вычислений.
**Конфигурация мира**:
Модуль чистых данных со всеми числами мира: приток, профили возвратов, доли
покупателей, D0. Правка модуля — смена мира. Модуль-близнец контракта схемы:
там колонки, здесь числа.
**Когорта дня**:
Люди, впервые пришедшие в мир в один день, со всеми их куками и днями
активности. Единица плана состава: когорта — функция зерна и номера дня.
**Человек**: **Человек**:
Устойчивая личность в составе мира, которой принадлежат одна или две куки. Участник модели магазина, который может заходить с одного или двух
Не поле источника: кликстрим видит куку, а заказ — пользователя магазина. устройств. Генератор знает, какие идентификаторы посетителя ему принадлежат,
но в событиях сайта этой связи нет.
**Посетитель**: **Посетитель**:
Кука, наблюдаемая в кликстриме и обозначенная `ClientID`; единица Браузер на устройстве, который счетчик сайта узнает по куке — сохраненному
`uniq(ClientID)`. Один человек может быть представлен несколькими посетителями. в браузере идентификатору. В кликстриме он обозначается `ClientID`:
_Избегать_: «посетитель» про человека после склейки `uniq(ClientID)` считает посетителей, и у одного человека их может быть несколько.
_Избегать_: «посетитель» про человека после объединения его идентификаторов.
**Пользователь магазина**: **Пользователь магазина**:
Представление человека со стороны заказов; в записи заказа обозначается Человек, известный серверной части магазина под идентификатором `user_id`.
`user_id`. Через заказы связывает разные куки человека, в кликстрим напрямую Он указан в заказах и позволяет связать разные `ClientID` одного человека;
не попадает. в кликстриме поля `user_id` нет. Например, покупки с телефона и ноутбука
могут иметь разные `ClientID`, но один `user_id` в заказах.
**Приток**:
Появление новых кук на всём протяжении оси модельного времени; единица —
кука (`ClientID`). Из-за притока накопленная аудитория растёт с
горизонтом и не совпадает с дневной.
**Хвост возвратов**:
Окно активности, отсчитанное от первого дня человека и общее на обе
его куки; дольше окна кука не возвращается.
**Предыстория**:
Когорты плана с первым днём активности до D0; событий не порождают.
**День активности**:
День, в который кука хоть раз появилась в мире: день её рождения или день
возврата. Единица плана состава: за день он у куки один, а визитов внутри
него бывает несколько.
_Избегать_: визит (визит — про сессию)
**Визит**: **Визит**:
Подряд идущие события одной куки без пауз длиннее 30 минут, не пересекающие Последовательность событий одного посетителя без пауз длиннее 30 минут
границу суток. Поле `VisitID` — эталон для лабы сессий. «Сессия» — то же и без перехода через полночь по часовому поясу счетчика. Сессия означает
понятие словами аналитики: в данных и в коде оно зовётся визитом, но имена то же самое; поле `VisitID` служит эталоном в задании по нарезке сессий.
вроде «лаба сессий» и «сборка сессий» остаются.
**День активности**:
День, в который посетитель появился на сайте хотя бы раз: впервые или
вернувшись. За один день активности у него может быть несколько визитов.
_Избегать_: визит в значении целого дня активности.
**Паспорт куки**: **Паспорт куки**:
Устройство и город, приписанные куке на всю жизнь: кука — это браузер на Устройство и город, закрепленные за посетителем во все дни его активности.
устройстве. Держит их план состава, разворачивает в поля события Генератор записывает их в поля событий. У двух посетителей одного человека
день-функция; у двухкуковой пары город один на две куки, устройства разные. город совпадает, а устройства различаются.
**Покупатель**:
Человек, которого план состава пометил склонным покупать. Метка значима:
помеченный доходит до заказа заметно чаще прочих, но и непомеченный иногда
покупает. Из покупателей отбираются двухкуковые пары.
_Избегать_: «покупатель» про того, кто купил в конкретный день — это визит
с заказом.
**День-функция**:
Функция (зерно, номер дня), выдающая упорядоченный поток событий этих
модельных суток. Состояния между днями нет: день D не зависит от того,
прожиты ли дни до него.
**Торговое событие**: **Торговое событие**:
Событие корзины или покупки — отдельная строка потока, а не просмотр Запись о добавлении товара в корзину или о покупке: `add_to_cart` или
страницы. Садится на ту страницу, где случилось: корзина — на карточку `purchase`. Это отдельная строка кликстрима со страницей, где произошло
товара, покупка — на страницу подтверждения заказа. действие: карточкой товара для корзины или страницей подтверждения для покупки.
### Хранилище и форматы данных
**Хранилище**:
Аналитическая база стенда в ClickHouse. Данные проходят слои STG, ODS, DDS
и DM: от исходных сообщений до витрин для отчетов. Хранилище принимает
данные по документации источников и приводит их к своей модели и именам.
_Избегать_: склад, склад данных.
**Сырые данные**:
Исходные сообщения Kafka, сохраненные в слое STG без разбора: одно сообщение
занимает одну строку таблицы. Слой STG также называют сырым слоем.
_Избегать_: сырьё.
**Справочник**: **Справочник**:
Данные, которые хранилище не производит, а получает готовыми со стороны: Готовые сведения из внешнего источника, которыми хранилище дополняет
содержимым владеет источник вне хранилища, слои только читают. Живут в базе свои данные. На стенде они находятся в базе `dic`; пока это только каталог
`dic`. Первый и пока единственный — каталог товаров. товаров. Объект `DICTIONARY` ClickHouse — одна из форм доступа к справочнику.
_Избегать_: словарь — этим словом в проекте зовут и глоссарий понятий, и _Избегать_: словарь в значении справочных данных: это слово уже обозначает
объект `DICTIONARY` ClickHouse, одну из форм, в которой справочник доступен. `DICTIONARY` и словарь понятий.
**Каталог товаров**: **Каталог товаров**:
`data/catalog/products.csv` — общий файл генератора и словаря Список товаров с артикулами, названиями, категориями, брендами, ценами
ClickHouse. Форма файла решена, длина — нет: строки дописываются. и уровнями спроса. Файл `data/catalog/products.csv` общий для генератора
и справочника ClickHouse; товары можно добавлять строками в этом формате.
**Уровень спроса**:
Свойство товара в каталоге: как часто открытую карточку кладут в корзину.
Три значения — магнит, обычный, залёживается. Постоянная часть мира, а не
поведение дня: он и делает конверсию «просмотр → корзина» по товарам и
брендам различимой.
**Ось модельного времени**:
Собственный календарь мира генератора. Дни считаются от фиксированного
первого дня D0; реальный календарь в модели не участвует. Между прогонами
живут только зерно и позиция на оси.
**Стартовый мир**:
Первые восемь дней оси (понедельник по понедельник), которыми стенд
наполняется при каждом `make up`. Он же эталонный мир, всегда один и тот же:
на нем принимаются следующие этапы.
_Избегать_: зерновой мир
**Паспорт мира**:
`data/world-inventory.json` — единственное, что о мире хранится в git: шапка
(зерно, версия генератора, хеш каталога), строка на каждый день событий
(число и хеш байтов) и строка на каждый отправленный слепок (хеш байтов).
Счётчик сверх этого заводится только с названным читателем. Сам мир не
хранится, а пересчитывается; паспорт отвечает на один вопрос: тот ли это мир.
_Избегать_: опись мира, манифест, мини-манифест
**Пошаговый режим**:
Базовый способ движения по оси модельного времени: «прожить следующий
день» — явное действие.
**Живой день**:
Проигрывание текущего модельного дня в реальном времени с ускорением;
включается по требованию — постоянным фоном идёт, только пока включён
выключатель `world_live`.
**Пакетный режим**:
Проигрывание готового дня пачкой, без темпа: заливка снимка при старте
стенда, пересборки и проверки.
**Даги модельного времени**:
Даги Airflow, которыми двигают ось модельного времени: шаговые даги играют
день — пачкой или в темпе, — а выключатель держит стенд живущим, пока включён.
Коротко — даги времени.
_Избегать_: пульт мира, управляющие даги
**Шаговый даг**:
Даг модельного времени, который играет день и ставит позицию на оси:
`world_next_day` — пачкой, `world_live_day` — в темпе. Запускается руками,
расписания и паузы не имеет; автоматику поверх него даёт выключатель.
_Избегать_: работник
**Граница суток**:
Единственный структурный шов модели: сессии режутся по ней, слепок заказов
снимается на ней, день проживается только целиком.
**Контракт схемы**:
Python-модуль с описателями колонок события — собственность генератора.
Из него выводятся генератор, валидация и документация формата; хранилище
строится по документации, не по модулю.
**Нормализованное имя**: **Нормализованное имя**:
Имя колонки источника, приведённое к нашему стилю (snake_case). Живёт в Имя поля источника, записанное в стиле `snake_case`: например,
контракте схемы и в описании выгрузки. Не то же, что имя атрибута в модели `ClientID` становится `client_id`. Оно указано в описании выгрузки и
данных: слой DDS складывает модель и называет атрибуты по ней. контракте схемы. Слой DDS выбирает имена по своей модели данных, поэтому
они не обязаны повторять нормализованные имена источника.
**Описание выгрузки**: **Описание выгрузки**:
Публичная документация формата события: таблица колонок, собранная из Документ, который объясняет поля и формат событий источника:
контракта схемы. По ней пишется сторона хранилища — как в бою по документации [`docs/formats/clickstream-event.md`](docs/formats/clickstream-event.md).
источника. Правится только контракт, документ пересобирается. По нему строится прием данных в хранилище. Документ собирается из
_Избегать_: описание схемы, документация контракта описаний колонок генератора: правят их, затем пересобирают документ.
_Избегать_: описание схемы, документация контракта.
**Канонический сериализатор**:
Единственная граница, где запись генератора превращается в байты. Фиксированный
порядок ключей и строк — основа побайтовой воспроизводимости.
**Проигрыватель**:
Компонент доставки готового потока дня в приёмник. Два режима: пакетный
(пачкой, без темпа) и живой день.
**Слепок**:
Полная выгрузка заказов окна изменяемости, снятая бэкендом на границе суток:
состояние заказов на этот момент, а не поток их изменений. Опоздавший заказ
может отсутствовать в ранних слепках; после первого появления ездит до конца
своего окна.
**Дата слепка**:
Дата завершившегося модельного дня, состояние которого снято на исходящей
границе суток. Одна для всех записей выгрузки; не дата заказа и не время
отправки сообщения.
**Время аудита источника**:
Время создания или последнего изменения строки заказа по часам базы источника.
В записи слепка это `created_at` и `updated_at`; бизнес-время покупки живёт в
событии `purchase` отдельно.
**Судьба заказа**:
Исход, моменты изменений, вычеркнутая позиция и опоздание заказа — всё
решается бросками при его рождении и укладывается в окно изменяемости целиком.
Слепок любого дня — чтение готовой судьбы, а не накопление состояния.
**Окно изменяемости**:
Сколько модельных дней заказ ещё может измениться и потому продолжает ездить
в слепках. Константа мира: семь дней. За окном заказ замёрз, выручка дня
перестала «дышать».
**Пакетный забор**:
Способ приёма топика, при котором данные тянет запрос по команде, а не
матвью непрерывно. Так стенд принимает заказы: у слепка есть начало и конец,
и забирать его уместно тогда, когда он собран целиком. Противоположность —
потоковый приём событий.
_Избегать_: путать с приёмником и пакетным режимом — те про сторону
генератора, про отправку; забор — про сторону хранилища, про чтение.
**Техническая проверка**: **Техническая проверка**:
Проверка, чей оракул — внутренний эталон стенда (паспорт мира), которого у Проверка по внутреннему эталону стенда, например сравнение числа событий
настоящего магазина нет. За учебный урок DQ не выдаётся. с паспортом мира. Проверка качества данных (DQ) опирается на данные
_Избегать_: DQ-проверка — та работает на наблюдаемых данных магазина. магазина и связи между ними; ей не нужен эталон генератора, которого
у настоящего магазина нет.
_Избегать_: DQ-проверка в значении проверки по паспорту мира.
### Заказы
**Слепок**:
Дневная выгрузка состояния заказов из серверной части магазина.
Она содержит заказы, оформленные за последние семь модельных дней,
с учетом изменений к концу дня. Опоздавший заказ может отсутствовать
в первых слепках.
**Дата слепка**:
Дата модельного дня, на конец которого снято состояние заказов;
поле `snapshot_date` одинаково во всех строках выгрузки. Даты оформления
заказов могут различаться; выгрузка отправляется на следующий модельный день.
**Время аудита источника**:
Время создания и последнего изменения записи заказа по часам базы
магазина: поля `created_at` и `updated_at`. Время покупки в событии
`purchase` записывает счетчик сайта, и оно может отличаться от этих полей.
**Окно изменяемости**:
Семь модельных дней, включая день оформления, в течение которых заказ
может измениться и входит в слепки. После этого его состояние окончательно.
Например, заказ от 1 июня входит в окно слепков за 1–7 июня; опоздание
сокращает число его появлений, но не сдвигает конец окна.
**Пакетный забор**:
Чтение сообщений Kafka отдельным запросом по команде. Так хранилище
принимает заказы после отправки всего слепка. События сайта оно принимает
непрерывно через материализованное представление.
_Избегать_: смешивать с пакетным режимом генератора: забор — чтение
на стороне хранилища, режим генератора — отправка.
### Модельное время и запуск стенда
**Мир**:
Модель интернет-магазина: люди, их посещения сайта, товары и заказы,
которые создает генератор.
**Ось модельного времени**:
Календарь магазина в генераторе. Первый день, D0, — 1 июня 2026 года,
понедельник (`world.ORIGIN`); следующие дни отсчитываются от него.
Реальное время — часы компьютера, на котором работает стенд. Дата запуска
не меняет даты событий: D0 остается 1 июня, даже если стенд запущен осенью.
**Граница суток**:
Конец одного модельного дня и начало следующего, полночь по часовому
поясу счетчика. Здесь заканчиваются визиты и снимается состояние заказов
для слепка. Даги времени продвигают мир только на целые дни.
**Стартовый мир**:
Данные первых восьми модельных дней, с понедельника по следующий
понедельник, которыми стенд наполняется при первом `make up`.
Команда вызывает `world_initialize`: если мир уже готов и сохранен,
повторный запуск оставляет данные и позицию времени прежними.
`make rebuild-storage` пересоздает стартовый мир. При неизменных коде,
каталоге и настройках его данные повторяются и служат эталоном проверок.
_Избегать_: зерновой мир.
**Паспорт мира**:
Файл для сверки с эталонными данными: `data/world-inventory.json`.
В нем записаны зерно генерации, версия генератора, контрольная сумма
каталога, число событий и контрольная сумма каждого начального дня,
контрольные суммы слепков и счетчики расхождений по заказам.
В git хранится паспорт; сами события и слепки пересчитываются генератором.
_Избегать_: опись мира, манифест, мини-манифест.
**Пошаговый режим**:
Продвижение модельного времени по явной команде: запустить генерацию
следующего дня или нескольких дней.
**Пакетный режим**:
Отправка событий готового модельного дня без пауз между ними.
Используется для начального наполнения, пересборок и проверок.
**Живой день**:
Постепенная отправка событий по времени их появления в модельном дне,
с ускорением: по умолчанию сутки занимают около 24 реальных минут.
Чтобы следующие дни запускались автоматически, снимают с паузы `world_live`.
**Даги модельного времени**:
Наборы связанных задач Airflow для генерации следующих дней магазина:
`world_next_day`, `world_live_day` и `world_live`. Первые два выполняют
шаг по времени, третий автоматически запускает живые дни, пока снят с паузы.
Короткое название — даги времени.
_Избегать_: пульт мира, управляющие даги.
**Шаговый даг**:
Даг, который отправляет данные следующих модельных дней и после успеха
записывает новую позицию времени. `world_next_day` отправляет их пачкой,
`world_live_day` — один день с ускорением. У них нет расписания;
их запускают вручную или из другого дага.
_Избегать_: работник.
### Устройство генератора
**Зерно генерации**:
Число, которое задает повторяемую последовательность случайных выборов
генератора. Одинаковые зерно и день дают одинаковые данные при неизменных
коде, каталоге и настройках. Эталон использует `CANONICAL_SEED` из `seeds.py`.
**Состав мира**:
Люди в модели, их идентификаторы посетителей и заранее заданные привычки:
когда они приходят, как часто возвращаются, склонны ли покупать.
У части людей две куки — это двухкуковые пары.
_Избегать_: состояние мира.
**План состава**:
Расчет того, какие посетители придут в каждый день и какие свойства у них
будут. В `plan.py` из зерна и номера дня получаются новые посетители,
их возвраты и назначенные заказы людей с двумя куками.
**Когорта дня**:
Группа людей, впервые пришедших в один день, со всеми их куками и днями
активности. Генератор рассчитывает ее по зерну и номеру первого дня.
**Приток**:
Появление новых посетителей (`ClientID`) по мере продвижения модельного
времени. Поэтому число разных посетителей за весь период растет и
не совпадает с числом посетителей за отдельный день.
**Хвост возвратов**:
Срок от первого появления человека, в пределах которого его куки могут
возвращаться на сайт. У человека с двумя куками конец срока общий.
**Предыстория**:
Люди, впервые пришедшие до D0, и план их активности. До D0 события
не создаются; возвраты этих людей дают повторных посетителей уже в первый
день генерируемых данных.
**Покупатель**:
Человек, которому генератор заранее назначил повышенную склонность
покупать. Он чаще доходит до заказа, но иногда покупают и остальные.
Из таких людей выбираются те, у кого будут две куки.
_Избегать_: покупатель в значении любого посетителя с покупкой за день;
для этого случая — «визит с заказом».
**Уровень спроса**:
Свойство товара, от которого зависит, как часто после просмотра карточки
его добавляют в корзину. В колонке `demand` каталога три значения:
`магнит`, `обычный`, `залёживается`. Они постоянны по дням и задают
различия в конверсии товаров и брендов.
**Конфигурация мира**:
Числовые настройки модели магазина в `world.py`: приток, частота возвратов,
доли покупателей, начальная дата и другие параметры. Изменение этих
настроек меняет генерируемые данные.
**Подпоток**:
Отдельная последовательность случайных выборов для части генератора,
например для торговли в заданный день. В `seeds.py` она определяется
общим зерном, днем и компонентом; для состава мира есть отдельные
последовательности. Результат не зависит от порядка расчета частей.
**День-функция**:
Расчет событий одних модельных суток по зерну и номеру дня, без генерации
предыдущих дней. Функция `day.stream(seed, day)` возвращает события
в порядке времени.
**Судьба заказа**:
Заранее рассчитанные изменения заказа: итоговый статус, время оплаты
или отмены, удаленная позиция и задержка первого появления в слепке.
Они укладываются в семидневное окно заказа. Слепок показывает состояние
на выбранный день без проигрывания предыдущих изменений.
**Контракт схемы**:
Описания колонок события в модуле генератора `schema.py`: имена,
типы и ограничения. По ним генератор создает и проверяет события,
а `schema_doc.py` собирает описание выгрузки для хранилища.
**Канонический сериализатор**:
Код в `serialize.py`, который превращает события и записи заказов в JSON
для отправки. Порядок ключей и формат значений фиксированы, поэтому
одинаковые записи дают одинаковые байты.
**Проигрыватель**:
Код в `player.py`, который доставляет готовые события дня в приемник,
например в Kafka или файл. В пакетном режиме он отправляет их без пауз,
в режиме живого дня — с ускорением по времени событий.
+98 -77
View File
@@ -5,132 +5,153 @@
> курсовая работа. Если вам нужна помощь, обратитесь к автору материалов — > курсовая работа. Если вам нужна помощь, обратитесь к автору материалов —
> ментору [@dementev_dev](https://t.me/dementev_dev). > ментору [@dementev_dev](https://t.me/dementev_dev).
Этот стенд — для изучения и экспериментов с кликстримом и ClickHouse. Здесь Здесь вы будете работать с хранилищем данных небольшого интернет-магазина:
можно проследить весь путь данных: от кликстрима и заказов до витрин, проверок загружать и обрабатывать данные в ClickHouse, строить витрины, проверять
и графиков. Это преемник учебного стенда качество данных и создавать графики. Стенд рассчитан на тех, кто уже изучил
[clickstream-ch-kafka-superset-demo](https://git.dementev.space/ddmitry/clickstream-ch-kafka-superset-demo). основы ClickHouse и хочет попрактиковаться.
В стенде подняты кластер ClickHouse из двух шардов, отдельный keeper, Kafka, Генератор создает заказы и кликстрим — записи о действиях посетителей
Airflow, Superset, Prometheus и Grafana; события кликстрима и заказы бэкенда магазина. Данные поступают через Kafka в ClickHouse и проходят четыре слоя
приезжают из генератора и идут по слоям STG → ODS → DDS → DM. `make up` обработки: STG, ODS, DDS и DM. В кластере ClickHouse два шарда и отдельный
наполняет стенд стартовым миром — первой неделей модельного времени. Чтобы ClickHouse Keeper. Airflow управляет загрузкой и обработкой, Superset
разобраться в устройстве, начните с [путеводителя](docs/handbook/README.md), а показывает отчеты, а Prometheus и Grafana помогают следить за работой стенда.
учиться — с [уроков](docs/labs/README.md).
При первом запуске стенд загрузит данные за неделю работы магазина.
После запуска начните с [путеводителя](docs/handbook/README.md): он объясняет
устройство хранилища. Затем переходите к [учебным заданиям](docs/labs/README.md).
## Быстрый старт ## Быстрый старт
Нужны Docker с Compose и около 8 ГБ памяти, доступной Docker. Это не объём Для запуска нужны Docker с Compose, `make`, `curl` и `jq`.
ноутбука, а то, что отдано самому Docker: в Docker Desktop и WSL2 он живёт Выделите Docker около 8 ГБ оперативной памяти. В Docker Desktop и WSL2
внутри виртуальной машины и получает лишь часть памяти хозяина. Сколько выдано проверьте лимит памяти виртуальной машины: он может быть меньше, чем общий
сейчас, в байтах, покажет `docker info --format '{{.MemTotal}}'`. В WSL2 это объем памяти компьютера. Команда `docker info --format '{{.MemTotal}}'`
поднимается параметром `memory` в файле `.wslconfig` домашнего каталога покажет доступный Docker объем в байтах. В WSL2 лимит задается параметром
пользователя Windows; после правки нужен `wsl --shutdown`. Если своей машины не `memory` в файле `.wslconfig` в домашнем каталоге пользователя Windows.
хватает, стенд одинаково хорошо живёт на недорогом VPS. После изменения выполните `wsl --shutdown`. Если ресурсов компьютера
недостаточно, стенд можно запустить на VPS.
`make up` нужны `curl` и `jq`: первым он ходит в Airflow, вторым читает По умолчанию должны быть свободны порты
паспорт мира, по которому ждет заливки. По умолчанию должны быть свободны `23000`, `28080`, `28088`, `28123`, `28124`, `29000`, `29001`, `29090`
порты `23000`, `28080`, `28088`, `28123`, `28124`, `29000`, `29001`, `29090` и `29092`. Для работы с командами `make` в каталоге `generator/` также
и `29092`. Всем целям генератора в `generator/` нужен `uv`. понадобится `uv`.
Сначала скопируйте настройки стенда: В каталоге репозитория скопируйте образец настроек:
```bash ```bash
cp .env.example .env cp .env.example .env
```
В `.env` задаются имя экземпляра стенда, внешние порты, учебные логины,
пароли и ключи. Проверьте `DOCKER_GID`: это числовой идентификатор группы
`docker` на вашей машине. Узнать его можно командой `getent group docker`.
Если число отличается от образца, замените значение в `.env`. От этой
настройки зависит запуск генератора из Airflow.
Запустите стенд:
```bash
make up make up
``` ```
`make up` сам ждет заливки стартового мира; что он делает по шагам и какие Команда запустит службы и дождется загрузки начальных данных. Порядок запуска
проверки есть у стенда, описано в [карте и проверки результата описаны в [справочнике проверок](docs/architecture/testing.md).
проверок](docs/architecture/testing.md). Если обязательной настройки в `.env` не хватает, Compose сообщит об ошибке.
Внутренние адреса служб, имена топиков и версии образов заданы в `compose.yaml`
и Dockerfile.
`.env.example` — образец обязательной локальной настройки. В нём живут имя Логины и пароли Postgres и Grafana сохраняются при первом запуске в томах
экземпляра, внешние порты, учебные учётные данные и ключи. Одно значение образца Docker. Простая правка `.env` их не изменит. Чтобы задать новые значения
верно не везде — `DOCKER_GID`: у группы `docker` на каждой машине свой номер, и через `.env`, потребуется `make clean` и повторный `make up`. При этом
свой подскажет `getent group docker`. Пока он не сойдётся, стенд работает весь, будут удалены все локальные данные стенда.
кроме [дагов модельного
времени](docs/architecture/world.md#даги-модельного-времени). `compose.yaml` не
дублирует их умолчаниями: если значения нет в `.env`, Compose сразу предложит
скопировать образец. Внутренние адреса, имена топиков и версии образов описывают
сам стенд, поэтому записаны литералами в `compose.yaml` и Dockerfile.
Учётные данные Postgres и Grafana применяются при создании их томов.
После первого запуска меняйте их только вместе с `make clean`: команда удалит
все локальные данные стенда, а следующий `make up` создаст их с новыми
значениями.
## Адреса и пароли ## Адреса и пароли
С настройками из `.env.example` порты доступны только с локальной машины: С настройками из `.env.example` порты доступны только с локальной машины:
- нода 1 — `http://127.0.0.1:28123`, нативный порт `29000`; - узел ClickHouse 1 — `http://127.0.0.1:28123`, нативный порт `29000`;
- нода 2 — `http://127.0.0.1:28124`, нативный порт `29001`; - узел ClickHouse 2 — `http://127.0.0.1:28124`, нативный порт `29001`;
- Kafka — `127.0.0.1:29092`; - Kafka — `127.0.0.1:29092`;
- Airflow — `http://127.0.0.1:28080`, пользователь `admin`, пароль `airflow`; - Airflow — `http://127.0.0.1:28080`, пользователь `admin`, пароль `airflow`;
- Superset — `http://127.0.0.1:28088`, пользователь `admin`, пароль `superset`; - Superset — `http://127.0.0.1:28088`, пользователь `admin`, пароль `superset`;
- Prometheus — `http://127.0.0.1:29090`; - Prometheus — `http://127.0.0.1:29090`;
- Grafana — `http://127.0.0.1:23000`, пользователь `admin`, пароль `admin`. - Grafana — `http://127.0.0.1:23000`, пользователь `admin`, пароль `admin`.
Службы, пользователи и роли, применение настроек и результаты сверки Подробнее о службах, правах пользователей и изменении настроек —
конфигурации описаны в [справочнике стенда](docs/architecture/stand.md). в [справочнике стенда](docs/architecture/stand.md).
## Сдвинуть мир вперед ## Добавить данные за следующие дни
В Airflow запустите даг `world_next_day`: по умолчанию он сыграет один день, а Время в учебном магазине идет отдельно от календарного. В документации оно
в форме запуска можно задать другое число дней. Снимите с паузы `world_live`, называется модельным временем, а сам магазин с его посетителями и заказами —
и мир поедет сам в темпе модельного времени. Подробности — в [справочнике миром.
мира](docs/architecture/world.md#даги-модельного-времени).
## Вернуть мир к началу В Airflow запустите `world_next_day`. Это даг — набор связанных задач,
которые Airflow выполняет в заданном порядке. Он создаст и загрузит данные
за следующий день работы магазина. В форме запуска можно задать число дней.
`make rebuild-storage` запустит в Airflow явную пересборку: удалит прикладные Чтобы новые данные поступали непрерывно, снимите с паузы даг `world_live`.
базы ClickHouse, топики и позиции потребителей Kafka, затем создаст схему Он будет запускать генерацию день за днем; один модельный день занимает
и данные заново. Службы и их тома останутся на месте. Перед пересборкой около 24 минут. Подробности — в [справочнике генератора и модельного
поставьте `world_live` на паузу и дайте уже начатому шаговому дагу закончить времени](docs/architecture/world.md#даги-модельного-времени).
день.
## Выключить или сбросить целиком ## Восстановить начальные данные
`make down` остановит контейнеры без удаления данных. `make clean` удалит весь Если хотите начать заново, поставьте `world_live` на паузу и дождитесь
стенд вместе с именованными томами и для обычной работы не нужен. завершения уже запущенного дага генерации. Затем выполните
`make rebuild-storage`.
Команда удалит прикладные базы ClickHouse, топики и позиции потребителей
Kafka, затем заново создаст схему хранилища и загрузит начальные данные.
Службы и их тома останутся на месте.
## Остановить или удалить стенд
`make down` остановит стенд и сохранит данные. Для повторного запуска
выполните `make up`.
`make clean` удалит контейнеры и все данные в томах Docker. Для обычного
завершения работы достаточно `make down`.
## Подключиться из DBeaver ## Подключиться из DBeaver
Драйвер ClickHouse в DBeaver подключается по JDBC через HTTP-порт. Укажите хост Драйвер ClickHouse в DBeaver подключается по JDBC через HTTP-порт. Укажите хост
`127.0.0.1`, порт `28123`, пользователя `analyst` и пароль из переменной `127.0.0.1`, порт `28123`, пользователя `analyst` и пароль из переменной
`CLICKHOUSE_ANALYST_PASSWORD` в `.env`; в образце это `analyst-26-reader`. `CLICKHOUSE_ANALYST_PASSWORD` в `.env`; в образце это `analyst-26-reader`.
Ко второй ноде — порт `28124`. Для подключения ко второму узлу укажите порт `28124`.
Пользователь `analyst` читает все слои и справочники. Нативные порты `29000` и Пользователь `analyst` может читать данные во всех слоях и справочниках.
`29001` нужны `clickhouse-client`, но не DBeaver. Остальные пользователи и роли Для создания учебных витрин используйте подключение, описанное
описаны в [справочнике в [инструкции к заданиям](docs/labs/README.md#как-применить-свой-ddl).
стенда](docs/architecture/stand.md#пользователи-и-роли-clickhouse). Нативные порты `29000` и `29001` нужны `clickhouse-client`, но не DBeaver.
Остальные пользователи и роли описаны в
[справочнике стенда](docs/architecture/stand.md#пользователи-и-роли-clickhouse).
## Документация ## Документация
- [docs/handbook/](docs/handbook/) — путеводитель по стенду и порядок чтения. - [docs/handbook/](docs/handbook/) — путеводитель по стенду и порядок чтения.
- [docs/labs/](docs/labs/) — учебные задания с порядком прохождения и - [docs/labs/](docs/labs/) — учебные задания с порядком прохождения и
самопроверкой. самопроверкой.
- [docs/specs/](docs/specs/) — спеки: источник истины о задуманном. - [docs/specs/](docs/specs/) — требования к стенду и программа обучения.
- [docs/adr/](docs/adr/) — принятые решения с доводами и отвергнутыми - [docs/adr/](docs/adr/) — решения об устройстве стенда: что выбрали,
вариантами: почему сделано так, а не иначе. какие варианты отклонили и почему.
- [docs/architecture/](docs/architecture/) — рабочие справочники по зонам - [docs/architecture/](docs/architecture/) — технические справочники:
ответственности: - [хранилище](docs/architecture/storage.md) — правила именования,
- [хранилище](docs/architecture/storage.md) — конвенции имён, раскладка по распределение данных по шардам, прием событий и список таблиц.
шардам, приём событий, карта таблиц.
- [конвейер](docs/architecture/etl.md) — движение данных между слоями и - [конвейер](docs/architecture/etl.md) — движение данных между слоями и
работа дагов. работа дагов.
- [мир и модельное время](docs/architecture/world.md) — начальные данные, - [мир и модельное время](docs/architecture/world.md) — начальные данные,
ход дней и управление генератором. создание данных за следующие дни и управление генератором.
- [стенд](docs/architecture/stand.md) — службы, доступ и настройка. - [стенд](docs/architecture/stand.md) — службы, доступ и настройка.
- [проверки](docs/architecture/testing.md) — что утверждает каждая цель - [проверки](docs/architecture/testing.md) — команды `make` для проверки
`make` и куда класть новую проверку. стенда и правила добавления новых проверок.
- [мониторинг](docs/architecture/monitoring/README.md) — сбор метрик и - [мониторинг](docs/architecture/monitoring/README.md) — сбор метрик и
устройство дашборда. устройство дашборда.
- [Superset](docs/architecture/superset.md) — подключения, наборы данных и - [Superset](docs/architecture/superset.md) — подключения, наборы данных и
граница автоматической настройки. автоматическая настройка при запуске.
- [docs/research/](docs/research/) — исследования; сейчас это формат - [docs/research/](docs/research/) — исследования; сейчас это формат
кликстрима Яндекса, по которому строится модель события. кликстрима Яндекса, по которому строится модель события.
- [docs/formats/](docs/formats/) — описания форматов источников: по ним - [docs/formats/](docs/formats/) — форматы исходных данных, необходимые
пишется сторона хранилища. Собираются из кода командой `make docs` в для их загрузки и обработки. Эти документы собираются из кода командой
`generator/`, руками не правятся. `make docs` в `generator/`; вручную их менять не нужно.
- [AGENTS.md](AGENTS.md) — контракт работы в репозитории. - [AGENTS.md](AGENTS.md) — правила работы в репозитории для ИИ-агентов.
+24 -32
View File
@@ -1,18 +1,17 @@
"""Трансформации слоя DDS: сборка моделей. """Трансформации DDS: заказы, сессии и карта идентичностей.
Заказ и сессия собираются заменой дневных партиций. Карта идентичностей Запросы рассчитывают заказы и сессии за выбранные дни и записывают результат
пересчитывается целиком после заказа, потому что читает его текущую модель. в промежуточные таблицы. Затем готовые дневные разделы этих таблиц заменяют
разделы основных таблиц. Пока идет расчет, читатели видят прежние данные.
Расписания у дага нет и параметров тоже: объём каждого прогона он выводит сам Карта идентичностей пересчитывается целиком после заказов. Новые строки
из состояния слоёв и позиции мира, поэтому один и тот же запуск одинаково добавляются с меткой времени сборки; представление с FINAL выбирает последнюю
верен из триггера дневного конвейера, с кнопки менти и после падения версию каждой строки.
(docs/architecture/etl.md, «Объём прогона»).
Заказ и сессия гасят повтор заменой партиции целиком: день собирается в Даг запускается без расписания и параметров. Даты он выбирает по состоянию
таблицу-двойник и подменяет собой партицию цели. Карта пишет полный набор пар таблиц и позиции мира. Новый запуск заново определяет диапазон: наличие даты
новой версией, а точное чтение даёт FINAL. Замена партиции разобрана в не подтверждает полноту дня и не гарантирует повтор его сборки после сбоя.
sql/dds/order_replace.sql; точное чтение карты описано в Подробности — docs/architecture/etl.md, «Объём прогона» и «Идемпотентность».
sql/ddl/50-dds-views.sql.
""" """
from __future__ import annotations from __future__ import annotations
@@ -257,16 +256,11 @@ def _days(first_day: datetime.date, last_day: datetime.date) -> list[str]:
@task @task
def replacements(scope: dict[str, object]) -> list[dict[str, object]]: def replacements(scope: dict[str, object]) -> list[dict[str, object]]:
"""Разложить дни в аргументы размноженной задачи замены. """Подготовить параметры отдельной задачи replace для каждого дня.
Шаг существует ради ограничения Airflow: размножать задачу можно expand_kwargs получает список словарей и создает по одному экземпляру
только по целому возврату другой задачи — ни по ключу словаря, ни по задачи на каждый словарь. Дата подставляется в SQL шаблоном Jinja:
карте поверх него. Отсюда отдельный возврат под один список. параметр ClickHouse в ALTER ... PARTITION здесь не поддерживается.
День едет в запрос подстановкой Jinja, а не параметром ClickHouse:
в ALTER … PARTITION сервер параметр не подставляет — замерено,
отвечает синтаксической ошибкой. Значение при этом не внешнее, его
вернул сам ClickHouse запросом объёма.
""" """
return [{"params": {"day": day}} for day in scope["days"]] return [{"params": {"day": day}} for day in scope["days"]]
@@ -276,8 +270,8 @@ def replacements(scope: dict[str, object]) -> list[dict[str, object]]:
schedule=None, schedule=None,
start_date=START_DATE, start_date=START_DATE,
is_paused_upon_creation=False, is_paused_upon_creation=False,
# Два прогона разом собирали бы один и тот же двойник: второй затёр бы # Одновременные запуски используют одни промежуточные таблицы.
# чужую сборку между вставкой и заменой партиций. # Второй мог бы очистить результат первого до замены партиций.
max_active_runs=1, max_active_runs=1,
template_searchpath=str(SQL_ROOT), template_searchpath=str(SQL_ROOT),
tags=["dds"], tags=["dds"],
@@ -298,7 +292,7 @@ def dds_transform():
@task_group(group_id="order") @task_group(group_id="order")
def order(): def order():
"""Заказ: дышащее окно плюс дни, которых в слое ещё нет.""" """Заказ: изменяемые дни последнего слепка и пропущенные дни."""
@task @task
def scope() -> dict[str, object]: def scope() -> dict[str, object]:
@@ -306,8 +300,7 @@ def dds_transform():
query = (SQL_ROOT / "dds" / "order_scope.sql").read_text(encoding="utf-8") query = (SQL_ROOT / "dds" / "order_scope.sql").read_text(encoding="utf-8")
hook = ClickHouseHook(clickhouse_conn_id=CLICKHOUSE_CONNECTION) hook = ClickHouseHook(clickhouse_conn_id=CLICKHOUSE_CONNECTION)
row = hook.get_first(query) row = hook.get_first(query)
# Пустой ответ означает пустой источник: собирать нечего, и # Без заказов ODS расчет невозможен: задача сообщает об ошибке.
# зеленеть на этом нельзя — прогон встаёт с внятной причиной.
if row is None: if row is None:
raise AirflowException( raise AirflowException(
"в ods.order нет ни одного заказа: собирать нечего; " "в ods.order нет ни одного заказа: собирать нечего; "
@@ -342,9 +335,9 @@ def dds_transform():
do_xcom_push=False, do_xcom_push=False,
) )
# Замена идёт по партиции за раз — другой формы у операции нет. День # На каждый день создается отдельная задача замены партиции.
# становится отдельной задачей: в интерфейсе видно, сколько дней несёт # Неудачную замену можно повторить, пока промежуточная таблица
# прогон, а повтор после отказа стоит одного дня, а не всей пересборки. # сохраняет рассчитанные строки этого запуска.
replace = SQLExecuteQueryOperator.partial( replace = SQLExecuteQueryOperator.partial(
task_id="replace", task_id="replace",
conn_id=CLICKHOUSE_CONNECTION, conn_id=CLICKHOUSE_CONNECTION,
@@ -370,9 +363,8 @@ def dds_transform():
"проверьте, дошёл ли поток до хранилища" "проверьте, дошёл ли поток до хранилища"
) )
first_day, last_day = row first_day, last_day = row
# Пропуск, а не отказ: слой полон по последний прожитый день, а # Для каждого прожитого дня уже есть строки сессий.
# сегодняшний ещё идёт. Это обычное состояние повторного прогона, # Новых дней для расчета нет: группа задач будет пропущена.
# и в интерфейсе оно видно пропущенной группой.
if first_day is None: if first_day is None:
raise AirflowSkipException( raise AirflowSkipException(
f"сессии собраны по {last_day} включительно, " f"сессии собраны по {last_day} включительно, "
+12 -8
View File
@@ -1,9 +1,12 @@
"""Трансформации слоя DM: сборка стендовых витрин. """Трансформации слоя DM: сборка стендовых витрин.
Выручка наследует ритм заказов. Сверка соединяет окно заказов с прожитым Даты для расчета выручки берутся из последнего слепка заказов и списка
хвостом событий. Обе заменяют дневные партиции. Трафик наследует пропущенных дней. Сверка покупок с заказами учитывает также прожитые дни
ретроспективность карты идентичностей и пересчитывает всю историю новой событий и дни, где заказ еще не найден. Запросы записывают результаты
версией. Объем каждого прогона выводится из состояния слоев. в промежуточные таблицы, затем заменяют дневные разделы основных таблиц.
Трафик пересчитывается за всю историю: новая связь посетителя с пользователем
магазина меняет число известных пользователей и за прошлые дни.
""" """
from __future__ import annotations from __future__ import annotations
@@ -62,10 +65,11 @@ def _days(first_day: datetime.date, last_day: datetime.date) -> list[str]:
@task @task
def replacements(scope: dict[str, object]) -> list[dict[str, object]]: def replacements(scope: dict[str, object]) -> list[dict[str, object]]:
"""Разложить дни в аргументы размноженной задачи замены. """Подготовить параметры отдельной задачи replace для каждого дня.
Отдельный словарь нужен каждому экземпляру ``expand_kwargs``; тот же expand_kwargs получает список словарей и создает по одному экземпляру
прием разобран в ``dds_transform.py`` на замене партиций заказов. задачи на каждый словарь. Дата подставляется в SQL шаблоном Jinja:
параметр ClickHouse в ALTER ... PARTITION здесь не поддерживается.
""" """
return [{"params": {"day": day}} for day in scope["days"]] return [{"params": {"day": day}} for day in scope["days"]]
@@ -141,7 +145,7 @@ def dm_transform():
@task_group(group_id="purchase_vs_orders") @task_group(group_id="purchase_vs_orders")
def purchase_vs_orders(): def purchase_vs_orders():
"""Сверка: окно заказов, прожитый хвост и предварительные дни.""" """Сверка: изменяемые заказы, прожитые покупки и дни ожидания заказа."""
@task @task
def scope() -> dict[str, object]: def scope() -> dict[str, object]:
+91 -84
View File
@@ -1,111 +1,118 @@
# Модель сессии в DDS # Модель сессии в DDS
Учебный результат: менти строит расчётную сущность, которой нет в потоке, — Сессия объединяет события одного визита. На стенде вы учитесь определять
режет события на сессии по правилу визита, чеканит идентификатор и сверяет ее границы по событиям, вычислять устойчивый идентификатор и сверять
свою нарезку с эталоном. Стенд здесь играет роль сессионизатора: в бою её результат с эталоном. Поэтому сборка читает `dds.event_v`, где нет готового
играет Метрика, которая режет визиты у себя и с октября 2025 отдаёт готовый `VisitID`. Этот идентификатор источника остается в ODS для проверки.
`VisitID` прямо в хитах. Поэтому стенд считает сессии сам, а `VisitID`
генератора использует только как эталон самопроверки — иначе сборка сессий
выродилась бы в `GROUP BY` по чужому полю и центральный урок
слоя пропал бы. Без этого абзаца действия стенда выглядят странно: «зачем
считать то, что уже дано колонкой».
## Зерно ## Зерно
Одна строка — один визит ([CONTEXT.md](../../../CONTEXT.md)): подряд идущие Зерно описывает, чему соответствует одна строка: здесь это один
события одной куки без пауз длиннее 30 минут, не пересекающие границу суток. [визит](../../../CONTEXT.md#люди-и-события). В него входят подряд идущие
В нарезку входят все три типа событий — торговые садятся на свои страницы события одного `client_id` без пауз длиннее 30 минут и без перехода через
и держат сессию наравне с просмотрами. События внутри сессии модельную полночь. Пауза ровно в 30 минут новую сессию не открывает.
упорядочиваются по метке времени события (`UTCEventTime` — опорная колонка Учитываются все три типа событий: `pageview`, `add_to_cart`, `purchase`.
приёма).
Граница суток проходит по модельному дню счётчика, и он у события уже есть: День сессии `session_date` берется из `event_date` событий DDS — это
`EventDate` — свойство содержимого. Раз сессия сутки не пересекает, у всех `EventDate` источника, дата по часовому поясу счетчика. У всех событий
её событий `EventDate` один; он и становится **днём сессии** — колонкой одной сессии она одинакова. Пересчитывать ее из времени UTC не нужно.
партиции, без пересчёта.
SQL рассчитывает границы визитов двумя оконными функциями: первая отмечает
начало нового визита, вторая по этим отметкам присваивает событиям номер
визита. Обе используют порядок `(event_time, event_id)`: при равном времени
события идут по идентификатору. Иначе два расчета могли бы по-разному
упорядочить события и отнести отметку начала к разным строкам.
## Идентификатор ## Идентификатор
Идентификатор сессии в природе не существует: сессия — расчётная сущность, `session_id` вычисляется как `cityHash64(client_id, min(event_time))`.
и id ей чеканит тот, кто сессионизирует. `VisitID` — такой же суррогат, При повторной сборке тех же событий посетитель и начало визита остаются
отчеканенный генератором в роли Метрики; наш — `session_id`, прежними, поэтому идентификатор не меняется. Счетчик или случайный UUID
детерминированный хеш от пары «кука + начало сессии» (`cityHash64`). выдали бы другие значения.
Детерминированность обязательна: партиция дня пересобирается заменой, и
повторный прогон обязан отчеканить те же id — счётчик или UUID это правило
ломают.
`session_id` — не ключ сортировки и не ключ шардирования, а колонка-ручка: По `session_id` можно сопоставить сессии до и после пересборки и назвать
в соединениях он не участвует — события его не несут, мост к заказам живёт конкретную строку при разборе расхождения. Он не используется для сортировки
на событиях. Читатели ручки — лаба сессий (сопоставить свою нарезку со или шардирования. У событий этого поля нет; связь с заказами строится
стендовой и назвать расхождение одним числом) и разбор DQ-расхождений. по событиям покупки.
## Состав строки ## Состав строки
Боевой образец — таблица визитов Метрики, парная к нашему формату хита; по - `client_id`, `session_id`, `session_date` — посетитель, сессия и ее день.
ней состав и собран: - `started_at`, `finished_at`, `duration_seconds` — время первого и последнего
события и разница между ними в секундах. Времена хранятся в часовом поясе
счетчика.
- `events_total`, `pageviews`, `cart_adds`, `purchases` — число всех событий
и счетчики трех типов.
- `entry_url`, `exit_url` — страницы первого и последнего события.
- `utm_*` — метки первого события: по ним визит относят к рекламной кампании.
- `device_category`, `region_city` — устройство и город посетителя.
На стенде они постоянны для одной куки.
- `_load_id`, `_load_ts` — идентификатор запуска и время сборки строки.
- **ключи и времена**: кука, `session_id`, начало и конец сессии, В готовом SQL страницы выбираются через `argMin` и `argMax`, а UTM,
длительность, день сессии. Времена — в поясе счётчика по правилу слоя; устройство и город — через `argMin`, только по `event_time`. При одинаковом
- **счётчики**: событий всего, просмотров, корзин, покупок; времени выбор между событиями не определен. Полный порядок окон нарезки
- **страницы**: входная и выходная — как `StartURL`/`EndURL` визита; полного сам по себе эту неоднозначность не устраняет. Это ограничение учитывается
списка страниц нет и у Метрики, топы считаются по событиям; в [задании ступени 4](../../labs/4-1-session-pipeline.md).
- **атрибуция**: UTM-метки первого события сессии — так визит атрибутирует
Метрика. В v1 витрина брала последнее касание — заменено осознанно;
- **паспорт куки**: категория устройства (расшифрованная, язык бизнес-модели)
и город — у куки они постоянны, берутся с первого события;
- **служебные**: `_load_id` и `_load_ts` собравшего запуска — правило слоя
([модель заказа](order.md), «Служебные колонки»).
Осознанные расхождения с образцом: **денег в сессии нет** — визит Метрики В сессии нет суммы выручки: ее считают по заказам магазина. Клиентская
несёт клиентскую выручку, у нас деньги считает только бэкенд (спека, раздел сумма покупки используется в сверке с заказами и в учебной UTM-витрине.
7), а клиентская сумма живёт в сверке; **целей нет** — их место заняли
счётчики торговых событий.
## Движок и наполнение ## Движок и наполнение
`dds.session_rep` — `ReplicatedMergeTree` без колонки версии, по образцу `dds.session_rep` хранит готовые сессии в `ReplicatedMergeTree` без колонки
заказа: партиция — день сессии, ключ сортировки — кука и начало сессии. версии. При чтении не нужно заново выполнять оконный расчет. Партиция —
`dds.session_dist` шардирует по `cityHash64(client_id)`, тем же выражением, раздел таблицы за один `session_date`; ключ сортировки — посетитель
что события, — это ко-локация с `dds.identity_map_rep` ([дока и начало сессии. Читайте результат через `dds.session_v`: представление
хранилища](../storage.md), «Раскладка по шардам»). над `dds.session_dist` явно перечисляет деловые и служебные колонки.
Совпадение ключа с событиями не случайно: все события куки лежат на одном
шарде, поэтому сессионизация локальна по природе — капкан смены ключа при
вставке, который дока хранилища обещает сборкам DDS, у сессий не стреляет.
Сборка — дневной батч: хранилище выше ODS по смыслу батчевое, раз второй Источник сборки — `dds.event_v`. События, сессии и карта идентичностей
источник приходит раз в сутки. Собирается день, который мир уже прожил, и распределены по одному ключу `cityHash64(client_id)`: данные посетителя
собирается один раз и целиком; какой день прожит, слой спрашивает у позиции находятся на одном шарде. Роли объектов и правило записи через `_dist`
мира ([дневной конвейер](../etl.md), «Объём прогона»). Идемпотентность держит описаны в [справочнике хранилища](../storage.md#раскладка-по-шардам).
замена партиции. В отличие от заказов, где батч пересобирает до K «дышащих»
дней, у сессии дышащего окна нет: прожитый день самодостаточен (правило
спеки, раздел 6). Второй раз он соберётся только
вынужденно — когда упавшая замена оставила позади дыру и хвост от неё накрыл
соседние дни. Дней в одном прогоне при этом бывает несколько — столько,
сколько мир успел прожить без слоя. Внутри живого дня
модель сессии про сегодня честно молчит — «сегодня» смотрят через
`dds.event_v`; сессия и по смыслу не финальна, пока день не прожит.
Публичная поверхность — `dds.session_v`: обычное представление с явным Три SQL-файла выполняются последовательно в группе `session` дага
списком деловых и собственных служебных колонок над `dds.session_dist`. `dds_transform`:
Роли объектов и доступ следуют общей [конвенции](../storage.md), «Имена
объектов».
Источник сборки — `dds.event_v` ([контракт события](event.md)): сессии 1. `session_scope.sql` находит первую дату событий, за которую еще нет
нужны от него ключи нарезки, тип события, страница, UTM и паспорт куки. сессий. Последний день для расчета — первый день событий ODS
Механика запуска — где собирается партиция и как подменяется на кластере — плюс `world_position` минус один; `world_position` — номер следующего
не входит: это устройство [дневного конвейера](../etl.md). дня для генерации. Так в расчет попадают только полностью
прожитые дни. При пустом ODS запрос не возвращает строк и задача падает;
если пропущенных прожитых дней нет, `first_day` равен `NULL` и группа
пропускается.
2. `session_rebuild.sql` очищает промежуточную таблицу
`dds.session_stage_rep`. Затем рассчитывает сессии выбранных дней
и записывает строки через `dds.session_stage_dist`. Пока идет расчет,
в основной таблице остаются прежние данные. Вставка ждет доставки
строк на шарды благодаря `distributed_foreground_insert = 1`.
3. `session_replace.sql` заменяет в `dds.session_rep` раздел за каждый
выбранный день готовым разделом `dds.session_stage_rep`. Для каждого
дня Airflow создает отдельную задачу `replace`.
Когда все события завершенного дня приняты в ODS, их набор на стенде
больше не меняется. Поэтому готовые сессии обычно не пересчитываются. Если пропущен ранний
день, диапазон включает и все последующие прожитые дни, даже уже собранные.
Если за день сохранилась хотя бы одна сессия, поиск пропущенных дней
не обнаружит потерю остальных. Условия повторного запуска и замены
разделов описаны в [справочнике конвейера](../etl.md#идемпотентность).
Сессий еще идущего дня в модели нет; его события доступны через
`dds.event_v`. Перед выбором дней даг ждет завершения доставки событий
в ODS — см. [готовность источника](../etl.md#готовность-источника).
## Сверка с эталоном ## Сверка с эталоном
Счётная DQ-проверка с зерном «кука × день»: число сессий нашей нарезки Проверка `sessions_vs_reference` в `dq_check` сравнивает для каждого
равно `uniqExact(VisitID)` по тем же событиям. Счёт по Distributed ловит и посетителя и дня число строк `dds.session_v` с `uniqExact(VisitID)`
склеенную, и разрезанную сессию — мимо него проходит лишь взаимная из `ods.event_v`. Эталон читается из ODS, потому что `dds.event_v`
компенсация ошибок внутри одной куки за день; эталон в модель не тащим — скрывает `VisitID`.
строка сессии остаётся нашей, а не генераторной. Поштучное сопоставление
границ — материал лабы, где менти разбирает найденное расхождение руками. Дом проверки — Нулевое число расхождений подтверждает только число сессий каждого
контур DQ этапа 4. Сверке нужен `VisitID`, а сборке сессий — нет; контракт посетителя за день. Например, ошибочное объединение двух визитов
`dds.event_v` его скрывает, и за эталоном проверка идёт в `ods.event_v` и разделение другого на два сохранят их общее число. Поэтому в заданиях
([контракт события](event.md)). вы дополнительно сопоставляете границы сессий по исходным событиям и проверяете страницы,
UTM, устройство и город.
## Читатели ## Читатели
+168 -141
View File
@@ -1,179 +1,202 @@
# Слой DM: витрины # Слой DM: витрины
Как модель DDS превращается в витрины для потребителя BI: какую форму Витрины готовят данные DDS для отчетов и дашбордов. Здесь описаны форма
принимает витрина, кто какие витрины строит и как слой проверяется. Состав хранения, правила обновления и проверки. Состав колонок задан в
колонок каждой витрины задаёт раздел 7 мастер-спеки; здесь — форма, [спецификации](../specs/2026-07-30-stand-v2-realism.md#витрины-dm)
наполнение и контроль. и [учебных заданиях](../labs/README.md).
## Таблица или представление ## Таблица или представление
Дневной агрегат и сверка по заказам меняют зерно источника. На этом стенде такие Зерно — то, чему соответствует одна строка. Например, в заказах это заказ,
результаты хранятся в `dm.<имя>_rep` / `_dist` и читаются через публичное а в дневной выручке — день и категория товара. На стенде результат
представление `dm.<имя>_v` с явным списком колонок. Имена и устройство объектов с новым зерном хранится в таблице: отчет читает готовые итоги без
следуют общим [конвенциям хранилища](storage.md). Таблицы наполняет повторного расчета. Таблицу наполняет `dm_transform`.
`dm_transform`; ритм и механика пересборки описаны ниже. В задании этот путь
приходится пройти целиком: рассчитать агрегат, загрузить его и включить
обновление в конвейер.
Переименование колонок и обогащение строки зерно не меняют. Такие витрины на У такой витрины есть локальная `_rep`, распределенная `_dist` и публичное
стенде остаются обычными представлениями и не хранят вторую копию событий. представление `_v` с явным списком колонок. Для обновления по дням
нужна промежуточная таблица `_stage_rep`, в которую пишут через `_stage_dist`.
В нее записывают рассчитанные строки, пока основная таблица хранит прежние.
Затем дневные разделы основной таблицы заменяют готовыми разделами
промежуточной. Такие разделы называются партициями. Подробности — в
[справочнике хранилища](storage.md#имена-объектов).
На объёмах стенда все витрины могли бы остаться представлениями: условия Если запрос только переименовывает или дополняет поля и сохраняет одну
проходят сквозь их цепочку строку на событие, достаточно обычного представления. Оно выполняет
запрос при чтении, не хранит вторую копию событий и не требует пересборки.
На небольших данных стенда и агрегаты могли бы вычисляться при чтении
([исследование](../research/2026-08-22-view-predicate-pushdown.md)). ([исследование](../research/2026-08-22-view-predicate-pushdown.md)).
Мы все равно оставили материализованные агрегаты ради практики: без них в Готовые таблицы нужны в программе для практики загрузки, обновления
программе не было бы загрузки и пересборки витрин. Представления дают другой и включения витрин в конвейер.
случай: преобразование строк на прежнем зерне.
## Что готово в стенде, а что остаётся заданием ## Что готово в стенде, а что остаётся заданием
Стенд привозит готовыми витрины трёх сюжетов дашборда этапа 8 и Готовы три витрины дашборда и сводка качества:
инфраструктуру контроля:
- `revenue_daily` — выручка по дням и категориям; образец материализованной - `revenue_daily` — одна строка на день заказа и категорию товара;
витрины, на него ссылаются учебные задания; образец дневной пересборки для учебных заданий.
- `purchase_vs_orders` — сверка A+C с классами расхождений; зерно — заказ, - `purchase_vs_orders` — одна строка на заказ: сопоставление покупки
шардирование сохраняет ко-локацию заказов по `cityHash64(order_id)`; из кликстрима с заказом магазина и класс расхождения.
- `daily_traffic` — пара «посетители» / «известные пользователи», обогащение - `daily_traffic` — одна строка на день: число посетителей и известных
[картой идентичностей](dds/identity.md); пользователей с учетом [карты идентичностей](dds/identity.md).
- `dq_summary` — сводка проверок качества (ниже). - `dq_summary` — одна строка на день данных и проверку качества.
Остальные витрины спеки нужно построить в учебных заданиях по готовому ТЗ и В [заданиях ступени 1](../labs/README.md) вы строите `events_enriched_v`,
образцу. Разминка — представление `events_enriched_v` (зерно события), затем `top_pages_daily`, `session_overview`, `dq_errors_daily` и `utm_effectiveness`.
агрегаты `top_pages_daily`, `session_overview` и `dq_errors_daily`, последней — Для каждой физической витрины нужно добавить сборку в `dm_transform`.
самая содержательная `utm_effectiveness` с уроком `declared_*`. В финале Представление `events_enriched_v` отдельной задачи не требует.
каждого задания с агрегатом его задачу нужно добавить в `dm_transform`. До
этого конвейер о витрине не знает, после — пересобирает её вместе со
стендовыми. Представление из разминки добавлять некуда: его не нужно
пересобирать. Тексты заданий лежат в [`docs/labs/`](../labs/README.md).
Дашборд этапа 8 читает только готовые витрины стенда, поэтому его приёмка не Стендовый дашборд читает готовые витрины и работает до выполнения заданий.
зависит от домашних заданий. Отдельный дашборд по учебным витринам можно Дашборд по своим витринам можно собрать в
собрать в [факультативном задании](../labs/optional/superset-dashboard.md), [факультативе](../labs/optional/superset-dashboard.md) после ступени 2.
доступном после ступени 2. Устройство стендового дашборда описано в Устройство стендового дашборда — в [справочнике Superset](superset.md).
[справочнике Superset](superset.md).
## Ритм пересборки ## Ритм пересборки
День витрины пересобирается, пока может измениться его источник, — Дни для пересчета выбирают по тому, как меняется источник:
поэтому ритмов три, по числу источников:
- **от событий и сессий** — день замерзает сразу; пересборка догоняет - События и сессии прожитого дня на стенде больше не меняются. Витрины
хвост несобранных дней заменой дневных партиций; по ним рассчитывают показатели от первого пропущенного дня до последнего
- **от заказов** — день дышит, пока входит в последний слепок; пересборка прожитого и заменяют дневные партиции.
заменяет партиции этого отрезка, дни за ним закрыты по построению; - Заказы могут меняться в течение семи модельных дней. Последний слепок ODS
- **от карты идентичностей** — не замерзает никогда: склейка задает диапазон дат для пересборки; пропущенные дни могут расширить его.
ретроспективна, покупка сегодня делает куку известной во всех её прошлых - Новая связь в карте идентичностей позволяет определить пользователя
днях, а карта пересчитывается целиком. по его прежним визитам: один `client_id` мог встречаться и до покупки,
которая связала его с `user_id`.
Поэтому `daily_traffic`, как и карта, пересчитывается целиком.
Поэтому `daily_traffic` наследует у своего источника и ритм, и механику: `daily_traffic` каждый раз добавляет всю историю новой версией
полный пересчёт всех дней каждый прогон в `ReplacingMergeTree(_load_ts)`, в `ReplacingMergeTree(_load_ts)`. Дневных партиций у нее нет, а текущие
как у самой карты. Дневных партиций у неё нет: партиция — единица замены, строки выбирает представление через `FINAL`.
а эта витрина заменяется только целиком. На стенде такая работа остается
быстрой; витрина читает сессии и карту через их публичные договоры.
Пересчитывать точечно только затронутые дни не из чего: для этого надо
знать, какие связки новые, а полный пересчёт карты этот след затирает —
восстановление следа оставлено лабе повышенной сложности (issue #149).
У `purchase_vs_orders` два источника и одна непрерывная пересборка. Заказная У `purchase_vs_orders` два источника. Начало диапазона — самая ранняя
сторона задает изменяемое окно последним принятым слепком ODS, событийная — из трех дат: первая дата оформления заказа в последнем слепке, первый
последний прожитый день с `purchase`. Левая граница возвращает первую дыру и пропущенный день и первый день с `awaiting_order` — покупкой, для которой
самый ранний день с `awaiting_order`: предварительная строка уже есть в заказ пока не поступил. День ожидания проверяется отдельно: строки за него
витрине, поэтому поиск отсутствующего дня ее не заметит. Правая граница уже есть в витрине, поэтому он не считается пропущенным.
доходит до более позднего из двух хвостов. Так один прогон догоняет пакетный Конец диапазона — более поздняя из последней даты заказов в слепке и последнего
разгон длиннее K и не оставляет старое ожидание замороженным. прожитого дня с покупкой. Один запуск пересобирает весь отрезок,
даже если с предыдущего запуска прошло больше семи модельных дней.
Для учебной сводки брака действует отдельное правило: дни выбираются
по времени загрузки, а прежние итоги сохраняются после удаления
источника по TTL. Оно разобрано в
[задании `dq_errors_daily`](../labs/1-4-dq-errors-daily.md).
## Витрины стенда ## Витрины стенда
Публичные представления перечисляют деловые колонки и координаты запуска Представления `_v` явно перечисляют поля для отчетов, а также `_load_id`
`_load_id`, `_load_ts` явно. Новая колонка физической таблицы сама в договор не и `_load_ts` — идентификатор запуска и время записи. Новая колонка
попадет. физической таблицы появится в представлении только после изменения его SQL.
### `dm.revenue_daily_v` ### `dm.revenue_daily_v`
Зерно: день заказа × категория товара. Витрина читает только оплаченные Одна строка содержит итоги оплаченных заказов за день по категории товара.
заказы из `dds.order_v`, синхронно разворачивает три массива позиций и узнает Запрос читает `dds.order_v` и разворачивает массивы артикулов, количеств
категорию артикула через `dic.products`. Один заказ с несколькими позициями и цен в строки позиций заказа. Артикул, количество и цена с одним индексом
категории считается в `orders` один раз. `units` складывает количество единиц, становятся одной строкой. Категория артикула берется из `dic.products`.
`revenue` складывает цену позиции × количество. Скидка, доставка и итог заказа
между категориями не делятся. `aov` равен `revenue / orders` в том же срезе и
хранит два знака после запятой.
Первый запуск строит всю доступную историю. Следующие берут границы дней из `orders` считает разные заказы: несколько позиций одной категории
последнего слепка ODS: только источник знает, какие даты еще может изменить. в заказе не увеличивают этот счетчик. `units` — сумма количества товаров,
Пропущенный оплаченный день расширяет левую границу, чтобы частичный отказ `revenue` — сумма произведений цены на количество. Скидка и доставка
не оставил дыру после следующего прогона. Агрегат при этом читает DDS; ODS в нее не входят. `aov` равен `revenue / orders` для того же дня и категории
отвечает только на вопрос об объеме. с округлением до двух знаков после запятой.
`dm.revenue_daily_rep` нарезана по `report_date`; день собирается в Первый запуск рассчитывает выручку за всю доступную историю. Следующие
таблице-доноре и заменяется через `REPLACE PARTITION`. Цель и донор шардируются берут диапазон дат оформления заказов из последнего слепка ODS.
по `cityHash64(product_category)`. Кроме дня, у витрины только категория, а Если в DDS есть оплаченные заказы за день, отсутствующий в витрине,
шесть категорий стартового мира близки по объему. Более сложный составной ключ левая граница расширяется до него. Агрегат читает DDS; ODS отвечает
распределил бы эту маленькую таблицу без нового урока. только на вопрос об объеме.
В группе `revenue_daily` дага `dm_transform` три шага:
1. [`revenue_daily_scope.sql`](../../sql/dm/revenue_daily_scope.sql)
возвращает `first_day` и `last_day`. Без заказов в ODS или DDS
запрос не возвращает строк, и задача завершается ошибкой.
2. [`revenue_daily_rebuild.sql`](../../sql/dm/revenue_daily_rebuild.sql)
очищает `dm.revenue_daily_stage_rep`, рассчитывает показатели за выбранные
дни и записывает строки через `dm.revenue_daily_stage_dist`.
Вставка ждет доставки строк на шарды.
3. [`revenue_daily_replace.sql`](../../sql/dm/revenue_daily_replace.sql)
заменяет в `dm.revenue_daily_rep` каждую дневную партицию `report_date`
готовой партицией `dm.revenue_daily_stage_rep`.
Повтор замены не добавляет копий строк. Ограничения при частичном отказе
описаны в [конвейере](etl.md#идемпотентность).
Основная и промежуточная таблицы распределяют строки по
`cityHash64(product_category)`. На каждый день приходится не больше шести
категорий стартового мира. Для такой маленькой таблицы достаточно простого
ключа по категории.
### `dm.purchase_vs_orders_v` ### `dm.purchase_vs_orders_v`
Зерно — заказ. Покупки из `dds.event_v` сначала сворачиваются по Одна строка сопоставляет события покупки с заказом магазина. События
`purchase_id[1]`: контракт источника обещает один номер и одну объявленную из `dds.event_v` группируются по номеру заказа `purchase_id[1]`:
сумму, а отдельный счет событий оставляет класс `duplicate_event` по формату источника у покупки один номер и одна заявленная сумма.
наблюдаемым. Заказы приходят из `dds.order_v`. Обе стороны записаны Отдельный счет событий позволяет обнаружить повтор покупки
подзапросами, затем соединены `FULL OUTER GLOBAL JOIN`: голая левая и присвоить класс `duplicate_event`. Заказы берутся из `dds.order_v`.
Distributed-таблица вернула бы каждый непарный заказ по разу с каждого шарда
Полное соединение `FULL OUTER GLOBAL JOIN` сохраняет и покупки без заказа,
и заказы без покупки. Обе стороны записаны подзапросами: при прямом чтении
левой Distributed-таблицы каждый непарный заказ повторился бы по числу шардов
([исследование](../research/2026-08-22-distributed-join.md)). ([исследование](../research/2026-08-22-distributed-join.md)).
Непарная сторона остается `NULL` благодаря `join_use_nulls = 1`. При Если для строки нет пары, поля отсутствующей стороны получают `NULL`
`join_use_nulls = 0` ClickHouse заполнил бы ее нулем или пустой строкой и скрыл благодаря `join_use_nulls = 1`. При `join_use_nulls = 0` они заполнились бы
отсутствие источника. Клиентская `declared_revenue` и серверная `items_total` нулями или пустыми строками, которые сложнее отличить от данных источника.
сравниваются как `Decimal(18, 2)`. Классы проверяются по порядку: Клиентская `declared_revenue` и серверная `items_total` сравниваются
как `Decimal(18, 2)`. Классы проверяются по порядку:
`awaiting_order` для покупки без заказа, затем `cancelled`, `lost_event`, `awaiting_order` для покупки без заказа, затем `cancelled`, `lost_event`,
`duplicate_event`, `amount_delta`, `match`. Поэтому отмененный заказ без `duplicate_event`, `amount_delta`, `match`. Поэтому отмененный заказ без
события остается отмененным, а не потерянным. события остается отмененным, а не потерянным.
Витрина нарезана по `order_day`, цель и донор шардируются по Партиции витрины разделены по `order_day`. Основная и промежуточная таблицы
`cityHash64(order_id)`. Диапазон собирается в доноре одной вставкой и распределяют строки по `cityHash64(order_id)`. Запрос рассчитывает весь
заменяется по дням. Первичная сборка охватывает всю историю обоих источников; выбранный диапазон и одной вставкой записывает строки в промежуточную
следующие прогоны идут по двустороннему ритму, описанному выше. таблицу. Затем ее дневные разделы заменяют разделы основной таблицы.
Первичная сборка охватывает всю историю обоих источников;
следующие прогоны выбирают дни по правилам обоих источников, описанным выше.
### `dm.daily_traffic_v` ### `dm.daily_traffic_v`
В строке один день. `visitors` считает разные `client_id` из В строке один день. `visitors` считает разные `client_id` из
`dds.session_v`; `known_users` считает разные `user_id` после левого соединения `dds.session_v`; `known_users` считает разные `user_id` после левого соединения
с полной текущей `dds.identity_map_v`. Анонимная сессия остается в витрине. с полной текущей `dds.identity_map_v`. Анонимная сессия остается в витрине.
Несколько кук одного человека увеличивают `visitors`, но дают одного Несколько кук, связанных с одним пользователем магазина, учитываются
`known_user`, поэтому `known_users <= visitors`. в `visitors` по отдельности, а в `known_users` дают одного пользователя.
Поэтому `known_users <= visitors`.
Сессии и карта лежат по `client_id`, но публичные `_v` скрывают Сессии и карта распределены по `client_id`. Однако на ClickHouse 26.3
`Distributed`-таблицы от локальной подстановки. На ClickHouse 26.3 чтение через публичные `_v` с `distributed_product_mode = 'local'`
`EXPLAIN PLAN` показал чтение обоих шардов на инициаторе: настройка не дало локального соединения. Поэтому в запросе явно указан
`distributed_product_mode = 'local'` локального соединения не дала. Поэтому `GLOBAL LEFT JOIN`. Он сохраняет сессии без известного пользователя;
витрина использует явный `GLOBAL LEFT JOIN` через публичные договоры, а не в `known_users` учитываются только ненулевые `user_id`.
подменяет их физическими таблицами. Это дороже по сети, зато граница слоев Обновление всей истории новой версией описано в разделе
остается явной. Новая связь куки меняет прошлые дни, поэтому каждый запуск [«Ритм пересборки»](#ритм-пересборки).
пишет полную историю новой версией в `ReplacingMergeTree(_load_ts)`, а
публичное представление выбирает текущие строки через `FINAL`.
## `dm.dq_summary` ## `dm.dq_summary`
Зерно публичного `dm.dq_summary_v` — «день данных × атомарная проверка». Одна строка `dm.dq_summary_v` — день данных и одна проверка.
Строка отвечает, каково качество дня сейчас; историю запусков хранит Airflow. Строка отвечает, каково качество дня сейчас; историю запусков хранит Airflow.
Партиция совпадает с днём данных и заменяется через `REPLACE PARTITION`. Партиция совпадает с днём данных и заменяется через `REPLACE PARTITION`.
Деловые колонки сводки: Деловые колонки сводки:
- `data_date` — проверяемый день; - `data_date` — проверяемый день;
- `check_name` — устойчивое имя инварианта; - `check_name` — имя проверяемого правила;
- `reference_rows` — число деловых ключей на эталонной стороне; - `reference_rows` — число деловых ключей на эталонной стороне;
- `actual_rows` — число деловых ключей в проверяемом объекте; - `actual_rows` — число деловых ключей в проверяемом объекте;
- `failed_rows` — число отсутствующих или различающихся ключей; - `failed_rows` — число отсутствующих или различающихся ключей;
- `status` — `pass` при нуле нарушений, иначе `fail`. - `status` — `pass` при нуле нарушений, иначе `fail`.
Служебные `_load_id` и `_load_ts` называют собравший запуск. Представление В `_load_id` и `_load_ts` записаны идентификатор запуска и время расчета
перечисляет колонки явно. Порогов и промежуточного состояния нет. сводки. Даже одно расхождение дает `fail`.
## Звено DQ конвейера ## Звено DQ конвейера
Даг `dq_check` стоит в `etl_pipeline` после `dm_transform` и проверяет пять Даг `dq_check` стоит в `etl_pipeline` после `dm_transform` и проверяет пять
атомарных договоров: правил:
- `sessions_vs_reference`: ключ — `client_id` внутри дня, эталон — - `sessions_vs_reference`: ключ — `client_id` внутри дня, эталон —
`uniqExact(VisitID)` из `ods.event_v`, объект — число строк `uniqExact(VisitID)` из `ods.event_v`, объект — число строк
@@ -192,43 +215,47 @@ Distributed-таблица вернула бы каждый непарный з
сравниваются с прямыми агрегатами `dds.session_v` и сравниваются с прямыми агрегатами `dds.session_v` и
`dds.identity_map_v`. `dds.identity_map_v`.
Область каждой проверки выводится из объединения сторон. Полностью потерянный Каждая проверка берет дни и ключи из обоих сравниваемых наборов данных.
день или ключ поэтому не исчезает из результата. Разные ошибки тоже не могут Поэтому потерянная строка остается видна на другой стороне сравнения.
скомпенсировать друг друга общим итогом: `failed_rows` считается после Расхождения считаются по ключам: завышение одного показателя не скроет
сопоставления деловых ключей. занижение другого в общей сумме за день.
`classes_vs_inventory` — техническая растяжка пути от генератора до DM. Она `classes_vs_inventory` сравнивает результат с паспортом мира. Это техническая
подтверждает, что до витрины доехал записанный в паспорте мир, но не проверяет проверка доставки и обработки эталонных данных стенда. Она не проверяет
сам генератор: у настоящего магазина такого оракула нет. сам генератор; у настоящего магазина такого эталона нет.
Четыре SQL-сверки считают дневные итоги в ClickHouse и возвращают Airflow только Четыре SQL-сверки считают дневные итоги в ClickHouse и возвращают Airflow только
сводку и малый срез диагностики. Техническая сверка с паспортом читает небольшой сводку и несколько примеров расхождений. Техническая сверка с паспортом читает небольшой
файл JSON в Python. Все пять сборок пишут в общий донор. После их успеха даг файл JSON в Python. Результаты всех пяти проверок записываются в общую
заменяет проверенные дневные партиции. Затем именованные задачи читают промежуточную таблицу. Когда расчеты завершились, ее дневные разделы
опубликованную сводку и падают при `failed_rows > 0`. Расхождение успевает заменяют разделы `dm.dq_summary_rep`. Затем отдельные задачи читают
сохраниться до красного завершения, а ошибка сборки не затирает прежний сохраненную сводку и завершаются ошибкой при `failed_rows > 0`.
результат. Красная задача печатает не больше двадцати нарушенных ключей за Так расхождение сохраняется в таблице до отказа задачи. Если сам расчет
проверку со значениями сторон. Для полного списка итоговую агрегацию SQL-сверки не выполнен, прежняя сводка остается на месте. Задача с расхождениями
выводит в журнал не больше двадцати ключей со значениями обеих сторон. Для полного списка итоговую агрегацию SQL-сверки
заменяют на `SELECT * FROM comparison WHERE failed ORDER BY data_date, заменяют на `SELECT * FROM comparison WHERE failed ORDER BY data_date,
business_key`. business_key`.
Если после починки день исчез с обеих сторон, его больше нет и в новом доноре. Если день исчез из обоих сравниваемых наборов, новый расчет не создаст
Такой день удаляется из сводки отдельно: старая красная строка не должна для него строк. Даг отдельно удаляет прежний результат за этот день
превращаться в историю попыток. из сводки.
## Даг самопроверки ## Даг самопроверки
Домашние задания проверяет `homework_check` — отдельный даг без расписания, `homework_check` запускается вручную и содержит задачу для каждого из
который запускают вручную. В нём есть задача для каждого из [пяти заданий по витринам](../labs/README.md). Если витрины нет или значения
[пяти заданий по витринам](../labs/README.md). расходятся с источником, задача завершается ошибкой. Журнал показывает
Задача красная, пока витрины нет или счёт не сходится, и зелёная, когда причину и примеры несовпадений. Успех означает, что проверяемые значения
построено верно; сообщение об ошибке говорит, что не так и куда смотреть. Проверки — инварианты против источников витрины, совпали; остальные требования задания вы проверяете сами.
золотые константы не прибиваются. В `dq_summary` даг не пишет: он инструмент
лабы, а не история качества конвейера.
Приём проверен на соседнем учебном стенде Airflow + Greenplum: Ожидаемые значения рассчитываются по исходным данным текущего стенда.
валидационный даг с группами проверок по слоям давал обратную связь по каждому Для четырех заданий это события или сессии DDS, для сводки брака —
шагу задания, не трогая рабочий конвейер. таблицы ошибок ODS. Сравнение по деловому ключу учитывает отсутствующие
строки с обеих сторон. Результаты не пишутся в `dq_summary`:
самопроверка учебных витрин отделена от качества готового конвейера.
Готовые SQL-проверки открывайте после собственного решения: они содержат
расчет существенной части задания.
## Отклонённые варианты ## Отклонённые варианты
+107 -82
View File
@@ -1,28 +1,23 @@
# Дневной конвейер трансформаций # Дневной конвейер трансформаций
Как принятые источники превращаются в модель DDS и витрины DM: кто запускает Даг `etl_pipeline` собирает модель DDS, обновляет витрины DM и проверяет
трансформации, когда и в каком объёме. Правила самих сущностей — зерно, результат. Здесь описаны запуск, выбор дней и запись результата в таблицы.
движки, что именно пересобирается — живут в доках слоя Состав моделей — в [справочниках DDS](dds/README.md) и [DM](dm.md).
([карта модели DDS](dds/README.md)); здесь — только конвейер.
Дневная сборка вперёд появляется на этапе 4 и остаётся основным режимом
конвейера. На этапе 5 к ней добавляется партиционная переобработка: запуск
произвольного прошлого дня по команде.
## Форма ## Форма
Дневной конвейер на стенде один — даг `etl_pipeline`. Своей работы `etl_pipeline` последовательно запускает `dds_transform`, `dm_transform`
у него нет: он дёргает слойные даги последовательно и ждёт каждый — и `dq_check`. Каждый следующий даг начинается после успеха предыдущего.
`dds_transform`, затем `dm_transform` и `dq_check`. Состав проверок описан в Для этого `TriggerDagRunOperator` ждет завершения запущенного им дага.
[доке слоя DM](dm.md). Если тот завершился ошибкой, задача ожидания в конвейере тоже завершится
ошибкой.
Поведение проверено через Context7 по документации Airflow 3.x (3.1.6).
Ждущие вызовы сделаны `TriggerDagRunOperator`. Проверка через Context7 по У этих дагов нет расписания. Их вызывает конвейер; для отдельного разбора
ближайшей доступной документации Airflow 3.x (версия 3.1.6) подтвердила, что можно запустить даг слоя вручную. У каждого разрешен один активный запуск:
оператор опрашивает точный созданный им прогон и краснеет вслед за потомком. два запуска одного дага используют одни промежуточные таблицы и могли бы
стереть результаты друг друга. Расчет каждой модели выполняется отдельной
Слойные даги устроены по образцу `orders_ingest`: без расписания, один задачей или группой задач.
активный прогон, запускаются триггером конвейера или рукой при разборе
происшествий. Витрины внутри `dm_transform` — отдельные задачи.
## Три пути запуска ## Три пути запуска
@@ -34,83 +29,113 @@ world_live_day ───┼─> etl_pipeline ─> dds_transform ─> dm_transfor
world_next_day ─────> источники без ETL world_next_day ─────> источники без ETL
``` ```
- **Живой день.** `world_live_day` дёргает конвейер с ожиданием — последним - `world_live_day` запускает конвейер после успешной генерации, приема
шагом, после установки позиции. Порядок несёт смысл: день сыгран — факт, заказов и обновления `world_position`. Если трансформация упала, день
упавшая трансформация его не отменяет, поэтому позиция ставится раньше; но уже прожит и позиция не откатывается. После исправления причины
зеленеть, не узнав, разложился ли день, шаговый даг не должен ([ADR запустите `etl_pipeline` вручную.
0003](../adr/0003-dag-run-state.md)). Такой отказ чинится перезапуском - `world_next_day` отправляет дни пачкой и ждет приема заказов, но конвейер
`etl_pipeline` рукой — перезапуску ничего не нужно помнить, см. «Объём не запускает. После него запустите `etl_pipeline` сами.
прогона». - `world_initialize` запускает конвейер последним шагом: при успешном
- **Разгон пачкой.** `world_next_day` конвейер не дёргает: даги модельного первом `make up` начальные данные уже собраны в DDS и DM.
времени управляют генератором и только им. После разгона менти запускает
конвейер сам — и видит, как слои догоняют уехавший вперёд мир.
- **Рождение мира.** `world_initialize` дёргает конвейер последним шагом:
`make up` отдаёт стенд с собранными слоями, а не пустые витрины. Уроки
ручного запуска начинаются со следующих дней.
## Объём прогона ## Объём прогона
Параметров у конвейера нет: объём он выводит сам из `world_position` и Объем прогона — дни, которые обрабатывает один запуск. Даты передавать
состояния слоёв. Сессии догоняют хвост — от первого несобранного дня по не нужно: даги определяют их по состоянию таблиц и `world_position`.
последний прожитый; заказы пересобирают дышащее окно и тот же хвост; карта Например, после генерации семи дней без ETL один запуск рассчитает сессии
идентичностей пересчитывается целиком; правила витрин — у за все семь дней.
[слоя DM](dm.md).
Поэтому один и тот же запуск одинаково верен из триггера, с кнопки менти и
после падения, а разгону на семь дней хватает одного прогона.
Окно заказов задаёт последний слепок, наблюдаемый в ODS. У каждого заказа Для расчета сессий выбираются дни от первого, за который в DDS еще нет
`snapshot_date` равен последнему слепку, в котором источник его показал. сессий, до последнего прожитого.
Трансформация выбирает строки с максимальной `snapshot_date` и берёт среди них `world_position` хранит номер следующего дня для генерации, поэтому
минимальный и максимальный `order_date`. Это границы дней, состояние которых последний прожитый день равен `min(EventDate) + world_position - 1`.
источник ещё может изменить Начало календаря берется из ODS; события ранних дней там не удаляются по TTL.
([ADR 0014](../adr/0014-order-version-snapshot-date.md)). Кроме живого окна, Один `max(EventDate)` не годится: события за день есть и во время его
трансформация закрывает пропуски: находит самый ранний день, представленный в генерации, когда последние визиты еще не закончились.
ODS и ещё не собранный в DDS, затем при необходимости расширяет до него левую Точный запрос — [`session_scope.sql`](../../sql/dds/session_scope.sql).
границу.
Позиция мира заказам не отвечает: она говорит, какой день прожит, а не какой Для заказов диапазон задает последний принятый слепок ODS. Среди строк
доехал. Прогон дня D везёт слепок дня D − 1, да и ставится позиция раньше с максимальной `snapshot_date` выбираются самая ранняя и самая поздняя
трансформации, так что после неудачного приёма обгоняет хранилище. Слой обязан даты оформления заказа. Состояние заказов за эти дни еще может измениться.
считать объём по тому, что у него есть, а не по тому, что мир о себе заявляет. Если в ODS есть заказы за более ранний день, а в DDS за него нет строк,
начало диапазона сдвигается до этого дня. Запрос заново рассчитывает
заказы за все дни выбранного отрезка.
Для сессий границу задаёт `world_position`: она отличает полностью прожитый Позиция мира для выбора дней заказов не подходит: с событиями дня D
день от ещё идущего. Сами события этой границы не показывают: и посреди дня D, приходит слепок за D−1. Объем рассчитывается по принятым строкам ODS;
и сразу после его конца их позднейший день равен D. Если собрать сессии посреди их наличие еще не доказывает полноту слепка.
дня, визит, начатый в 23:50, останется обрезанным: завершённый день слой
повторно не собирает. Поэтому последний прожитый день равен первому дню Карта идентичностей пересчитывается целиком. Витрины выбирают дни
событий плюс позиция минус один по своим источникам — см. [ритм пересборки DM](dm.md#ритм-пересборки).
([`sql/dds/session_scope.sql`](../../sql/dds/session_scope.sql)). Если за день сохранилась хотя бы одна строка, запрос не считает его
Оговорка про «доехал» остаётся в силе и здесь: за хвост потока отвечает не пропущенным. Полноту данных проверяют отдельно.
позиция, а «Готовность источника».
## Готовность источника ## Готовность источника
Первый шаг `dds_transform` проходит два барьера за общий срок в 300 секунд. Перед сборкой DDS задача `wait_for_source` последовательно ждет двух
Сначала клиент `confluent-kafka` сверяет подтвержденные смещения группы условий:
`clickstream_hits` с верхними смещениями обоих разделов `hits`. Группа, оба
раздела и все смещения должны существовать. Суммарное отставание должно стать
нулевым. Каждый опрос пишет в журнал оба смещения и отставание по разделу.
Только после этого шаг проверяет обе ноды ClickHouse через 1. Группа `clickstream_hits` дочитала оба раздела топика `hits`.
`clusterAllReplicas`: у `ods.event_dist` в `system.distribution_queue` не Подтвержденные смещения сравниваются с верхними смещениями Kafka.
должно остаться файлов. Каждый опрос пишет в журнал ноды, число файлов и Группа и смещения должны существовать, суммарное отставание — равняться нулю.
`last_exception` очереди. Оба барьера опрашиваются раз в три секунды и делят 2. В `system.distribution_queue` у `ods.event_dist` не осталось файлов
один крайний срок. После Kafka срок не отсчитывается заново. доставки на шарды. Запрос через `clusterAllReplicas` читает обе ноды.
Зелёный приём заказов о событиях не говорит ничего — топики разные и Опросы идут с паузой в три секунды; на обе стадии задан общий срок
потребители разные. Одного нулевого отставания Kafka тоже мало: чтец мог 300 секунд. Журнал показывает смещения и отставание Kafka, затем число
подтвердить сообщение раньше, чем Distributed-таблица доставила файл на шард. файлов и `last_exception` очереди на каждой ноде. Если готовность не
Два барьера закрывают обе части пути до того, как сессии выберут свой объем. подтверждена за срок ожидания, задача завершается ошибкой.
Одного нулевого отставания Kafka мало: сообщение может быть подтверждено,
пока Distributed-таблица еще доставляет его на шард. Успех `orders_ingest`
тоже не заменяет эти проверки: у заказов другой топик и потребитель.
Эти условия проверяют доставку на момент опроса. Они не останавливают
поступление новых событий и не сверяют содержимое таблиц. Поэтому
`world_position` отдельно ограничивает сборку сессий прожитыми днями,
а качество результата проверяет `dq_check`.
## Идемпотентность ## Идемпотентность
Партиция не заменяется простой вставкой. День собирается в таблицу-двойник — Идемпотентность означает, что повторная обработка тех же исходных данных
вставка через Distributed раскладывает строки по шардам, — затем не добавляет копии деловых строк. Служебные `_load_id` и `_load_ts`
`REPLACE PARTITION` на кластере атомарно подменяет кусок каждого шарда. Эта при новой сборке меняются.
механика — часть самих трансформаций и строится этапом 4: без неё дневной
прогон не повторяем. Сущности, пересчитываемые целиком, идемпотентны и без Результат дневного расчета сначала записывается в промежуточную таблицу.
замены: полная перезапись в `ReplacingMergeTree` — так живут карта Она нужна, чтобы читатели продолжали видеть прежние данные, пока новые
идентичностей и витрина `daily_traffic` ([слой DM](dm.md)). еще рассчитываются. Например, для сессий это `dds.session_stage_rep`,
а основная таблица — `dds.session_rep`.
Задачи выполняются в таком порядке:
1. `scope` выбирает даты для расчета.
2. `rebuild` очищает промежуточную таблицу, рассчитывает строки за выбранные
дни и записывает их через `_stage_dist`. Настройка
`distributed_foreground_insert = 1` заставляет вставку дождаться
доставки строк на шарды.
3. `replace` заменяет в основной таблице раздел за каждый выбранный день
готовым разделом промежуточной таблицы. Раздел таблицы называется
партицией; для замены используется `REPLACE PARTITION`.
Структура и распределение строк по шардам у двух таблиц должны совпадать.
Замена выполняется на локальных `_rep` с `ON CLUSTER`. На каждом шарде
читатель видит либо старый, либо новый раздел целиком — это атомарность
замены. Общей транзакции для двух шардов и всех дней нет: при сбое один
шард может уже содержать новый результат, а другой — прежний. Повторить
`replace` можно, пока промежуточная таблица хранит тот же результат;
копии строк в основной таблице от этого не появятся.
Новый запуск всего конвейера заново выбирает дни. Он не обязан повторить
все дни неудачного запуска: если дата уже есть в целевой таблице, поиск
пропусков ее не выберет. В частности, изменение SQL сессий само по себе
не заставляет пересчитать заполненные дни. Повторная сборка выбранного
дня разобрана в [задании ступени 4](../labs/4-1-session-pipeline.md).
Карта идентичностей и `daily_traffic` обновляются иначе: каждый запуск
добавляет полный результат новой версией в `ReplacingMergeTree(_load_ts)`.
Публичные представления читают текущие строки через `FINAL`. Старые версии
могут физически оставаться до фонового слияния; повторяемость относится
к результату чтения, а не к числу строк на диске.
## Чего конвейер о себе не знает ## Чего конвейер о себе не знает
+72 -91
View File
@@ -30,115 +30,96 @@ keeper, Kafka, каркас сервисов. Этап 2 собран: в `sql/d
## Имена объектов ## Имена объектов
Имя объекта заканчивается тем, что это за объект: Суффикс показывает назначение объекта:
| Суффикс | Что это | | Суффикс | Что это |
|---|---| |---|---|
| `_rep` | локальная таблица шарда, движок семейства `Replicated*` | | `_rep` | локальная таблица: хранит данные своего шарда, движок семейства `Replicated*` |
| `_dist` | `Distributed` поверх одноимённой локальной | | `_dist` | таблица `Distributed`: читает данные шардов и распределяет запись между ними |
| `_kafka` | таблица на движке `Kafka` | | `_kafka` | таблица на движке `Kafka`: читает сообщения топика |
| `_file` | таблица на движке `File` — чтец файла | | `_file` | таблица на движке `File`: читает файл |
| `_mv` | материализованное представление | | `_mv` | материализованное представление: обрабатывает вставляемые данные |
| `_v` | обычное представление, публичная поверхность чтения | | `_v` | обычное представление: выполняет сохраненный запрос при чтении |
Суффикс носит каждый физический объект. Голого имени у таблицы не существует: Для запросов к моделям DDS и витринам DM используйте `_v`. Представления
запрос к `stg.hits_raw` даёт громкую ошибку «нет такой таблицы» — а под голым явно перечисляют колонки, поэтому добавление поля в физическую таблицу
именем в документах и разговоре понимается сущность, у которой этих объектов не меняет набор полей для читателя. У `dds.event_v` нет отдельной таблицы
несколько. Единственное исключение — словари: у них воплощение одно, шардировать DDS: представление читает события ODS.
нечего, и суффикс ничего не различал бы. В зоне `dic` обе формы стоят рядом и
правило видно целиком: `dic.products_file` — чтец файла, `dic.products` — сам
словарь.
Распространённая конвенция, где голое имя означает локальную таблицу, а В документации имя без суффикса, например `stg.hits_raw`, обозначает
распределённая получает суффикс `_all`, ошибается иначе: забытый суффикс тихо сущность с несколькими объектами. Таблицы с таким именем нет: в запросе
возвращает данные одного шарда. Правило спеки «проверки и контрольные суммы — нужно указать суффикс. Исключение — словари ClickHouse: `dic.products`
только по `Distributed`» такую тишину переживает плохо. обращается к каталогу товаров, а `dic.products_file` читает его файл.
Второе свойство — в списке по алфавиту объекты группируются по сущности, а не по В STG имя берется из топика: `hits_raw`, `orders_raw`. В типизированных
технологии: все четыре объекта топика `hits` стоят рядом, потому что различаются слоях оно называет сущность в единственном числе: `event`, `order`,
хвостом, а не началом имени. Речь про дерево в клиенте и про `ORDER BY name`: `session`. Объекты одной сущности различаются суффиксом и стоят рядом
порядок выдачи `SHOW TABLES` документация не оговаривает. при сортировке имен по алфавиту.
Начало имени — сущность, и берётся она в разных слоях из разных мест. В STG имя `_rep` и `_dist` нужны для загрузки, диагностики и обслуживания. Роль
приходит от транспорта: слой хранит то, что доехало по топику, и зовётся именем `bi_reader` может читать их в ODS, DDS и DM, но для обычного анализа
топика — `hits_raw` от `hits`, `orders_raw` от `orders`. В типизированных слоях принято использовать `_v`. Это соглашение о чтении, а не ограничение прав;
имя приходит от предметной области и стоит в единственном числе: `event`, причина широкого доступа описана в
`order`, `session`. Граница между «как привезли» и «что это такое» [ADR 0007](../adr/0007-clickhouse-access.md) и
проходит по STG, и имена её показывают.
В DDS публичный договор сущности заканчивается на `_v`. Потребительские запросы
и витрины называют только представление; `_rep` и `_dist` служат загрузке,
диагностике и обслуживанию. Представление перечисляет колонки явно, даже если
его запрос — простая проекция над `_dist`: договор не выдаёт устройство
кластера и случайно добавленные в физическую таблицу колонки. У события
физической копии в DDS нет, но правило то же: `dds.event_v` — его публичный
договор.
Право доступа здесь намеренно шире договора. Роль `bi_reader` может читать
любые объекты ODS, DDS и DM, но физические объекты от этого не становятся
публичной поверхностью. Конвенцию держат явный список колонок, документация и
ревью, а не запрет на физические таблицы.
Широкое право принято в [ADR 0007](../adr/0007-clickhouse-access.md): узкая
граница поверх `Distributed` потребовала бы отдельной механики. Механизм
ClickHouse и отклонённые варианты записаны в
[исследовании](../research/2026-08-25-clickhouse-distributed-view-security.md). [исследовании](../research/2026-08-25-clickhouse-distributed-view-security.md).
## Раскладка по шардам ## Раскладка по шардам
Пишем только в `_dist`. Локальные таблицы остаются для чтения и обслуживания — Шард хранит часть строк. При записи через `_dist` место строки выбирает
операций с партициями, ручной переобработки. Правило не про удобство: при записи ключ шардирования. При прямой записи в `_rep` она остается на той ноде,
через распределённую таблицу раскладку определяет ключ шардирования, то есть где выполнена вставка. Поэтому данные записывают через `_dist`,
свойство данных, а при записи в локальную — то, какая нода случайно выполняла а операции с партициями выполняют на локальных `_rep`.
код. Отсюда урок стенда: какая нода читала топик, меняется между прогонами
(видно в колонке `consumer_host`), а куда легли данные — нет.
Ключи шардирования: `cityHash64(ClientID)` у событий, `cityHash64(order_id)` у У событий ключ — `cityHash64(ClientID)`, у заказов —
заказов, `cityHash64` сырой строки у STG и у таблицы ошибок. У первых двух хеш `cityHash64(order_id)`, у STG и таблиц ошибок — хеш сырой строки.
выбран против перекоса: структурированный числовой идентификатор распределяется Хеш распределяет числовые идентификаторы по шардам и направляет одинаковые
по остатку от деления неравномерно. У сырого слоя выбора нет — строку иначе не сообщения на один шард. У сырой строки и разобранного события разные
разложишь; там хеш даёт другое свойство, одинаковые сообщения ложатся на один ключи: они могут оказаться на разных шардах. Счетчики STG и ODS сравнивают
шард. по всему кластеру через `_dist`, а не отдельно по каждой ноде.
Ключи у слоёв разные, и это имеет наблюдаемое следствие: сырая строка и При `INSERT INTO цель_dist SELECT … FROM источник_dist` важна настройка
разобранное из неё событие почти всегда оказываются на разных шардах. `parallel_distributed_insert_select`. При значении `2` и одном кластере
Пошардовые счётчики STG и ODS поэтому не сходятся и сходиться не должны — вставка может выполняться локально на шардах, сохраняя раскладку источника
сверять слои можно только через `_dist`. вместо ключа цели. Если ключи различаются, укажите
`parallel_distributed_insert_select = 0`: нода, принявшая запрос,
соберет результат и распределит его по ключу цели. Так сделано в
[`sql/ods/order_parse.sql`](../../sql/ods/order_parse.sql) и при сборке
выручки. Неверную раскладку проверяют по `hostName()` и ожидаемому шарду
строки; общий счет по `_dist` ее не показывает.
Разные ключи у слоёв ставят капкан на вставке `INSERT INTO цель_dist SELECT … События, `dds.session_dist` и `dds.identity_map_dist` распределены
FROM источник_dist`. Заводское значение `parallel_distributed_insert_select` — по `cityHash64(client_id)` (в ODS поле называется `ClientID`). Данные
2: если обе таблицы на одном кластере, вставка исполняется локально на каждом одного посетителя лежат вместе — это ко-локация. Заказы DDS и их сверка
шарде, минуя ключ шардирования цели. Строки наследуют раскладку источника, и сохраняют раскладку по `cityHash64(order_id)`. Обычное соединение допускается
ошибки при этом нет — расхождение видно только пересчётом ключей по по ключу ко-локации; для остальных соединений применяется `GLOBAL`.
`hostName()`. Так половина заказов и легла на оба шарда, где Само совпадение ключей еще не гарантирует локального выполнения запроса
`ReplacingMergeTree` через границу шарда дубли не схлопывает (замер — «Что через представления — пример есть в [описании `daily_traffic`](dm.md#dmdaily_traffic_v).
проверено», #137). Лекарство — явное `parallel_distributed_insert_select = 0`
в `SETTINGS` такой вставки: раскладку делает инициатор, по ключу цели
(`sql/ods/order_parse.sql`). Капкан ждёт и сборки DDS: они тоже будут читать
`_dist` и писать в `_dist` с другим ключом.
Ключи ко-локации названы заранее, потому что на них стоит политика соединений из Для дневной пересборки DDS и DM результат сначала записывают
раздела 6 спеки: обычное соединение разрешено только по ключу ко-локации, всё в промежуточную таблицу. Основная таблица в это время остается доступна
прочее — через `GLOBAL`. Значит `dds.session_dist` и `dds.identity_map_dist` для чтения с прежними данными. Например, заказы записываются через
шардируются по `cityHash64(client_id)`. Объекты DDS с зерном заказа сохраняют `dds.order_stage_dist` в `dds.order_stage_rep`, затем ее дневные разделы
ко-локацию по `cityHash64(order_id)`; ключи остальных частей модели и витрин заменяют разделы основной `dds.order_rep`.
появляются вместе с ними.
Замена партиций не используется для `ods.order`: версии прошлых дней У промежуточной и основной таблиц должны совпадать структура, ключи
доливаются, а прямое чтение Kafka не задаёт границы полного слепка ([ADR партиции и сортировки, первичный ключ и политика хранения. Поэтому
0010](../adr/0010-order-versions-in-ods.md)). Если партиционная пересборка `dds.order_stage_rep` объявлена через `AS dds.order_rep` с совпадающими
понадобится будущим объектам DDS или DM, партиция-донор должна быть заранее ключами. Требования сверены через Context7 с
разложена по шардам по тому же ключу. Форму донора следует решать вместе с таким [документацией ClickHouse](https://clickhouse.com/docs/sql-reference/statements/alter/partition#replace-partition)
объектом, а не переносить на ODS заранее. для схемы на ClickHouse 26.3. DDL показан в
Первым такой донор понадобился заказу DDS, и правило на нём видно целиком:
`dds.order_stage_rep` объявлен через `AS dds.order_rep` и шардируется тем же
`cityHash64(order_id)`. Совпадение структуры требует сама операция, а совпадение
ключа — устройство замены: она идёт по локальным таблицам, шард за шардом, и
строка, попавшая в доноре не на свой шард, при подмене пропала бы с одного шарда
и задвоилась на другом. Доводы целиком — в
[`sql/ddl/45-dds-tables.sql`](../../sql/ddl/45-dds-tables.sql). [`sql/ddl/45-dds-tables.sql`](../../sql/ddl/45-dds-tables.sql).
Промежуточная таблица должна также распределять строки по тому же ключу,
здесь — `cityHash64(order_id)`. `REPLACE PARTITION` копирует раздел между
локальными таблицами каждого шарда и не перераспределяет строки.
Поэтому уже в промежуточной таблице заказ должен находиться на нужном шарде.
Перед заменой вставка с `distributed_foreground_insert = 1` дожидается
доставки данных на шарды.
В `ods.order` этот способ не применяется: туда добавляют версии заказов
из слепков, а актуальные строки выбираются при чтении
([ADR 0010](../adr/0010-order-versions-in-ods.md)).
## Служебные колонки ## Служебные колонки
Собственные колонки не повторяют имён виртуальных. Виртуальные даёт движок: Собственные колонки не повторяют имён виртуальных. Виртуальные даёт движок:
+93 -4
View File
@@ -36,7 +36,7 @@ Airflow. В ClickHouse он не пишет ничего.
| `metadata.yaml` | версия формата, `1.0.0`; больше в нем ничего нет | | `metadata.yaml` | версия формата, `1.0.0`; больше в нем ничего нет |
| `databases/clickhouse.yaml` | подключение к ClickHouse: URI без пароля и постоянный uuid | | `databases/clickhouse.yaml` | подключение к ClickHouse: URI без пароля и постоянный uuid |
| `datasets/ClickHouse/*.yaml` | три витрины: `revenue_daily_v`, `purchase_vs_orders_v`, `daily_traffic_v` | | `datasets/ClickHouse/*.yaml` | три витрины: `revenue_daily_v`, `purchase_vs_orders_v`, `daily_traffic_v` |
| `charts/*.yaml` | три графика: столбики выручки, сводная таблица сверки, линии трафика | | `charts/*.yaml` | девять графиков: четыре крупных числа, столбики выручки и кольцо долей, сводная таблица сверки, линии трафика и доля известных |
| `dashboards/business_stories.yaml` | дашборд «Бизнес-сюжеты» и раскладка панелей | | `dashboards/business_stories.yaml` | дашборд «Бизнес-сюжеты» и раскладка панелей |
Связаны файлы не путями, а uuid. Датасет называет `database_uuid`, график — Связаны файлы не путями, а uuid. Датасет называет `database_uuid`, график —
@@ -46,20 +46,51 @@ Airflow. В ClickHouse он не пишет ничего.
Uuid у стенда розданы рукой: общее начало Uuid у стенда розданы рукой: общее начало
`19600000-0000-4000-8000-`, дальше номер объекта в последнем блоке — `1`–`3` `19600000-0000-4000-8000-`, дальше номер объекта в последнем блоке — `1`–`3`
у датасетов, `101`–`103` у графиков, `201` у дашборда у датасетов, `101`–`109` у графиков, `201` у дашборда
(`…-000000000101`). Экспорт из интерфейса приносит свои, сгенерированные; (`…-000000000101`). Экспорт из интерфейса приносит свои, сгенерированные;
их заменяют на эти, иначе повторный импорт заведет вторую копию объекта вместо их заменяют на эти, иначе повторный импорт заведет вторую копию объекта вместо
обновления первой. обновления первой.
Датасет несет русские имена колонок, формат денег и метрику — а график только Датасет несет русские имена колонок, формат чисел и метрики — а график только
ссылается на них по имени. Значит, подпись или формат правятся в одном месте, а ссылается на них по имени. Значит, подпись или формат правятся в одном месте, а
не в каждом графике. не в каждом графике. Два исключения проверены на живом стенде, Superset 6.1.
Подписи оси график берет не из датасета, а из своего `y_axis_format`: без него
ось выручки уходит в «1.2M». И разрез сводной таблицы подписан именем колонки
(`mismatch_class`), хотя колонке даты в той же таблице русское имя достается.
Датасеты объявлены физическими (`schema: dm`, `sql: null`), хотя за именем стоит Датасеты объявлены физическими (`schema: dm`, `sql: null`), хотя за именем стоит
представление ClickHouse. Импорт в источник не ходит и разницы между таблицей и представление ClickHouse. Импорт в источник не ходит и разницы между таблицей и
представлением не знает, а виртуальный датасет со своим SQL просто повторил бы представлением не знает, а виртуальный датасет со своим SQL просто повторил бы
запрос, который уже живет в `sql/ddl/60-dm-views.sql`. запрос, который уже живет в `sql/ddl/60-dm-views.sql`.
## Из чего собран экран
Четыре строки панелей:
- ряд крупных чисел — выручка, заказы, средний чек, посетители;
- выручка по дням и категориям, рядом доля категорий кольцом;
- сводная сверки purchase и заказов;
- посетители и известные пользователи, рядом доля известных.
Правая панель пары отвечает на вопрос «сколько из целого» и берет цвета левой:
категория окрашена одинаково на столбиках и в кольце, доля известных — как
линия известных пользователей рядом. Цвет закрепляет `label_colors` в
`metadata` дашборда. Шесть категорий выписаны в него руками по каталогу
генератора (`generator/src/clickstream_generator/catalog.py`): седьмая
останется без закрепленного цвета, пока файл не поправят.
Средний чек и доля известных считаются метриками датасета —
`SUM(revenue) / nullIf(SUM(orders), 0)` и то же для доли известных. Знаменатель
обернут не зря: на периоде без заказов деление Decimal на ноль в ClickHouse —
не `nan`, а исключение, и панель показала бы ошибку вместо пустоты. У витрины
выручки есть готовая колонка `aov`, но среднее по ней дало бы другое число:
день с тремя заказами получил бы тот же вес, что день с тремя сотнями.
«Период» — единственный фильтр дашборда, тип `filter_time` с пустой целью
(`targets: [{}]`). Такой фильтр колонку не называет: он подставляет выбранный
период в то временное условие, которое уже стоит у каждого графика. Поэтому
сверка сужается по `order_day`, а остальные панели — по `report_date`.
## Импорт при каждой инициализации ## Импорт при каждой инициализации
Последняя строка `infra/superset/init.sh`: Последняя строка `infra/superset/init.sh`:
@@ -152,6 +183,17 @@ superset import-directory /app/import --overwrite
завершается нулем в любом случае. Поэтому импорт проверяют глазами или завершается нулем в любом случае. Поэтому импорт проверяют глазами или
запросом к API — но не тем, что команда отработала. запросом к API — но не тем, что команда отработала.
**Знак рубля приходит не из формата валюты.** Символ Superset берет у
`Intl.NumberFormat` по коду валюты и английской локали стенда: код `RUB`
печатается как «RUB», а знак «₽» вызов роняет, и форматирование молча
откатывается к числу без валюты. Поэтому единица стоит в названии панели —
«Выручка, ₽», — а метрика несет обычный формат `,d`.
**`label_colors` знает имя ряда, а не подпись.** Для разреза имя ряда — это
значение колонки («Бытовая техника»), для графика без разреза — имя метрики
(`known_share`), а не ее русская подпись. Цвет, закрепленный за подписью
«Доля известных», не применяется.
**`--overwrite` синхронизирует датасет разрушительно.** Колонки и метрики **`--overwrite` синхронизирует датасет разрушительно.** Колонки и метрики
берутся из YAML целиком: добавленная в интерфейсе метрика или подпись колонки берутся из YAML целиком: добавленная в интерфейсе метрика или подпись колонки
исчезнет при следующем старте контейнера. Правка датасета — это правка файла. исчезнет при следующем старте контейнера. Правка датасета — это правка файла.
@@ -163,3 +205,50 @@ superset import-directory /app/import --overwrite
живое подключение и хранилище пароля. Сами графики и дашборд ни одна цель не живое подключение и хранилище пароля. Сами графики и дашборд ни одна цель не
проверяет: их смотрят глазами после переинициализации. Полная карта целей — в проверяет: их смотрят глазами после переинициализации. Полная карта целей — в
[доке проверок](testing.md). [доке проверок](testing.md).
## Отклоненные варианты
Оживить дашборд предложат снова, поэтому отказы записаны с доводами. Замерено
на живом стенде 7 сентября 2026 года, Superset 6.1.
**Карта регионов России.** Замысел стоял на клике: щелчок по региону
перестраивает весь экран. Плагин Country Map кросс-фильтр не отдает вовсе —
`behaviors` не объявлены, `setDataMask` не вызывается, а щелчок приближает
карту. Без клика остается картинка, и она рассказывает меньше, чем кажется: 16
городов стенда сворачиваются в 14 регионов из 83, и больше половины сессий
приходится на один. Карта отдана факультативу «Добавь названия регионов в
отчет»: там читатель собирает справочник сам, и она становится наградой за
него.
**Регион как общее измерение витрин.** Чтобы фильтр по региону действовал на
весь экран, регион пришлось бы завести в каждой витрине: Superset датасеты
между собой не соединяет, а фильтр по колонке, которой нет в датасете панели,
отбрасывается с причиной `COL_NOT_IN_DATASOURCE`. У заказа географии нет — она
бралась бы от человека через карту идентичностей и приписывалась бы сущности, у
которой ее в источнике не было. Вдобавок не сходятся ритмы: витрина от карты
идентичностей не замерзает никогда, а выручка живет окном последнего слепка
([дока DM](dm.md)). Стендовый словарь регионов отклонен раньше и по своей
причине — он повторил бы урок `dictGet` каталога вторым разом.
**Кросс-фильтрация между панелями.** Общих измерений у витрин почти нет:
категория есть только у выручки, а день у сверки называется `order_day` против
`report_date` у остальных. Клик перестроил бы часть экрана и оставил другую
нетронутой, а вывод из этого читатель делает про данные, а не про устройство
витрин. Панель фильтров этой болезнью не болеет — там колонка задается явно для
каждой витрины, — поэтому фильтр периода дашборду разрешен, а кросс-фильтрация
нет.
**Вкладки и блоки «что смотреть».** Вкладки были условным вариантом: их
заводят, когда экран перестает читаться одним взглядом. Четыре строки панелей
читаются сразу, поэтому вкладки спрятали бы половину экрана за щелчком без
выигрыша. Блок с текстом «что смотреть» рядом с панелью означал
бы, что панель сама себя не объясняет: тогда чинить надо название и подписи, а
не дописывать к ним пояснение.
**Пороги и раскраска панелей.** Правило
[спеки](../specs/2026-07-30-stand-v2-realism.md), раздел 9: на дашборд смотрят
глазами, красное и зеленое живет в проверках.
Поводы вернуться к разговору: плагин карты начинает отдавать кросс-фильтр; у
заказа появляется адрес доставки; витрины получают общее измерение по своей
причине, не ради дашборда.
+19 -22
View File
@@ -1,50 +1,47 @@
# Путеводитель по стенду # Путеводитель по стенду
Если вы впервые видите стенд, начните отсюда. Путеводитель подскажет, на что Путеводитель объясняет, как данные поступают в хранилище, обрабатываются
смотреть и в каком порядке, а за подробностями отправит в справочники. Так вы и попадают в отчеты. В главах есть короткие примеры, которые можно выполнить
разберетесь в устройстве, прежде чем браться за уроки. на запущенном стенде перед переходом к учебным заданиям.
## Как читать ## Как читать
Тропа такая: [словарь понятий](../../CONTEXT.md) → главы путеводителя → Читайте главы в порядке ниже. Незнакомые термины можно найти в
[справочники `docs/architecture/`](../architecture/) → [словаре понятий](../../CONTEXT.md), а подробности устройства —
[уроки](../labs/README.md). в [технических справочниках](../architecture/). К ним достаточно обращаться
по мере необходимости. После путеводителя переходите к
Словарь не надо читать подряд. Возвращайтесь к нему за незнакомым словом. [учебным заданиям](../labs/README.md).
Первая статья словаря объясняет хранилище. Следом идет несколько статей об
устройстве генератора; они понадобятся нескоро.
## Карта хранилища ## Карта хранилища
[Интерактивная карта](architecture-map.html) показывает путь данных через слои. [Интерактивная карта](architecture-map.html) показывает путь данных через слои.
Gitea показывает HTML как текст. Откройте файл из своей копии репозитория Gitea показывает HTML как текст. Откройте файл из своей копии репозитория
или скачайте его кнопкой «Raw» и откройте в браузере. или скачайте его кнопкой «Raw» и откройте в браузере.
Держите карту рядом с главой «Хранилище и его слои». Сверяйтесь с картой при чтении главы «Хранилище и его слои».
## Главы ## Главы
### [Хранилище и его слои](storage-layers.md) ### [Хранилище и его слои](storage-layers.md)
От события в Kafka до витрины: глава разбирает путь через STG, ODS, DDS и DM Как меняются данные при переходе из Kafka в слои STG, ODS, DDS и DM.
и показывает, за что отвечает каждый слой. В конце главы вы посмотрите события, сессии и выручку с помощью SQL.
### [Потоки Airflow и даги модельного времени](airflow-dags.md) ### [Потоки Airflow и даги модельного времени](airflow-dags.md)
Шаговый даг играет модельный день, а дневной конвейер раскладывает данные по Какие даги создают новые данные, а какие обрабатывают их в хранилище.
слоям. Глава поможет увидеть их связь и роль каждого дага. Вы запустите генерацию и обработку за день и посмотрите результат в витринах.
### [Мониторинг: куда смотреть](monitoring.md) ### [Мониторинг: куда смотреть](monitoring.md)
Свежесть и поток данных, брак и отставание собраны на дашборде «Данные». Что показывают панели Grafana и как меняются их показания, пока в стенд
Глава научит находить нужную панель и разбирать ее показания. поступают события и заказы.
### [Немного Superset](superset.md) ### [Немного Superset](superset.md)
Глава покажет, как витрина становится графиком и где проходит граница между Как Superset строит графики по витринам и что происходит с вашими отчетами
своими объектами и теми, которые поднимает стенд. при перезапуске стенда.
### [Генератор: откуда берутся данные](generator.md) ### [Генератор: откуда берутся данные](generator.md)
Здесь генератор — источник данных, а не предмет изучения. Глава в общих Откуда берутся события и заказы, почему начальные данные повторяются
чертах объяснит, откуда берутся события и заказы и в каком виде их получает и как сверить число загруженных событий с ожидаемым.
хранилище.
+52 -41
View File
@@ -1,7 +1,8 @@
# Потоки Airflow и даги модельного времени # Потоки Airflow и даги модельного времени
В Airflow два рода дагов. Даги мира создают мир и проигрывают модельные дни, Airflow управляет генерацией данных и их обработкой в хранилище. Это разные
а дневной конвейер раскладывает сыгранный день по слоям хранилища. этапы: сначала в Kafka поступают события и заказы за день работы магазина,
затем по ним рассчитываются данные DDS и витрины DM.
```mermaid ```mermaid
flowchart LR flowchart LR
@@ -14,64 +15,72 @@ flowchart LR
## Два потока входят по-разному ## Два потока входят по-разному
События приходят в топик `hits`. Хранилище разбирает их само и непрерывно: События поступают в топик `hits` и обрабатываются по мере поступления:
материализованные представления переносят строки из STG в ODS без участия материализованные представления переносят строки из STG в ODS без участия
Airflow. Airflow.
Заказы приходят в топик `orders` один раз в модельные сутки, целым слепком. У Заказы поступают в топик `orders` раз в модельные сутки одной выгрузкой.
слепка есть начало и конец, поэтому даг `orders_ingest` забирает его одной Она содержит состояние заказов на конец дня и называется слепком.
порцией. Такой способ приема называется [пакетным](../../CONTEXT.md). Даг `orders_ingest` загружает ее целиком. Такой способ приема называется
[пакетным](../../CONTEXT.md).
## Даги мира ## Даги мира
Два дага с тегом «жизненный цикл мира» отвечают за стартовый мир: В документации миром называется учебный магазин с его посетителями,
товарами и заказами. Время в нем идет отдельно от календарного.
Два дага с тегом «жизненный цикл мира» создают начальные данные:
| Даг | Что делает | | Даг | Что делает |
|---|---| |---|---|
| `world_initialize` | Создает стартовый мир, если его еще нет | | `world_initialize` | Создает начальные данные, если их еще нет |
| `world_recreate` | Стирает данные мира и собирает стартовый мир заново | | `world_recreate` | Удаляет прикладные данные и создает их заново |
Три дага с тегом «модельное время» проигрывают следующие дни. Именно их Для добавления следующих дней есть три дага с тегом «модельное время»:
называют дагами модельного времени.
| Даг | Что делает | | Даг | Что делает |
|---|---| |---|---|
| `world_next_day` | Играет следующие дни пачкой | | `world_next_day` | Генерирует данные за следующие дни без пауз между событиями |
| `world_live_day` | Играет следующий день в темпе модельного времени | | `world_live_day` | Генерирует данные за следующий день постепенно, примерно за 24 минуты |
| `world_live` | Запускает живые дни один за другим, пока снят с паузы | | `world_live` | Запускает `world_live_day` день за днем, пока снят с паузы |
Сколько дней уже сыграно, стенд помнит в переменной Airflow `world_position`: В переменной Airflow `world_position` хранится номер следующего дня для
в ней лежит номер первого несыгранного дня. `world_next_day` и `world_live_day` генерации. Например, при значении `8` генератор уже отправил события
записывают новое значение только после успешно сыгранного дня; оборванный за дни с 0 по 7.
прогон его не меняет.
Сыграв день D, эти два дага отправляют слепок заказов за день D−1 и ждут, пока `world_next_day` и `world_live_day` сначала отправляют события дня D,
`orders_ingest` его примет. Эти связи разобраны в [справочнике затем — слепок заказов за предыдущий день D−1. Они ждут, пока `orders_ingest`
мира](../architecture/world.md#даги-модельного-времени). загрузит слепок, и только после успешного завершения дня обновляют
`world_position`. Если генерация или прием данных завершились с ошибкой,
номер дня остается прежним. Подробности — в [справочнике генератора
и модельного времени](../architecture/world.md#даги-модельного-времени).
## Дневной конвейер ## Дневной конвейер
У `etl_pipeline` нет своей обработки. Он по очереди запускает `dds_transform`, Даг `etl_pipeline` по очереди запускает `dds_transform`, `dm_transform`
`dm_transform` и `dq_check` и ждет каждый даг. Параметров у него нет: сколько и `dq_check`, дожидаясь завершения каждого. Они рассчитывают данные DDS,
дней обработать, он сам определяет по `world_position` и по тому, что уже лежит обновляют витрины DM и проверяют качество результата.
в слоях. Поэтому один и тот же запуск верен и когда его запустил другой даг, и
когда вы нажали кнопку, и когда его повторяют после сбоя. Детали есть в При запуске `etl_pipeline` не нужно указывать даты. Дни для обработки
определяются по `world_position` и по уже загруженным данным в слоях.
Детали есть в
[справочнике конвейера](../architecture/etl.md#объём-прогона). [справочнике конвейера](../architecture/etl.md#объём-прогона).
`world_next_day` не запускает конвейер. После него слои хранилища отстают от После `world_next_day` новые события и заказы уже загружены, но данные DDS
мира, пока вы не запустите конвейер сами. `world_live_day`, наоборот, и DM еще не пересчитаны: запустите `etl_pipeline` вручную.
запускает `etl_pipeline` и ждет его. Поэтому даг остается в работе и после `world_live_day` запускает его автоматически и ждет завершения. Поэтому
конца модельного дня. `world_live_day` может продолжать работу после окончания генерации за день.
## Что значит красный ## Если даг завершился с ошибкой
Красный `etl_pipeline` не отменяет сыгранного дня: `world_position` уже Ошибка в `etl_pipeline` не меняет `world_position`: генерация и прием
записана. Достаточно перезапустить конвейер рукой; разбираться, на чем данных за день уже завершились. После устранения причины ошибки
остановился прошлый прогон, не нужно. Кто запускает конвейер, описано в перезапустите конвейер вручную. Даты для повторной обработки он определит сам.
Кто запускает конвейер, описано в
[разделе о трех путях запуска](../architecture/etl.md#три-пути-запуска). [разделе о трех путях запуска](../architecture/etl.md#три-пути-запуска).
Качество данных проверяет контур DQ. В дневном конвейере это даг `dq_check`: За качество данных отвечают проверки DQ (Data Quality) в даге `dq_check`.
упавшая задача печатает в лог ключи строк, не прошедших проверку. Состав Если проверка находит расхождение, задача завершается с ошибкой и записывает
в журнал ключи строк, которые ее не прошли. Состав
проверок описан в [разделе «Звено DQ проверок описан в [разделе «Звено DQ
конвейера»](../architecture/dm.md#звено-dq-конвейера). конвейера»](../architecture/dm.md#звено-dq-конвейера).
@@ -87,8 +96,9 @@ Airflow.
Откройте Airflow по адресу из раздела [«Адреса и Откройте Airflow по адресу из раздела [«Адреса и
пароли»](../../README.md#адреса-и-пароли). В Admin → Variables найдите пароли»](../../README.md#адреса-и-пароли). В Admin → Variables найдите
`world_position`. Затем запустите `world_next_day` на один день по [инструкции `world_position`. Затем запустите `world_next_day` на один день по [инструкции
из README](../../README.md#сдвинуть-мир-вперед). Убедитесь, что `orders_ingest` из README](../../README.md#добавить-данные-за-следующие-дни). Убедитесь, что
отработал, а витрины еще ничего не получили. Запустите `etl_pipeline` рукой. `orders_ingest` завершился успешно, а в витринах нового дня еще нет.
Запустите `etl_pipeline` вручную и дождитесь его завершения.
В DBeaver, подключенном по В DBeaver, подключенном по
[инструкции](../../README.md#подключиться-из-dbeaver), выполните два запроса: [инструкции](../../README.md#подключиться-из-dbeaver), выполните два запроса:
@@ -106,9 +116,10 @@ ORDER BY report_date DESC
LIMIT 3; LIMIT 3;
``` ```
В трафике уже есть сыгранный день, а в выручке только предыдущий: вместе с этим В витрине трафика уже есть новый день, а в выручке последняя дата —
днем пришел слепок заказов за предыдущий. Выручка сыгранного дня появится после предыдущий день. Причина в выгрузке заказов: вместе с событиями дня D
следующего. приходит слепок за день D−1. Выручка дня D появится после генерации
дня D+1 и запуска конвейера.
## Куда дальше ## Куда дальше
+152 -154
View File
@@ -149,15 +149,14 @@
<script> <script>
const MAP = { const MAP = {
"title": "Карта слоев хранилища", "title": "Карта слоев хранилища",
"lead": "Проследите, как один смысл меняет имя и поверхность между слоями.", "lead": "Проследите путь событий и заказов от Kafka до отчетов.",
"chapter_url": "https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/handbook/storage-layers.md#user-content-хранилище-и-его-слои", "chapter_url": "https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/handbook/storage-layers.md#user-content-хранилище-и-его-слои",
"intro": [ "intro": [
"Эта карта показывает путь событий и заказов от Kafka до витрин DM.", "Выберите объект, чтобы узнать, что он хранит, как его читать и откуда в него поступают данные.",
"Щелкните по узлу, чтобы увидеть его назначение, устройство и ключи.", "Выберите стрелку, чтобы посмотреть способ обработки и открыть SQL-запрос.",
"Щелкните по стрелке, чтобы узнать, кто и каким запросом переносит данные.", "Для выбранного объекта подсвечиваются все пути, по которым данные поступают в него и используются дальше.",
"Когда вы выбираете узел, карта подсвечивает все пути к нему и от него.", "Кнопки «События» и «Заказы» выделяют нужный поток. Остальные объекты остаются на месте, но становятся бледнее.",
"Переключатель потока приглушает другую часть карты, но не меняет раскладку.", "Если вы впервые знакомитесь со слоями хранилища, начните с главы по ссылке ниже."
"Сначала прочитайте главу «Хранилище и его слои», затем вернитесь к карте и рассмотрите объекты."
], ],
"streams": { "streams": {
"all": "Все", "all": "Все",
@@ -166,39 +165,39 @@
}, },
"dictionary": { "dictionary": {
"title": "Словарь", "title": "Словарь",
"caption": "сбоку цепочки: справочники" "caption": "справочные данные"
}, },
"layers": [ "layers": [
{ {
"id": "kafka", "id": "kafka",
"title": "Kafka", "title": "Kafka",
"caption": "откуда приходит" "caption": "сообщения источников"
}, },
{ {
"id": "stg", "id": "stg",
"title": "STG", "title": "STG",
"caption": "как поступило" "caption": "исходные сообщения"
}, },
{ {
"id": "ods", "id": "ods",
"title": "ODS", "title": "ODS",
"caption": "язык источника" "caption": "данные разобраны по полям"
}, },
{ {
"id": "dds", "id": "dds",
"title": "DDS", "title": "DDS",
"caption": "язык бизнеса" "caption": "события, заказы и сессии"
}, },
{ {
"id": "dm", "id": "dm",
"title": "DM", "title": "DM",
"caption": "данные для дашбордов" "caption": "витрины для отчетов"
} }
], ],
"mechanisms": [ "mechanisms": [
{ {
"id": "materialized_view", "id": "materialized_view",
"label": "матвью", "label": "материализованное представление",
"line": "solid", "line": "solid",
"rhythm": "непрерывно, по мере прихода сообщений" "rhythm": "непрерывно, по мере прихода сообщений"
}, },
@@ -206,13 +205,13 @@
"id": "airflow_dag", "id": "airflow_dag",
"label": "даг Airflow", "label": "даг Airflow",
"line": "dashed", "line": "dashed",
"rhythm": "пачкой: раз в модельные сутки (`orders_ingest`) или на каждом прогоне конвейера" "rhythm": "пакетно: раз в модельные сутки (`orders_ingest`) или при каждом запуске конвейера"
}, },
{ {
"id": "view", "id": "view",
"label": "представление", "label": "представление",
"line": "dotted", "line": "dotted",
"rhythm": "без копии, данные читаются на месте" "rhythm": "при запросе к представлению, без копирования данных"
} }
], ],
"nodes": [ "nodes": [
@@ -225,9 +224,9 @@
"events" "events"
], ],
"what": "Топик Kafka с событиями кликстрима. Каждое сообщение содержит одно событие.", "what": "Топик Kafka с событиями кликстрима. Каждое сообщение содержит одно событие.",
"read": "Топик напрямую не читают: таблица `stg.hits_raw_kafka` забирает сообщения из топика. Для данных событий используйте `stg.hits_raw_dist`.", "read": "Для просмотра загруженных сообщений используйте `stg.hits_raw_dist`. Таблица `stg.hits_raw_kafka` служит для приема из топика.",
"fill": [ "fill": [
"Источник событий пишет сообщения в топик в темпе выбранного режима мира." "Генератор отправляет события без пауз в пакетном режиме или постепенно в живом режиме."
], ],
"links": [ "links": [
{ {
@@ -248,10 +247,10 @@
"streams": [ "streams": [
"orders" "orders"
], ],
"what": "Топик Kafka со слепками заказов магазина. После модельного дня приходит снимок изменяемого окна.", "what": "Топик Kafka с дневными выгрузками заказов — слепками. Каждый слепок содержит состояние заказов за последние семь модельных дней.",
"read": "Топик напрямую не читают: таблица `stg.orders_raw_kafka` забирает сообщения из топика. Для заказов используйте `stg.orders_raw_dist`.", "read": "Для просмотра загруженных сообщений используйте `stg.orders_raw_dist`. Таблица `stg.orders_raw_kafka` служит для приема из топика.",
"fill": [ "fill": [
"Источник заказов публикует слепок пачкой после каждого модельного дня." "После генерации событий дня D источник отправляет слепок заказов за предыдущий день D−1."
], ],
"links": [ "links": [
{ {
@@ -272,17 +271,17 @@
"streams": [ "streams": [
"events" "events"
], ],
"what": "Сырая строка события и метаданные доставки. STG сохраняет сообщение без разбора.", "what": "Исходное сообщение события и сведения о его доставке из Kafka. Содержимое сохраняется целой строкой, без разбора на поля.",
"read": "Читайте `_dist`: у STG нет представления `_v`, а распределенная таблица показывает оба шарда.", "read": "Читайте `_dist`: у STG нет представления `_v`, а распределенная таблица показывает оба шарда.",
"parts": [ "parts": [
"`stg.hits_raw_kafka` — чтец топика `hits`.", "`stg.hits_raw_kafka` — таблица для чтения сообщений топика `hits`.",
"`stg.hits_raw_rep` — реплицируемая таблица на шарде.", "`stg.hits_raw_rep` — реплицируемая таблица на шарде.",
"`stg.hits_raw_dist` — распределенная поверхность записи и чтения.", "`stg.hits_raw_dist` — распределенная таблица для записи и чтения.",
"`stg.hits_raw_mv` — матвью приема сообщений из чтеца." "`stg.hits_raw_mv` — материализованное представление, которое записывает сообщения из Kafka в STG."
], ],
"fill": [ "fill": [
"`stg.hits_raw_mv` пишет непрерывно, по мере прихода сообщений.", "`stg.hits_raw_mv` пишет непрерывно, по мере прихода сообщений.",
"Сырые строки живут трое суток реального времени плюс хвост до суток." "Строки хранятся не менее трех суток от загрузки. Удаление целыми частями таблицы добавляет до суток к этому сроку; оно выполняется при очередном слиянии по TTL."
], ],
"links": [ "links": [
{ {
@@ -294,7 +293,7 @@
"url": "https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/10-stg-tables.sql" "url": "https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/10-stg-tables.sql"
}, },
{ {
"label": "DDL матвью", "label": "DDL материализованного представления",
"url": "https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/40-stg-views.sql" "url": "https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/40-stg-views.sql"
} }
] ]
@@ -307,16 +306,16 @@
"streams": [ "streams": [
"orders" "orders"
], ],
"what": "Сырые строки слепка заказов, метаданные доставки и идентификатор пачки `_load_id`.", "what": "Исходные строки выгрузки заказов, сведения о доставке и идентификатор загруженной партии `_load_id`.",
"read": "Читайте `_dist`: у STG нет представления `_v`, а распределенная таблица показывает оба шарда.", "read": "Читайте `_dist`: у STG нет представления `_v`, а распределенная таблица показывает оба шарда.",
"parts": [ "parts": [
"`stg.orders_raw_kafka` — чтец топика `orders` на первой ноде.", "`stg.orders_raw_kafka` — таблица для чтения топика `orders` на первом узле.",
"`stg.orders_raw_rep` — реплицируемая таблица на шарде.", "`stg.orders_raw_rep` — реплицируемая таблица на шарде.",
"`stg.orders_raw_dist` — распределенная поверхность записи и чтения." "`stg.orders_raw_dist` — распределенная таблица для записи и чтения."
], ],
"fill": [ "fill": [
"Задача `pull_batch` дага `orders_ingest` забирает слепок пачкой раз в модельные сутки.", "Задача `pull_batch` дага `orders_ingest` загружает слепок раз в модельные сутки.",
"Сырые строки живут трое суток реального времени плюс хвост до суток." "Строки хранятся не менее трех суток от загрузки. Удаление целыми частями таблицы добавляет до суток к этому сроку; оно выполняется при очередном слиянии по TTL."
], ],
"links": [ "links": [
{ {
@@ -337,13 +336,13 @@
"streams": [ "streams": [
"events" "events"
], ],
"what": "Типизированное широкое событие с именами полей источника. Запрос представления с `FINAL` возвращает актуальную версию события.", "what": "События, разобранные на колонки с типами данных. Имена полей сохранены как в источнике.",
"read": "Читайте `ods.event_v`: запрос представления с `FINAL` возвращает актуальные строки вместо физических версий.", "read": "Читайте `ods.event_v`: представление использует `FINAL` и возвращает последнюю версию каждого события.",
"parts": [ "parts": [
"`ods.event_rep` — реплицируемая таблица версий события на шарде.", "`ods.event_rep` — реплицируемая таблица версий события на шарде.",
"`ods.event_dist` — распределенная поверхность записи и диагностики.", "`ods.event_dist` — распределенная таблица записи и диагностики.",
"`ods.event_v` — представление актуального состояния с `FINAL`.", "`ods.event_v` — представление актуального состояния с `FINAL`.",
"`ods.event_mv` — матвью разбора годных строк STG." "`ods.event_mv` — материализованное представление для разбора сообщений STG без ошибок."
], ],
"keys": [ "keys": [
"`WatchID` — тот же идентификатор, что `event_id` в DDS.", "`WatchID` — тот же идентификатор, что `event_id` в DDS.",
@@ -377,16 +376,16 @@
"streams": [ "streams": [
"events" "events"
], ],
"what": "Сырые события, которые не прошли строгий прием, с классом ошибки и метаданными доставки.", "what": "Сообщения событий, которые не удалось разобрать по правилам ODS. Сохранены исходная строка, класс ошибки и сведения о доставке.",
"read": "Читайте `_dist`: представления `_v` нет, а распределенная таблица показывает ошибки с обоих шардов.", "read": "Читайте `_dist`: представления `_v` нет, а распределенная таблица показывает ошибки с обоих шардов.",
"parts": [ "parts": [
"`ods.event_errors_rep` — реплицируемая таблица ошибок на шарде.", "`ods.event_errors_rep` — реплицируемая таблица ошибок на шарде.",
"`ods.event_errors_dist` — распределенная поверхность чтения и записи.", "`ods.event_errors_dist` — распределенная таблица чтения и записи.",
"`ods.event_errors_mv` — матвью, которое отбирает негодные строки STG." "`ods.event_errors_mv` — материализованное представление для отбора сообщений STG с ошибками."
], ],
"fill": [ "fill": [
"`ods.event_errors_mv` пишет непрерывно, по мере записи в STG.", "`ods.event_errors_mv` пишет непрерывно, по мере записи в STG.",
"Ошибки живут один месяц и не останавливают прием годных событий." "Ошибки хранятся один месяц. Остальные события продолжают загружаться."
], ],
"links": [ "links": [
{ {
@@ -398,7 +397,7 @@
"url": "https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/20-ods-tables.sql" "url": "https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/20-ods-tables.sql"
}, },
{ {
"label": "DDL матвью", "label": "DDL материализованного представления",
"url": "https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/30-ods-views.sql" "url": "https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/30-ods-views.sql"
} }
] ]
@@ -411,11 +410,11 @@
"streams": [ "streams": [
"orders" "orders"
], ],
"what": "Текущая версия заказа на языке источника. Позиции пока остаются сырым фрагментом JSON.", "what": "Последняя загруженная версия заказа с именами полей из источника. Состав заказа пока хранится как фрагмент JSON.",
"read": "Читайте `ods.order_v`: запрос представления с `FINAL` возвращает последнюю версию по `snapshot_date`.", "read": "Читайте `ods.order_v`: запрос представления с `FINAL` возвращает последнюю версию по `snapshot_date`.",
"parts": [ "parts": [
"`ods.order_rep` — реплицируемая таблица версий заказа на шарде.", "`ods.order_rep` — реплицируемая таблица версий заказа на шарде.",
"`ods.order_dist` — распределенная поверхность записи и диагностики.", "`ods.order_dist` — распределенная таблица записи и диагностики.",
"`ods.order_v` — представление текущего состояния с `FINAL`." "`ods.order_v` — представление текущего состояния с `FINAL`."
], ],
"keys": [ "keys": [
@@ -423,7 +422,7 @@
"`user_id` — пользователь магазина, который переходит в заказ DDS." "`user_id` — пользователь магазина, который переходит в заказ DDS."
], ],
"fill": [ "fill": [
"Задача `parse_batch` дага `orders_ingest` разбирает пачку раз в модельные сутки.", "Задача `parse_batch` дага `orders_ingest` разбирает загруженный слепок раз в модельные сутки.",
"`ReplacingMergeTree(snapshot_date)` оставляет версию из последнего принятого слепка." "`ReplacingMergeTree(snapshot_date)` оставляет версию из последнего принятого слепка."
], ],
"links": [ "links": [
@@ -449,15 +448,15 @@
"streams": [ "streams": [
"orders" "orders"
], ],
"what": "Сырые строки слепка, которые не прошли строгий прием, с классом ошибки и `_load_id`.", "what": "Строки выгрузки заказов, которые не удалось разобрать по правилам ODS. Сохранены исходная строка, класс ошибки и `_load_id`.",
"read": "Читайте `_dist`: представления `_v` нет, а распределенная таблица показывает ошибки с обоих шардов.", "read": "Читайте `_dist`: представления `_v` нет, а распределенная таблица показывает ошибки с обоих шардов.",
"parts": [ "parts": [
"`ods.order_errors_rep` — реплицируемая таблица ошибок на шарде.", "`ods.order_errors_rep` — реплицируемая таблица ошибок на шарде.",
"`ods.order_errors_dist` — распределенная поверхность чтения и записи." "`ods.order_errors_dist` — распределенная таблица чтения и записи."
], ],
"fill": [ "fill": [
"Задача `parse_batch` дага `orders_ingest` пишет ошибки из той же пачки, что и годные заказы.", "Задача `parse_batch` дага `orders_ingest` сохраняет строки с ошибками при разборе той же выгрузки, из которой загружаются заказы.",
"Ошибки живут один месяц и не останавливают прием годных заказов." "Ошибки хранятся один месяц. Остальные заказы продолжают загружаться."
], ],
"links": [ "links": [
{ {
@@ -478,8 +477,8 @@
"streams": [ "streams": [
"events" "events"
], ],
"what": "Публичное представление события на языке бизнеса. Его запрос переименовывает поля и расшифровывает категорию устройства.", "what": "События с названиями полей, принятыми в DDS. Например, `WatchID` переименован в `event_id`, а числовой код устройства заменен названием категории.",
"read": "Читайте `dds.event_v`: запрос представления возвращает явный список колонок DDS.", "read": "Читайте `dds.event_v`: представление возвращает явно перечисленные колонки DDS.",
"parts": [ "parts": [
"`dds.event_v` — только представление над `ods.event_v`; своей таблицы в DDS нет." "`dds.event_v` — только представление над `ods.event_v`; своей таблицы в DDS нет."
], ],
@@ -490,7 +489,7 @@
"`purchase_id[1]` — тот же заказ, что `order_id` в DDS и DM." "`purchase_id[1]` — тот же заказ, что `order_id` в DDS и DM."
], ],
"fill": [ "fill": [
"Данные для `dds.event_v` читаются из актуальных событий ODS без копии и отдельного расписания." "При запросе к `dds.event_v` данные читаются из актуальных событий ODS. Отдельная загрузка в DDS не нужна."
], ],
"links": [ "links": [
{ {
@@ -511,22 +510,22 @@
"streams": [ "streams": [
"events" "events"
], ],
"what": "Сессия, которую запрос дага `dds_transform` собирает из последовательных событий одной куки. В источнике готовой сессии нет.", "what": "Сессии, рассчитанные дагом `dds_transform` по событиям посетителя. Новая сессия начинается после паузы дольше 30 минут или при смене модельной даты.",
"read": "Читайте `dds.session_v`: запрос представления возвращает деловые и служебные колонки.", "read": "Читайте `dds.session_v`: здесь есть начало, конец, длительность сессии, показатели событий и служебные поля загрузки.",
"parts": [ "parts": [
"`dds.session_rep` — реплицируемая таблица сессий на шарде.", "`dds.session_rep` — реплицируемая таблица сессий на шарде.",
"`dds.session_dist` — распределенная поверхность загрузки и диагностики.", "`dds.session_dist` — распределенная таблица загрузки и диагностики.",
"`dds.session_stage_rep` — донор дневных партиций на шарде.", "`dds.session_stage_rep` — промежуточная таблица для пересчета данных по дням на шарде.",
"`dds.session_stage_dist` — распределенная поверхность донора.", "`dds.session_stage_dist` — распределенная таблица для записи и чтения промежуточных данных.",
"`dds.session_v` — публичное представление." "`dds.session_v` — представление для чтения готовых данных."
], ],
"keys": [ "keys": [
"`client_id` — тот же посетитель, что в событии DDS и карте идентичностей.", "`client_id` — тот же посетитель, что в событии DDS и карте идентичностей.",
"`session_date` — день, который в `dm.daily_traffic_v` называется `report_date`." "`session_date` — день, который в `dm.daily_traffic_v` называется `report_date`."
], ],
"fill": [ "fill": [
"Даг `dds_transform` пересобирает данные прожитых дней при каждом прогоне конвейера.", "При каждом запуске `dds_transform` выбирает дни от первого пропущенного в DDS до последнего дня с завершенной генерацией.",
"Задачи `session.rebuild` и `session.replace` собирают день в доноре и заменяют партицию." "Задача `session.rebuild` рассчитывает данные в промежуточной таблице (доноре). `session.replace` заменяет ими дневные партиции основной таблицы."
], ],
"links": [ "links": [
{ {
@@ -551,14 +550,14 @@
"streams": [ "streams": [
"orders" "orders"
], ],
"what": "Заказ в текущем состоянии на языке бизнеса. Артикулы, количества и цены позиций хранятся в согласованных массивах.", "what": "Последнее известное состояние заказа, подготовленное для анализа. Артикулы, количества и цены товаров хранятся в массивах; элементы с одинаковым индексом относятся к одной позиции заказа.",
"read": "Читайте `dds.order_v`: запрос представления возвращает данные без таблиц и донора партиций.", "read": "Читайте `dds.order_v`, чтобы получить готовые заказы из основной таблицы.",
"parts": [ "parts": [
"`dds.order_rep` — реплицируемая таблица заказов на шарде.", "`dds.order_rep` — реплицируемая таблица заказов на шарде.",
"`dds.order_dist` — распределенная поверхность загрузки и диагностики.", "`dds.order_dist` — распределенная таблица загрузки и диагностики.",
"`dds.order_stage_rep` — донор дневных партиций на шарде.", "`dds.order_stage_rep` — промежуточная таблица для пересчета данных по дням на шарде.",
"`dds.order_stage_dist` — распределенная поверхность донора.", "`dds.order_stage_dist` — распределенная таблица для записи и чтения промежуточных данных.",
"`dds.order_v` — публичное представление." "`dds.order_v` — представление для чтения готовых данных."
], ],
"keys": [ "keys": [
"`order_id` — тот же заказ, что в ODS, событии покупки и витрине сверки.", "`order_id` — тот же заказ, что в ODS, событии покупки и витрине сверки.",
@@ -567,8 +566,8 @@
"`order_date` — день, который в витринах называется `report_date` или `order_day`." "`order_date` — день, который в витринах называется `report_date` или `order_day`."
], ],
"fill": [ "fill": [
"Даг `dds_transform` пересобирает изменяемое окно при каждом прогоне конвейера.", "При каждом запуске `dds_transform` пересчитывает дни заказов из последнего слепка и заполняет пропуски в DDS.",
"Задачи `order.rebuild` и `order.replace` собирают дни в доноре и заменяют партиции." "Задача `order.rebuild` рассчитывает данные в промежуточной таблице (доноре). `order.replace` заменяет ими дневные партиции основной таблицы."
], ],
"links": [ "links": [
{ {
@@ -594,20 +593,20 @@
"events", "events",
"orders" "orders"
], ],
"what": "Известные пары куки трекера и пользователя магазина. Связь появляется после сопоставленной покупки.", "what": "Связи между посетителем сайта и пользователем магазина. Они появляются, когда событие покупки сопоставлено с заказом по его номеру.",
"read": "Читайте `dds.identity_map_v`: запрос представления с `FINAL` возвращает актуальные строки полного пересчета.", "read": "Читайте `dds.identity_map_v`: представление с `FINAL` возвращает последнюю версию каждой пары посетителя и пользователя.",
"parts": [ "parts": [
"`dds.identity_map_rep` — реплицируемая таблица версий пар на шарде.", "`dds.identity_map_rep` — реплицируемая таблица версий пар на шарде.",
"`dds.identity_map_dist` — распределенная поверхность записи.", "`dds.identity_map_dist` — распределенная таблица записи.",
"`dds.identity_map_v` — точное публичное представление с `FINAL`." "`dds.identity_map_v` — представление последних версий строк с `FINAL`."
], ],
"keys": [ "keys": [
"`client_id` — кука из события и сессии DDS.", "`client_id` — идентификатор посетителя из cookie браузера, как в событии и сессии DDS.",
"`user_id` — пользователь магазина из заказа DDS." "`user_id` — пользователь магазина из заказа DDS."
], ],
"fill": [ "fill": [
"Задача `identity_map` дага `dds_transform` пересчитывает все известные пары после обновления заказов.", "Задача `identity_map` дага `dds_transform` пересчитывает все известные пары после обновления заказов.",
"Новая версия полного набора пишется при каждом прогоне; `FINAL` скрывает физические повторы." "При каждом запуске записывается новая версия набора связей."
], ],
"links": [ "links": [
{ {
@@ -632,19 +631,18 @@
"streams": [ "streams": [
"events" "events"
], ],
"what": "Дневные счетчики посетителей и известных пользователей после склейки идентичностей.", "what": "Число посетителей и известных пользователей за каждый день. Известными считаются пользователи магазина, связанные с посетителями сайта через карту идентичностей.",
"read": "Читайте `dm.daily_traffic_v`: запрос представления с `FINAL` возвращает актуальные строки полного пересчета.", "read": "Читайте `dm.daily_traffic_v`: представление с `FINAL` возвращает последний результат расчета за каждый день.",
"parts": [ "parts": [
"`dm.daily_traffic_rep` — реплицируемая таблица версий счетчиков.", "`dm.daily_traffic_rep` — реплицируемая таблица версий счетчиков.",
"`dm.daily_traffic_dist` — распределенная поверхность записи.", "`dm.daily_traffic_dist` — распределенная таблица записи.",
"`dm.daily_traffic_v` — точное публичное представление с `FINAL`." "`dm.daily_traffic_v` — представление последних версий строк с `FINAL`."
], ],
"keys": [ "keys": [
"`report_date` — тот же день, что `session_date` в сессии DDS." "`report_date` — тот же день, что `session_date` в сессии DDS."
], ],
"fill": [ "fill": [
"Задача `daily_traffic` дага `dm_transform` при каждом прогоне пересчитывает всю историю и записывает новую версию.", "Задача `daily_traffic` дага `dm_transform` при каждом запуске пересчитывает всю историю и записывает новую версию."
"`FINAL` оставляет текущую строку каждого дня."
], ],
"links": [ "links": [
{ {
@@ -669,22 +667,22 @@
"streams": [ "streams": [
"orders" "orders"
], ],
"what": "Дневная выручка, число заказов и единиц товара по категориям.", "what": "Выручка, число заказов и единиц товара за день по категориям. В расчет входят оплаченные заказы.",
"read": "Читайте `dm.revenue_daily_v`: это публичное представление готовой дневной витрины.", "read": "Читайте `dm.revenue_daily_v`, чтобы получить готовые показатели по дням и категориям товаров.",
"parts": [ "parts": [
"`dm.revenue_daily_rep` — реплицируемая таблица витрины на шарде.", "`dm.revenue_daily_rep` — реплицируемая таблица витрины на шарде.",
"`dm.revenue_daily_dist` — распределенная поверхность загрузки.", "`dm.revenue_daily_dist` — распределенная таблица загрузки.",
"`dm.revenue_daily_stage_rep` — донор дневных партиций на шарде.", "`dm.revenue_daily_stage_rep` — промежуточная таблица для пересчета данных по дням на шарде.",
"`dm.revenue_daily_stage_dist` — распределенная поверхность донора.", "`dm.revenue_daily_stage_dist` — распределенная таблица для записи и чтения промежуточных данных.",
"`dm.revenue_daily_v` — публичное представление." "`dm.revenue_daily_v` — представление для чтения готовых данных."
], ],
"keys": [ "keys": [
"`report_date` — тот же день, что `order_date` в заказе DDS.", "`report_date` — тот же день, что `order_date` в заказе DDS.",
"`product_category` — категория, найденная по `item_sku` заказа и ключу `sku` словаря." "`product_category` — категория, найденная по `item_sku` заказа и ключу `sku` словаря."
], ],
"fill": [ "fill": [
"Даг `dm_transform` пересобирает изменяемое окно заказов при каждом прогоне конвейера.", "При каждом запуске `dm_transform` пересчитывает дни заказов из последнего слепка и заполняет пропущенные дни витрины.",
"Задачи `revenue_daily.rebuild` и `revenue_daily.replace` собирают дни в доноре и заменяют партиции." "Задача `revenue_daily.rebuild` рассчитывает данные в промежуточной таблице (доноре). `revenue_daily.replace` заменяет ими дневные партиции витрины."
], ],
"links": [ "links": [
{ {
@@ -710,22 +708,22 @@
"events", "events",
"orders" "orders"
], ],
"what": "Сверка события покупки с заказом магазина. Одна строка показывает совпадение или класс расхождения.", "what": "Результат сверки покупок из кликстрима с заказами магазина. Для каждого номера заказа указано, совпали ли данные двух источников и какое расхождение найдено.",
"read": "Читайте `dm.purchase_vs_orders_v`: в результате запроса отсутствующей стороне соответствует `NULL`.", "read": "Читайте `dm.purchase_vs_orders_v`: если покупка или заказ отсутствует, поля этой стороны содержат `NULL`.",
"parts": [ "parts": [
"`dm.purchase_vs_orders_rep` — реплицируемая таблица сверки на шарде.", "`dm.purchase_vs_orders_rep` — реплицируемая таблица сверки на шарде.",
"`dm.purchase_vs_orders_dist` — распределенная поверхность загрузки.", "`dm.purchase_vs_orders_dist` — распределенная таблица загрузки.",
"`dm.purchase_vs_orders_stage_rep` — донор дневных партиций на шарде.", "`dm.purchase_vs_orders_stage_rep` — промежуточная таблица для пересчета данных по дням на шарде.",
"`dm.purchase_vs_orders_stage_dist` — распределенная поверхность донора.", "`dm.purchase_vs_orders_stage_dist` — распределенная таблица для записи и чтения промежуточных данных.",
"`dm.purchase_vs_orders_v` — публичное представление." "`dm.purchase_vs_orders_v` — представление для чтения готовых данных."
], ],
"keys": [ "keys": [
"`order_id` — тот же заказ, что `purchase_id[1]` в событии и `order_id` в заказе DDS.", "`order_id` — тот же заказ, что `purchase_id[1]` в событии и `order_id` в заказе DDS.",
"`order_day` — общий день события покупки и заказа." "`order_day` — дата заказа; если заказа еще нет, берется дата события покупки."
], ],
"fill": [ "fill": [
"Даг `dm_transform` пересобирает изменяемый хвост обоих источников при каждом прогоне конвейера.", "При каждом запуске `dm_transform` пересчитывает дни последнего слепка, добавляет новые дни покупок, заполняет пропуски и повторяет сверку дней с покупками без заказа.",
"Задачи `purchase_vs_orders.rebuild` и `purchase_vs_orders.replace` собирают дни в доноре и заменяют партиции." "Задача `purchase_vs_orders.rebuild` рассчитывает данные в промежуточной таблице (доноре). `purchase_vs_orders.replace` заменяет ими дневные партиции витрины."
], ],
"links": [ "links": [
{ {
@@ -751,19 +749,19 @@
"events", "events",
"orders" "orders"
], ],
"what": "Текущее состояние пяти атомарных проверок качества по дням. История запусков остается в Airflow.", "what": "Последние результаты пяти проверок качества данных за каждый день. Историю запусков можно посмотреть в Airflow.",
"read": "Читайте `dm.dq_summary_v`: запрос к публичному представлению возвращает последнюю опубликованную сводку дня.", "read": "Читайте `dm.dq_summary_v`, чтобы увидеть результат каждой проверки за день.",
"parts": [ "parts": [
"`dm.dq_summary_rep` — реплицируемая таблица опубликованной сводки.", "`dm.dq_summary_rep` — реплицируемая таблица опубликованной сводки.",
"`dm.dq_summary_dist` — распределенная поверхность чтения и записи.", "`dm.dq_summary_dist` — распределенная таблица чтения и записи.",
"`dm.dq_summary_stage_rep` — общий донор дневных партиций.", "`dm.dq_summary_stage_rep` — промежуточная таблица для результатов всех проверок за день.",
"`dm.dq_summary_stage_dist` — распределенная поверхность донора.", "`dm.dq_summary_stage_dist` — распределенная таблица для записи и чтения промежуточных данных.",
"`dm.dq_summary_v` — публичное представление." "`dm.dq_summary_v` — представление для чтения готовых данных."
], ],
"fill": [ "fill": [
"Даг `dq_check` при каждом прогоне сначала выполняет все пять проверок, затем заменяет готовые дневные партиции.", "Даг `dq_check` сначала рассчитывает результаты всех пяти проверок в промежуточной таблице, затем заменяет ими дневные партиции сводки.",
"`sessions_vs_reference` — `sql/dq/sessions_vs_reference.sql`.", "`sessions_vs_reference` — `sql/dq/sessions_vs_reference.sql`.",
"`classes_vs_inventory` — `sql/dq/classes_vs_inventory.sql` и паспорт мира.", "`classes_vs_inventory` — `sql/dq/classes_vs_inventory.sql` и эталонные числа из паспорта мира `data/world-inventory.json`.",
"`revenue_daily_vs_dds` — `sql/dq/revenue_daily_vs_dds.sql`.", "`revenue_daily_vs_dds` — `sql/dq/revenue_daily_vs_dds.sql`.",
"`purchase_vs_orders_vs_dds` — `sql/dq/purchase_vs_orders_vs_dds.sql`.", "`purchase_vs_orders_vs_dds` — `sql/dq/purchase_vs_orders_vs_dds.sql`.",
"`daily_traffic_vs_dds` — `sql/dq/daily_traffic_vs_dds.sql`." "`daily_traffic_vs_dds` — `sql/dq/daily_traffic_vs_dds.sql`."
@@ -791,17 +789,17 @@
"streams": [ "streams": [
"orders" "orders"
], ],
"what": "Словарь товаров вне цепочки слоев. По артикулу доступны название, бренд, категория и цена.", "what": "Словарь товаров. По артикулу можно получить название, бренд, категорию и цену. Эти данные доступны для расчетов в разных слоях.",
"read": "Читайте `dic.products`: у словаря одна поверхность без суффикса.", "read": "Читайте `dic.products`: к словарю обращаются напрямую, суффикса `_v` у него нет.",
"parts": [ "parts": [
"`dic.products_file` — чтец CSV-каталога из файла репозитория.", "`dic.products_file` — таблица, которая читает каталог товаров из CSV-файла репозитория.",
"`dic.products` — словарь в памяти поверх подложки." "`dic.products` — словарь в памяти, загруженный из этой таблицы."
], ],
"keys": [ "keys": [
"`sku` — тот же артикул, что `item_sku` в позиции заказа DDS." "`sku` — тот же артикул, что `item_sku` в позиции заказа DDS."
], ],
"fill": [ "fill": [
"ClickHouse перечитывает подложку каждые 60–90 секунд по настройке `LIFETIME`." "ClickHouse обновляет словарь из `dic.products_file` каждые 60–90 секунд согласно настройке `LIFETIME`."
], ],
"links": [ "links": [
{ {
@@ -825,8 +823,8 @@
"events" "events"
], ],
"rhythm": "непрерывно, по мере прихода сообщений", "rhythm": "непрерывно, по мере прихода сообщений",
"actor": "Чтец `stg.hits_raw_kafka` и матвью `stg.hits_raw_mv`.", "actor": "Таблица `stg.hits_raw_kafka` и материализованное представление `stg.hits_raw_mv`.",
"join": "Соединения нет: сообщение кладется строкой целиком.", "join": "Каждое сообщение Kafka записывается целой строкой, без разбора на поля.",
"links": [ "links": [
{ {
"label": "SQL", "label": "SQL",
@@ -847,8 +845,8 @@
"events" "events"
], ],
"rhythm": "непрерывно, по мере прихода сообщений", "rhythm": "непрерывно, по мере прихода сообщений",
"actor": "Матвью `ods.event_mv` разбирает годные строки.", "actor": "Материализованное представление `ods.event_mv` разбирает сообщения без ошибок.",
"join": "Соединения нет: матвью разбирает строку в колонки.", "join": "Материализованное представление разбирает сообщение из STG на колонки с нужными типами данных.",
"links": [ "links": [
{ {
"label": "SQL", "label": "SQL",
@@ -869,8 +867,8 @@
"events" "events"
], ],
"rhythm": "непрерывно, по мере прихода сообщений", "rhythm": "непрерывно, по мере прихода сообщений",
"actor": "Матвью `ods.event_errors_mv` отбирает строки, которые не прошли строгий прием.", "actor": "Материализованное представление `ods.event_errors_mv` отбирает сообщения с ошибками.",
"join": "Соединения нет: негодная строка уходит целиком вместе с классом ошибки.", "join": "Сообщение, которое не удалось разобрать по правилам ODS, сохраняется целиком вместе с классом ошибки.",
"links": [ "links": [
{ {
"label": "SQL", "label": "SQL",
@@ -890,9 +888,9 @@
"streams": [ "streams": [
"orders" "orders"
], ],
"rhythm": "пачкой, раз в модельные сутки", "rhythm": "одной выгрузкой, раз в модельные сутки",
"actor": "Задача `pull_batch` дага `orders_ingest` читает `stg.orders_raw_kafka`.", "actor": "Задача `pull_batch` дага `orders_ingest` читает `stg.orders_raw_kafka`.",
"join": "Соединения нет: сообщение слепка кладется целиком, пачка помечена `_load_id`.", "join": "Строки выгрузки сохраняются целиком. Общий `_load_id` позволяет найти все строки одной загрузки.",
"links": [ "links": [
{ {
"label": "SQL", "label": "SQL",
@@ -912,9 +910,9 @@
"streams": [ "streams": [
"orders" "orders"
], ],
"rhythm": "пачкой, раз в модельные сутки", "rhythm": "одной выгрузкой, раз в модельные сутки",
"actor": "Задача `parse_batch` дага `orders_ingest` пишет годные версии заказа.", "actor": "Задача `parse_batch` дага `orders_ingest` записывает версии заказов без ошибок разбора.",
"join": "Соединения нет: строка разбирается в колонки, версия задается `snapshot_date`.", "join": "Строка разбирается на колонки заказа. Версия определяется датой слепка `snapshot_date`.",
"links": [ "links": [
{ {
"label": "SQL", "label": "SQL",
@@ -934,9 +932,9 @@
"streams": [ "streams": [
"orders" "orders"
], ],
"rhythm": "пачкой, раз в модельные сутки", "rhythm": "одной выгрузкой, раз в модельные сутки",
"actor": "Задача `parse_batch` дага `orders_ingest` пишет негодные строки той же пачки.", "actor": "Задача `parse_batch` дага `orders_ingest` записывает строки с ошибками из загруженной выгрузки.",
"join": "Соединения нет: негодная строка пачки уходит целиком.", "join": "Строка с ошибкой сохраняется целиком вместе с классом ошибки и идентификатором загрузки.",
"links": [ "links": [
{ {
"label": "SQL", "label": "SQL",
@@ -956,9 +954,9 @@
"streams": [ "streams": [
"events" "events"
], ],
"rhythm": "без копии, данные читаются на месте", "rhythm": "при запросе к представлению, без копирования данных",
"actor": "Запрос `dds.event_v` читает данные из `ods.event_v` и переименовывает поля на языке бизнеса.", "actor": "Представление `dds.event_v` читает данные из `ods.event_v`.",
"join": "Соединения нет: представление переименовывает колонки той же строки.", "join": "Колонки события переименовываются по правилам DDS. Данные не копируются в отдельную таблицу.",
"links": [ "links": [
{ {
"label": "SQL", "label": "SQL",
@@ -978,9 +976,9 @@
"streams": [ "streams": [
"events" "events"
], ],
"rhythm": "пачкой, при каждом прогоне `dds_transform`", "rhythm": "при каждом запуске `dds_transform`",
"actor": "Задачи `session.rebuild` и `session.replace` дага `dds_transform`.", "actor": "Задачи `session.rebuild` и `session.replace` дага `dds_transform`.",
"join": "События одной `client_id` за `event_date` режутся по паузам; `session_id` - `cityHash64(client_id, min(event_time))`.", "join": "События одного `client_id` за день `event_date` упорядочиваются по времени. Пауза дольше 30 минут начинает новую сессию. Ее `session_id` рассчитывается как `cityHash64(client_id, min(event_time))`.",
"links": [ "links": [
{ {
"label": "SQL сборки", "label": "SQL сборки",
@@ -1004,9 +1002,9 @@
"streams": [ "streams": [
"orders" "orders"
], ],
"rhythm": "пачкой, при каждом прогоне `dds_transform`", "rhythm": "при каждом запуске `dds_transform`",
"actor": "Задачи `order.rebuild` и `order.replace` дага `dds_transform`.", "actor": "Задачи `order.rebuild` и `order.replace` дага `dds_transform`.",
"join": "Соединения нет: из заказа берутся поля и позиции, `order_date` выводится из `created_at`.", "join": "Поля заказа приводятся к типам DDS, а позиции из JSON разбираются в массивы. Дата `order_date` рассчитывается из `created_at`.",
"links": [ "links": [
{ {
"label": "SQL сборки", "label": "SQL сборки",
@@ -1030,9 +1028,9 @@
"streams": [ "streams": [
"events" "events"
], ],
"rhythm": "пачкой, при каждом прогоне `dds_transform`", "rhythm": "при каждом запуске `dds_transform`",
"actor": "Задача `identity_map` дага `dds_transform` соединяет покупки с заказами.", "actor": "Задача `identity_map` дага `dds_transform` соединяет покупки с заказами.",
"join": "Строки покупки и заказа соединяются по `purchase_id[1] = order_id`.", "join": "Событие покупки соединяется с заказом по `purchase_id[1] = order_id`. В результате связываются посетитель сайта и пользователь магазина.",
"links": [ "links": [
{ {
"label": "SQL", "label": "SQL",
@@ -1052,9 +1050,9 @@
"streams": [ "streams": [
"orders" "orders"
], ],
"rhythm": "пачкой, при каждом прогоне `dds_transform`", "rhythm": "при каждом запуске `dds_transform`",
"actor": "Задача `identity_map` дага `dds_transform` соединяет заказы с покупками.", "actor": "Задача `identity_map` дага `dds_transform` соединяет заказы с покупками.",
"join": "Строки заказа и покупки соединяются по `order_id = purchase_id[1]`; берется `user_id`.", "join": "Заказ соединяется с событием покупки по `order_id = purchase_id[1]`. Из заказа берется `user_id` пользователя магазина.",
"links": [ "links": [
{ {
"label": "SQL", "label": "SQL",
@@ -1074,9 +1072,9 @@
"streams": [ "streams": [
"events" "events"
], ],
"rhythm": "пачкой, при каждом прогоне `dm_transform`", "rhythm": "при каждом запуске `dm_transform`",
"actor": "Задача `daily_traffic` дага `dm_transform` считает посетителей по сессиям.", "actor": "Задача `daily_traffic` дага `dm_transform` считает посетителей по сессиям.",
"join": "Соединения нет: сессии группируются по `session_date`, который становится `report_date`.", "join": "К сессиям присоединяется карта идентичностей по `client_id`, затем данные группируются по `session_date`. Эта дата записывается в `report_date`.",
"links": [ "links": [
{ {
"label": "SQL", "label": "SQL",
@@ -1096,9 +1094,9 @@
"streams": [ "streams": [
"events" "events"
], ],
"rhythm": "пачкой, при каждом прогоне `dm_transform`", "rhythm": "при каждом запуске `dm_transform`",
"actor": "Задача `daily_traffic` дага `dm_transform` считает известных пользователей по `client_id`.", "actor": "Задача `daily_traffic` дага `dm_transform` считает известных пользователей по `client_id`.",
"join": "GLOBAL LEFT JOIN по `client_id`; сессии без пары остаются в витрине.", "join": "`GLOBAL LEFT JOIN` присоединяет карту идентичностей к сессиям по `client_id`. Сессии без известного пользователя тоже участвуют в расчете посетителей.",
"links": [ "links": [
{ {
"label": "SQL", "label": "SQL",
@@ -1118,9 +1116,9 @@
"streams": [ "streams": [
"orders" "orders"
], ],
"rhythm": "пачкой, при каждом прогоне `dm_transform`", "rhythm": "при каждом запуске `dm_transform`",
"actor": "Задачи `revenue_daily.rebuild` и `revenue_daily.replace` дага `dm_transform`.", "actor": "Задачи `revenue_daily.rebuild` и `revenue_daily.replace` дага `dm_transform`.",
"join": "Соединения таблиц нет: ARRAY JOIN раскрывает `item_sku`, `item_quantity`, `item_price`; `order_date` становится `report_date`.", "join": "`ARRAY JOIN` разворачивает массивы `item_sku`, `item_quantity`, `item_price` в строки позиций заказа. Выручка считается по оплаченным заказам; `order_date` записывается в `report_date`.",
"links": [ "links": [
{ {
"label": "SQL сборки", "label": "SQL сборки",
@@ -1144,9 +1142,9 @@
"streams": [ "streams": [
"orders" "orders"
], ],
"rhythm": "пачкой, при каждом прогоне `dm_transform`", "rhythm": "при каждом запуске `dm_transform`",
"actor": "Даг `dm_transform` получает категорию через `dictGet` по `item_sku`; ключ словаря — `sku`.", "actor": "Задача `revenue_daily.rebuild` дага `dm_transform`.",
"join": "Категория читается через `dictGet` по `item_sku`; ключ справочника - `sku`.", "join": "Функция `dictGet` получает категорию товара по артикулу `item_sku` из заказа. В словаре этому артикулу соответствует ключ `sku`.",
"links": [ "links": [
{ {
"label": "SQL", "label": "SQL",
@@ -1166,9 +1164,9 @@
"streams": [ "streams": [
"events" "events"
], ],
"rhythm": "пачкой, при каждом прогоне `dm_transform`", "rhythm": "при каждом запуске `dm_transform`",
"actor": "Задачи `purchase_vs_orders.rebuild` и `purchase_vs_orders.replace` дага `dm_transform` читают покупки.", "actor": "Задачи `purchase_vs_orders.rebuild` и `purchase_vs_orders.replace` дага `dm_transform` читают покупки.",
"join": "Покупки группируются по `purchase_id[1]` как `purchase_order_id`; `event_date` становится `purchase_day`, затем GLOBAL FULL OUTER JOIN `purchase_order_id = backend_order_id`.", "join": "События покупки группируются по номеру `purchase_id[1]`, который получает имя `purchase_order_id`. Дата `event_date` записывается в `purchase_day`. Затем `GLOBAL FULL OUTER JOIN` сопоставляет покупки с заказами по `purchase_order_id = backend_order_id`.",
"links": [ "links": [
{ {
"label": "SQL сборки", "label": "SQL сборки",
@@ -1192,9 +1190,9 @@
"streams": [ "streams": [
"orders" "orders"
], ],
"rhythm": "пачкой, при каждом прогоне `dm_transform`", "rhythm": "при каждом запуске `dm_transform`",
"actor": "Задачи `purchase_vs_orders.rebuild` и `purchase_vs_orders.replace` дага `dm_transform` читают заказы.", "actor": "Задачи `purchase_vs_orders.rebuild` и `purchase_vs_orders.replace` дага `dm_transform` читают заказы.",
"join": "GLOBAL FULL OUTER JOIN: `purchase_order_id = backend_order_id` (`purchase_id[1]` и `order_id`); отсутствующая сторона получает NULL.", "join": "`order_id` заказа получает имя `backend_order_id`, а `purchase_id[1]` покупки — `purchase_order_id`. `GLOBAL FULL OUTER JOIN` соединяет их по `purchase_order_id = backend_order_id`. Покупки без заказа и заказы без покупки сохраняются; поля отсутствующей стороны содержат `NULL`.",
"links": [ "links": [
{ {
"label": "SQL сборки", "label": "SQL сборки",
@@ -1308,9 +1306,9 @@
details.append(heading, meta); details.append(heading, meta);
addSection("Что это", node.what); addSection("Что это", node.what);
addSection("Как читать", node.read); addSection("Как читать", node.read);
addSection("Из чего состоит", node.parts); addSection("Связанные таблицы и представления", node.parts);
addSection("Ключи", node.keys); addSection("Как связать данные", node.keys);
addSection("Кто наполняет и как часто", node.fill); addSection("Как обновляются данные", node.fill);
addLinks(node.links); addLinks(node.links);
} }
function renderEdgeCard(edge) { function renderEdgeCard(edge) {
@@ -1318,10 +1316,10 @@
const heading = document.createElement("h2"); const heading = document.createElement("h2");
heading.textContent = `${nodeById.get(edge.from).name} → ${nodeById.get(edge.to).name}`; heading.textContent = `${nodeById.get(edge.from).name} → ${nodeById.get(edge.to).name}`;
details.append(heading); details.append(heading);
addSection("Механизм", mechanismById.get(edge.mechanism).label); addSection("Способ обработки", mechanismById.get(edge.mechanism).label);
addSection("Ритм", edge.rhythm); addSection("Когда выполняется", edge.rhythm);
addSection("Кто именно", edge.actor); addSection("Что выполняет обработку", edge.actor);
addSection("Как соединяет", edge.join); addSection("Что происходит с данными", edge.join);
addLinks(edge.links); addLinks(edge.links);
} }
function layoutMap() { function layoutMap() {
+150 -152
View File
@@ -1,13 +1,12 @@
title: Карта слоев хранилища title: Карта слоев хранилища
lead: Проследите, как один смысл меняет имя и поверхность между слоями. lead: Проследите путь событий и заказов от Kafka до отчетов.
chapter_url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/handbook/storage-layers.md#user-content-хранилище-и-его-слои chapter_url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/handbook/storage-layers.md#user-content-хранилище-и-его-слои
intro: intro:
- Эта карта показывает путь событий и заказов от Kafka до витрин DM. - Выберите объект, чтобы узнать, что он хранит, как его читать и откуда в него поступают данные.
- Щелкните по узлу, чтобы увидеть его назначение, устройство и ключи. - Выберите стрелку, чтобы посмотреть способ обработки и открыть SQL-запрос.
- Щелкните по стрелке, чтобы узнать, кто и каким запросом переносит данные. - Для выбранного объекта подсвечиваются все пути, по которым данные поступают в него и используются дальше.
- Когда вы выбираете узел, карта подсвечивает все пути к нему и от него. - Кнопки «События» и «Заказы» выделяют нужный поток. Остальные объекты остаются на месте, но становятся бледнее.
- Переключатель потока приглушает другую часть карты, но не меняет раскладку. - Если вы впервые знакомитесь со слоями хранилища, начните с главы по ссылке ниже.
- Сначала прочитайте главу «Хранилище и его слои», затем вернитесь к карте и рассмотрите объекты.
streams: streams:
all: Все all: Все
@@ -16,38 +15,38 @@ streams:
dictionary: dictionary:
title: Словарь title: Словарь
caption: 'сбоку цепочки: справочники' caption: справочные данные
layers: layers:
- id: kafka - id: kafka
title: Kafka title: Kafka
caption: откуда приходит caption: сообщения источников
- id: stg - id: stg
title: STG title: STG
caption: как поступило caption: исходные сообщения
- id: ods - id: ods
title: ODS title: ODS
caption: язык источника caption: данные разобраны по полям
- id: dds - id: dds
title: DDS title: DDS
caption: язык бизнеса caption: события, заказы и сессии
- id: dm - id: dm
title: DM title: DM
caption: данные для дашбордов caption: витрины для отчетов
mechanisms: mechanisms:
- id: materialized_view - id: materialized_view
label: матвью label: материализованное представление
line: solid line: solid
rhythm: непрерывно, по мере прихода сообщений rhythm: непрерывно, по мере прихода сообщений
- id: airflow_dag - id: airflow_dag
label: даг Airflow label: даг Airflow
line: dashed line: dashed
rhythm: 'пачкой: раз в модельные сутки (`orders_ingest`) или на каждом прогоне конвейера' rhythm: 'пакетно: раз в модельные сутки (`orders_ingest`) или при каждом запуске конвейера'
- id: view - id: view
label: представление label: представление
line: dotted line: dotted
rhythm: без копии, данные читаются на месте rhythm: при запросе к представлению, без копирования данных
nodes: nodes:
- id: kafka_hits - id: kafka_hits
@@ -56,9 +55,9 @@ nodes:
order: 0 order: 0
streams: [events] streams: [events]
what: Топик Kafka с событиями кликстрима. Каждое сообщение содержит одно событие. what: Топик Kafka с событиями кликстрима. Каждое сообщение содержит одно событие.
read: 'Топик напрямую не читают: таблица `stg.hits_raw_kafka` забирает сообщения из топика. Для данных событий используйте `stg.hits_raw_dist`.' read: "Для просмотра загруженных сообщений используйте `stg.hits_raw_dist`. Таблица `stg.hits_raw_kafka` служит для приема из топика."
fill: fill:
- Источник событий пишет сообщения в топик в темпе выбранного режима мира. - "Генератор отправляет события без пауз в пакетном режиме или постепенно в живом режиме."
links: links:
- label: Справочник - label: Справочник
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/storage.md#user-content-при%D1%91м-поток-и-его-свойства url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/storage.md#user-content-при%D1%91м-поток-и-его-свойства
@@ -70,10 +69,10 @@ nodes:
layer: kafka layer: kafka
order: 1 order: 1
streams: [orders] streams: [orders]
what: Топик Kafka со слепками заказов магазина. После модельного дня приходит снимок изменяемого окна. what: "Топик Kafka с дневными выгрузками заказов — слепками. Каждый слепок содержит состояние заказов за последние семь модельных дней."
read: 'Топик напрямую не читают: таблица `stg.orders_raw_kafka` забирает сообщения из топика. Для заказов используйте `stg.orders_raw_dist`.' read: "Для просмотра загруженных сообщений используйте `stg.orders_raw_dist`. Таблица `stg.orders_raw_kafka` служит для приема из топика."
fill: fill:
- Источник заказов публикует слепок пачкой после каждого модельного дня. - "После генерации событий дня D источник отправляет слепок заказов за предыдущий день D−1."
links: links:
- label: Справочник - label: Справочник
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/orders/ingestion.md#user-content-поток-данных url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/orders/ingestion.md#user-content-поток-данных
@@ -85,22 +84,22 @@ nodes:
layer: stg layer: stg
order: 0 order: 0
streams: [events] streams: [events]
what: Сырая строка события и метаданные доставки. STG сохраняет сообщение без разбора. what: "Исходное сообщение события и сведения о его доставке из Kafka. Содержимое сохраняется целой строкой, без разбора на поля."
read: 'Читайте `_dist`: у STG нет представления `_v`, а распределенная таблица показывает оба шарда.' read: 'Читайте `_dist`: у STG нет представления `_v`, а распределенная таблица показывает оба шарда.'
parts: parts:
- '`stg.hits_raw_kafka` — чтец топика `hits`.' - "`stg.hits_raw_kafka` — таблица для чтения сообщений топика `hits`."
- '`stg.hits_raw_rep` — реплицируемая таблица на шарде.' - "`stg.hits_raw_rep` — реплицируемая таблица на шарде."
- '`stg.hits_raw_dist` — распределенная поверхность записи и чтения.' - "`stg.hits_raw_dist` — распределенная таблица для записи и чтения."
- '`stg.hits_raw_mv` — матвью приема сообщений из чтеца.' - "`stg.hits_raw_mv` — материализованное представление, которое записывает сообщения из Kafka в STG."
fill: fill:
- '`stg.hits_raw_mv` пишет непрерывно, по мере прихода сообщений.' - '`stg.hits_raw_mv` пишет непрерывно, по мере прихода сообщений.'
- Сырые строки живут трое суток реального времени плюс хвост до суток. - Строки хранятся не менее трех суток от загрузки. Удаление целыми частями таблицы добавляет до суток к этому сроку; оно выполняется при очередном слиянии по TTL.
links: links:
- label: Справочник - label: Справочник
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/storage.md#user-content-при%D1%91м-поток-и-его-свойства url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/storage.md#user-content-при%D1%91м-поток-и-его-свойства
- label: DDL таблиц - label: DDL таблиц
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/10-stg-tables.sql url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/10-stg-tables.sql
- label: DDL матвью - label: DDL материализованного представления
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/40-stg-views.sql url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/40-stg-views.sql
- id: stg_orders - id: stg_orders
@@ -108,15 +107,15 @@ nodes:
layer: stg layer: stg
order: 1 order: 1
streams: [orders] streams: [orders]
what: Сырые строки слепка заказов, метаданные доставки и идентификатор пачки `_load_id`. what: "Исходные строки выгрузки заказов, сведения о доставке и идентификатор загруженной партии `_load_id`."
read: 'Читайте `_dist`: у STG нет представления `_v`, а распределенная таблица показывает оба шарда.' read: 'Читайте `_dist`: у STG нет представления `_v`, а распределенная таблица показывает оба шарда.'
parts: parts:
- '`stg.orders_raw_kafka` — чтец топика `orders` на первой ноде.' - "`stg.orders_raw_kafka` — таблица для чтения топика `orders` на первом узле."
- '`stg.orders_raw_rep` — реплицируемая таблица на шарде.' - "`stg.orders_raw_rep` — реплицируемая таблица на шарде."
- '`stg.orders_raw_dist` — распределенная поверхность записи и чтения.' - "`stg.orders_raw_dist` — распределенная таблица для записи и чтения."
fill: fill:
- Задача `pull_batch` дага `orders_ingest` забирает слепок пачкой раз в модельные сутки. - Задача `pull_batch` дага `orders_ingest` загружает слепок раз в модельные сутки.
- Сырые строки живут трое суток реального времени плюс хвост до суток. - Строки хранятся не менее трех суток от загрузки. Удаление целыми частями таблицы добавляет до суток к этому сроку; оно выполняется при очередном слиянии по TTL.
links: links:
- label: Справочник - label: Справочник
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/orders/ingestion.md#user-content-поток-данных url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/orders/ingestion.md#user-content-поток-данных
@@ -128,13 +127,13 @@ nodes:
layer: ods layer: ods
order: 0 order: 0
streams: [events] streams: [events]
what: Типизированное широкое событие с именами полей источника. Запрос представления с `FINAL` возвращает актуальную версию события. what: "События, разобранные на колонки с типами данных. Имена полей сохранены как в источнике."
read: 'Читайте `ods.event_v`: запрос представления с `FINAL` возвращает актуальные строки вместо физических версий.' read: "Читайте `ods.event_v`: представление использует `FINAL` и возвращает последнюю версию каждого события."
parts: parts:
- '`ods.event_rep` — реплицируемая таблица версий события на шарде.' - '`ods.event_rep` — реплицируемая таблица версий события на шарде.'
- '`ods.event_dist` — распределенная поверхность записи и диагностики.' - '`ods.event_dist` — распределенная таблица записи и диагностики.'
- '`ods.event_v` — представление актуального состояния с `FINAL`.' - '`ods.event_v` — представление актуального состояния с `FINAL`.'
- '`ods.event_mv` — матвью разбора годных строк STG.' - '`ods.event_mv` — материализованное представление для разбора сообщений STG без ошибок.'
keys: keys:
- '`WatchID` — тот же идентификатор, что `event_id` в DDS.' - '`WatchID` — тот же идентификатор, что `event_id` в DDS.'
- '`ClientID` — тот же посетитель, что `client_id` в DDS.' - '`ClientID` — тот же посетитель, что `client_id` в DDS.'
@@ -155,21 +154,21 @@ nodes:
layer: ods layer: ods
order: 1 order: 1
streams: [events] streams: [events]
what: Сырые события, которые не прошли строгий прием, с классом ошибки и метаданными доставки. what: "Сообщения событий, которые не удалось разобрать по правилам ODS. Сохранены исходная строка, класс ошибки и сведения о доставке."
read: 'Читайте `_dist`: представления `_v` нет, а распределенная таблица показывает ошибки с обоих шардов.' read: 'Читайте `_dist`: представления `_v` нет, а распределенная таблица показывает ошибки с обоих шардов.'
parts: parts:
- '`ods.event_errors_rep` — реплицируемая таблица ошибок на шарде.' - '`ods.event_errors_rep` — реплицируемая таблица ошибок на шарде.'
- '`ods.event_errors_dist` — распределенная поверхность чтения и записи.' - '`ods.event_errors_dist` — распределенная таблица чтения и записи.'
- '`ods.event_errors_mv` — матвью, которое отбирает негодные строки STG.' - '`ods.event_errors_mv` — материализованное представление для отбора сообщений STG с ошибками.'
fill: fill:
- '`ods.event_errors_mv` пишет непрерывно, по мере записи в STG.' - '`ods.event_errors_mv` пишет непрерывно, по мере записи в STG.'
- Ошибки живут один месяц и не останавливают прием годных событий. - Ошибки хранятся один месяц. Остальные события продолжают загружаться.
links: links:
- label: Справочник - label: Справочник
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/storage.md#user-content-таблица-ошибок url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/storage.md#user-content-таблица-ошибок
- label: DDL таблиц - label: DDL таблиц
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/20-ods-tables.sql url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/20-ods-tables.sql
- label: DDL матвью - label: DDL материализованного представления
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/30-ods-views.sql url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/30-ods-views.sql
- id: ods_order - id: ods_order
@@ -177,17 +176,17 @@ nodes:
layer: ods layer: ods
order: 2 order: 2
streams: [orders] streams: [orders]
what: Текущая версия заказа на языке источника. Позиции пока остаются сырым фрагментом JSON. what: "Последняя загруженная версия заказа с именами полей из источника. Состав заказа пока хранится как фрагмент JSON."
read: 'Читайте `ods.order_v`: запрос представления с `FINAL` возвращает последнюю версию по `snapshot_date`.' read: 'Читайте `ods.order_v`: запрос представления с `FINAL` возвращает последнюю версию по `snapshot_date`.'
parts: parts:
- '`ods.order_rep` — реплицируемая таблица версий заказа на шарде.' - '`ods.order_rep` — реплицируемая таблица версий заказа на шарде.'
- '`ods.order_dist` — распределенная поверхность записи и диагностики.' - '`ods.order_dist` — распределенная таблица записи и диагностики.'
- '`ods.order_v` — представление текущего состояния с `FINAL`.' - '`ods.order_v` — представление текущего состояния с `FINAL`.'
keys: keys:
- '`order_id` — тот же заказ в `dds.order_v` и `dm.purchase_vs_orders_v`.' - '`order_id` — тот же заказ в `dds.order_v` и `dm.purchase_vs_orders_v`.'
- '`user_id` — пользователь магазина, который переходит в заказ DDS.' - '`user_id` — пользователь магазина, который переходит в заказ DDS.'
fill: fill:
- Задача `parse_batch` дага `orders_ingest` разбирает пачку раз в модельные сутки. - Задача `parse_batch` дага `orders_ingest` разбирает загруженный слепок раз в модельные сутки.
- '`ReplacingMergeTree(snapshot_date)` оставляет версию из последнего принятого слепка.' - '`ReplacingMergeTree(snapshot_date)` оставляет версию из последнего принятого слепка.'
links: links:
- label: Справочник - label: Справочник
@@ -202,14 +201,14 @@ nodes:
layer: ods layer: ods
order: 3 order: 3
streams: [orders] streams: [orders]
what: Сырые строки слепка, которые не прошли строгий прием, с классом ошибки и `_load_id`. what: "Строки выгрузки заказов, которые не удалось разобрать по правилам ODS. Сохранены исходная строка, класс ошибки и `_load_id`."
read: 'Читайте `_dist`: представления `_v` нет, а распределенная таблица показывает ошибки с обоих шардов.' read: 'Читайте `_dist`: представления `_v` нет, а распределенная таблица показывает ошибки с обоих шардов.'
parts: parts:
- '`ods.order_errors_rep` — реплицируемая таблица ошибок на шарде.' - '`ods.order_errors_rep` — реплицируемая таблица ошибок на шарде.'
- '`ods.order_errors_dist` — распределенная поверхность чтения и записи.' - '`ods.order_errors_dist` — распределенная таблица чтения и записи.'
fill: fill:
- Задача `parse_batch` дага `orders_ingest` пишет ошибки из той же пачки, что и годные заказы. - Задача `parse_batch` дага `orders_ingest` сохраняет строки с ошибками при разборе той же выгрузки, из которой загружаются заказы.
- Ошибки живут один месяц и не останавливают прием годных заказов. - Ошибки хранятся один месяц. Остальные заказы продолжают загружаться.
links: links:
- label: Справочник - label: Справочник
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/storage.md#user-content-таблица-ошибок url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/storage.md#user-content-таблица-ошибок
@@ -221,8 +220,8 @@ nodes:
layer: dds layer: dds
order: 0 order: 0
streams: [events] streams: [events]
what: Публичное представление события на языке бизнеса. Его запрос переименовывает поля и расшифровывает категорию устройства. what: "События с названиями полей, принятыми в DDS. Например, `WatchID` переименован в `event_id`, а числовой код устройства заменен названием категории."
read: 'Читайте `dds.event_v`: запрос представления возвращает явный список колонок DDS.' read: "Читайте `dds.event_v`: представление возвращает явно перечисленные колонки DDS."
parts: parts:
- '`dds.event_v` — только представление над `ods.event_v`; своей таблицы в DDS нет.' - '`dds.event_v` — только представление над `ods.event_v`; своей таблицы в DDS нет.'
keys: keys:
@@ -231,7 +230,7 @@ nodes:
- '`event_date` — тот же день, что `EventDate` в ODS.' - '`event_date` — тот же день, что `EventDate` в ODS.'
- '`purchase_id[1]` — тот же заказ, что `order_id` в DDS и DM.' - '`purchase_id[1]` — тот же заказ, что `order_id` в DDS и DM.'
fill: fill:
- Данные для `dds.event_v` читаются из актуальных событий ODS без копии и отдельного расписания. - "При запросе к `dds.event_v` данные читаются из актуальных событий ODS. Отдельная загрузка в DDS не нужна."
links: links:
- label: Справочник - label: Справочник
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dds/event.md#user-content-форма url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dds/event.md#user-content-форма
@@ -243,20 +242,20 @@ nodes:
layer: dds layer: dds
order: 1 order: 1
streams: [events] streams: [events]
what: Сессия, которую запрос дага `dds_transform` собирает из последовательных событий одной куки. В источнике готовой сессии нет. what: "Сессии, рассчитанные дагом `dds_transform` по событиям посетителя. Новая сессия начинается после паузы дольше 30 минут или при смене модельной даты."
read: 'Читайте `dds.session_v`: запрос представления возвращает деловые и служебные колонки.' read: "Читайте `dds.session_v`: здесь есть начало, конец, длительность сессии, показатели событий и служебные поля загрузки."
parts: parts:
- '`dds.session_rep` — реплицируемая таблица сессий на шарде.' - '`dds.session_rep` — реплицируемая таблица сессий на шарде.'
- '`dds.session_dist` — распределенная поверхность загрузки и диагностики.' - '`dds.session_dist` — распределенная таблица загрузки и диагностики.'
- '`dds.session_stage_rep` — донор дневных партиций на шарде.' - '`dds.session_stage_rep` — промежуточная таблица для пересчета данных по дням на шарде.'
- '`dds.session_stage_dist` — распределенная поверхность донора.' - '`dds.session_stage_dist` — распределенная таблица для записи и чтения промежуточных данных.'
- '`dds.session_v` — публичное представление.' - '`dds.session_v` — представление для чтения готовых данных.'
keys: keys:
- '`client_id` — тот же посетитель, что в событии DDS и карте идентичностей.' - '`client_id` — тот же посетитель, что в событии DDS и карте идентичностей.'
- '`session_date` — день, который в `dm.daily_traffic_v` называется `report_date`.' - '`session_date` — день, который в `dm.daily_traffic_v` называется `report_date`.'
fill: fill:
- Даг `dds_transform` пересобирает данные прожитых дней при каждом прогоне конвейера. - "При каждом запуске `dds_transform` выбирает дни от первого пропущенного в DDS до последнего дня с завершенной генерацией."
- Задачи `session.rebuild` и `session.replace` собирают день в доноре и заменяют партицию. - "Задача `session.rebuild` рассчитывает данные в промежуточной таблице (доноре). `session.replace` заменяет ими дневные партиции основной таблицы."
links: links:
- label: Справочник - label: Справочник
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dds/session.md#user-content-движок-и-наполнение url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dds/session.md#user-content-движок-и-наполнение
@@ -270,22 +269,22 @@ nodes:
layer: dds layer: dds
order: 2 order: 2
streams: [orders] streams: [orders]
what: Заказ в текущем состоянии на языке бизнеса. Артикулы, количества и цены позиций хранятся в согласованных массивах. what: "Последнее известное состояние заказа, подготовленное для анализа. Артикулы, количества и цены товаров хранятся в массивах; элементы с одинаковым индексом относятся к одной позиции заказа."
read: 'Читайте `dds.order_v`: запрос представления возвращает данные без таблиц и донора партиций.' read: "Читайте `dds.order_v`, чтобы получить готовые заказы из основной таблицы."
parts: parts:
- '`dds.order_rep` — реплицируемая таблица заказов на шарде.' - '`dds.order_rep` — реплицируемая таблица заказов на шарде.'
- '`dds.order_dist` — распределенная поверхность загрузки и диагностики.' - '`dds.order_dist` — распределенная таблица загрузки и диагностики.'
- '`dds.order_stage_rep` — донор дневных партиций на шарде.' - '`dds.order_stage_rep` — промежуточная таблица для пересчета данных по дням на шарде.'
- '`dds.order_stage_dist` — распределенная поверхность донора.' - '`dds.order_stage_dist` — распределенная таблица для записи и чтения промежуточных данных.'
- '`dds.order_v` — публичное представление.' - '`dds.order_v` — представление для чтения готовых данных.'
keys: keys:
- '`order_id` — тот же заказ, что в ODS, событии покупки и витрине сверки.' - '`order_id` — тот же заказ, что в ODS, событии покупки и витрине сверки.'
- '`user_id` — пользователь магазина, который входит в карту идентичностей.' - '`user_id` — пользователь магазина, который входит в карту идентичностей.'
- '`item_sku` — тот же артикул, что `sku` в `dic.products`.' - '`item_sku` — тот же артикул, что `sku` в `dic.products`.'
- '`order_date` — день, который в витринах называется `report_date` или `order_day`.' - '`order_date` — день, который в витринах называется `report_date` или `order_day`.'
fill: fill:
- Даг `dds_transform` пересобирает изменяемое окно при каждом прогоне конвейера. - "При каждом запуске `dds_transform` пересчитывает дни заказов из последнего слепка и заполняет пропуски в DDS."
- Задачи `order.rebuild` и `order.replace` собирают дни в доноре и заменяют партиции. - "Задача `order.rebuild` рассчитывает данные в промежуточной таблице (доноре). `order.replace` заменяет ими дневные партиции основной таблицы."
links: links:
- label: Справочник - label: Справочник
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dds/order.md#user-content-движок-и-наполнение url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dds/order.md#user-content-движок-и-наполнение
@@ -299,18 +298,18 @@ nodes:
layer: dds layer: dds
order: 3 order: 3
streams: [events, orders] streams: [events, orders]
what: Известные пары куки трекера и пользователя магазина. Связь появляется после сопоставленной покупки. what: "Связи между посетителем сайта и пользователем магазина. Они появляются, когда событие покупки сопоставлено с заказом по его номеру."
read: 'Читайте `dds.identity_map_v`: запрос представления с `FINAL` возвращает актуальные строки полного пересчета.' read: "Читайте `dds.identity_map_v`: представление с `FINAL` возвращает последнюю версию каждой пары посетителя и пользователя."
parts: parts:
- '`dds.identity_map_rep` — реплицируемая таблица версий пар на шарде.' - '`dds.identity_map_rep` — реплицируемая таблица версий пар на шарде.'
- '`dds.identity_map_dist` — распределенная поверхность записи.' - '`dds.identity_map_dist` — распределенная таблица записи.'
- '`dds.identity_map_v` — точное публичное представление с `FINAL`.' - '`dds.identity_map_v` — представление последних версий строк с `FINAL`.'
keys: keys:
- '`client_id` — кука из события и сессии DDS.' - "`client_id` — идентификатор посетителя из cookie браузера, как в событии и сессии DDS."
- '`user_id` — пользователь магазина из заказа DDS.' - "`user_id` — пользователь магазина из заказа DDS."
fill: fill:
- Задача `identity_map` дага `dds_transform` пересчитывает все известные пары после обновления заказов. - "Задача `identity_map` дага `dds_transform` пересчитывает все известные пары после обновления заказов."
- Новая версия полного набора пишется при каждом прогоне; `FINAL` скрывает физические повторы. - "При каждом запуске записывается новая версия набора связей."
links: links:
- label: Справочник - label: Справочник
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dds/identity.md#user-content-сборка url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dds/identity.md#user-content-сборка
@@ -324,17 +323,16 @@ nodes:
layer: dm layer: dm
order: 0 order: 0
streams: [events] streams: [events]
what: Дневные счетчики посетителей и известных пользователей после склейки идентичностей. what: "Число посетителей и известных пользователей за каждый день. Известными считаются пользователи магазина, связанные с посетителями сайта через карту идентичностей."
read: 'Читайте `dm.daily_traffic_v`: запрос представления с `FINAL` возвращает актуальные строки полного пересчета.' read: "Читайте `dm.daily_traffic_v`: представление с `FINAL` возвращает последний результат расчета за каждый день."
parts: parts:
- '`dm.daily_traffic_rep` — реплицируемая таблица версий счетчиков.' - '`dm.daily_traffic_rep` — реплицируемая таблица версий счетчиков.'
- '`dm.daily_traffic_dist` — распределенная поверхность записи.' - '`dm.daily_traffic_dist` — распределенная таблица записи.'
- '`dm.daily_traffic_v` — точное публичное представление с `FINAL`.' - '`dm.daily_traffic_v` — представление последних версий строк с `FINAL`.'
keys: keys:
- '`report_date` — тот же день, что `session_date` в сессии DDS.' - '`report_date` — тот же день, что `session_date` в сессии DDS.'
fill: fill:
- Задача `daily_traffic` дага `dm_transform` при каждом прогоне пересчитывает всю историю и записывает новую версию. - "Задача `daily_traffic` дага `dm_transform` при каждом запуске пересчитывает всю историю и записывает новую версию."
- '`FINAL` оставляет текущую строку каждого дня.'
links: links:
- label: Справочник - label: Справочник
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dm.md#user-content-dmdaily_traffic_v url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dm.md#user-content-dmdaily_traffic_v
@@ -348,20 +346,20 @@ nodes:
layer: dm layer: dm
order: 1 order: 1
streams: [orders] streams: [orders]
what: Дневная выручка, число заказов и единиц товара по категориям. what: "Выручка, число заказов и единиц товара за день по категориям. В расчет входят оплаченные заказы."
read: 'Читайте `dm.revenue_daily_v`: это публичное представление готовой дневной витрины.' read: "Читайте `dm.revenue_daily_v`, чтобы получить готовые показатели по дням и категориям товаров."
parts: parts:
- '`dm.revenue_daily_rep` — реплицируемая таблица витрины на шарде.' - '`dm.revenue_daily_rep` — реплицируемая таблица витрины на шарде.'
- '`dm.revenue_daily_dist` — распределенная поверхность загрузки.' - '`dm.revenue_daily_dist` — распределенная таблица загрузки.'
- '`dm.revenue_daily_stage_rep` — донор дневных партиций на шарде.' - '`dm.revenue_daily_stage_rep` — промежуточная таблица для пересчета данных по дням на шарде.'
- '`dm.revenue_daily_stage_dist` — распределенная поверхность донора.' - '`dm.revenue_daily_stage_dist` — распределенная таблица для записи и чтения промежуточных данных.'
- '`dm.revenue_daily_v` — публичное представление.' - '`dm.revenue_daily_v` — представление для чтения готовых данных.'
keys: keys:
- '`report_date` — тот же день, что `order_date` в заказе DDS.' - '`report_date` — тот же день, что `order_date` в заказе DDS.'
- '`product_category` — категория, найденная по `item_sku` заказа и ключу `sku` словаря.' - '`product_category` — категория, найденная по `item_sku` заказа и ключу `sku` словаря.'
fill: fill:
- Даг `dm_transform` пересобирает изменяемое окно заказов при каждом прогоне конвейера. - "При каждом запуске `dm_transform` пересчитывает дни заказов из последнего слепка и заполняет пропущенные дни витрины."
- Задачи `revenue_daily.rebuild` и `revenue_daily.replace` собирают дни в доноре и заменяют партиции. - "Задача `revenue_daily.rebuild` рассчитывает данные в промежуточной таблице (доноре). `revenue_daily.replace` заменяет ими дневные партиции витрины."
links: links:
- label: Справочник - label: Справочник
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dm.md#user-content-dmrevenue_daily_v url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dm.md#user-content-dmrevenue_daily_v
@@ -375,20 +373,20 @@ nodes:
layer: dm layer: dm
order: 2 order: 2
streams: [events, orders] streams: [events, orders]
what: Сверка события покупки с заказом магазина. Одна строка показывает совпадение или класс расхождения. what: "Результат сверки покупок из кликстрима с заказами магазина. Для каждого номера заказа указано, совпали ли данные двух источников и какое расхождение найдено."
read: 'Читайте `dm.purchase_vs_orders_v`: в результате запроса отсутствующей стороне соответствует `NULL`.' read: "Читайте `dm.purchase_vs_orders_v`: если покупка или заказ отсутствует, поля этой стороны содержат `NULL`."
parts: parts:
- '`dm.purchase_vs_orders_rep` — реплицируемая таблица сверки на шарде.' - '`dm.purchase_vs_orders_rep` — реплицируемая таблица сверки на шарде.'
- '`dm.purchase_vs_orders_dist` — распределенная поверхность загрузки.' - '`dm.purchase_vs_orders_dist` — распределенная таблица загрузки.'
- '`dm.purchase_vs_orders_stage_rep` — донор дневных партиций на шарде.' - '`dm.purchase_vs_orders_stage_rep` — промежуточная таблица для пересчета данных по дням на шарде.'
- '`dm.purchase_vs_orders_stage_dist` — распределенная поверхность донора.' - '`dm.purchase_vs_orders_stage_dist` — распределенная таблица для записи и чтения промежуточных данных.'
- '`dm.purchase_vs_orders_v` — публичное представление.' - '`dm.purchase_vs_orders_v` — представление для чтения готовых данных.'
keys: keys:
- '`order_id` — тот же заказ, что `purchase_id[1]` в событии и `order_id` в заказе DDS.' - "`order_id` — тот же заказ, что `purchase_id[1]` в событии и `order_id` в заказе DDS."
- '`order_day` — общий день события покупки и заказа.' - "`order_day` — дата заказа; если заказа еще нет, берется дата события покупки."
fill: fill:
- Даг `dm_transform` пересобирает изменяемый хвост обоих источников при каждом прогоне конвейера. - "При каждом запуске `dm_transform` пересчитывает дни последнего слепка, добавляет новые дни покупок, заполняет пропуски и повторяет сверку дней с покупками без заказа."
- Задачи `purchase_vs_orders.rebuild` и `purchase_vs_orders.replace` собирают дни в доноре и заменяют партиции. - "Задача `purchase_vs_orders.rebuild` рассчитывает данные в промежуточной таблице (доноре). `purchase_vs_orders.replace` заменяет ими дневные партиции витрины."
links: links:
- label: Справочник - label: Справочник
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dm.md#user-content-dmpurchase_vs_orders_v url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/dm.md#user-content-dmpurchase_vs_orders_v
@@ -402,18 +400,18 @@ nodes:
layer: dm layer: dm
order: 3 order: 3
streams: [events, orders] streams: [events, orders]
what: Текущее состояние пяти атомарных проверок качества по дням. История запусков остается в Airflow. what: "Последние результаты пяти проверок качества данных за каждый день. Историю запусков можно посмотреть в Airflow."
read: 'Читайте `dm.dq_summary_v`: запрос к публичному представлению возвращает последнюю опубликованную сводку дня.' read: "Читайте `dm.dq_summary_v`, чтобы увидеть результат каждой проверки за день."
parts: parts:
- '`dm.dq_summary_rep` — реплицируемая таблица опубликованной сводки.' - '`dm.dq_summary_rep` — реплицируемая таблица опубликованной сводки.'
- '`dm.dq_summary_dist` — распределенная поверхность чтения и записи.' - '`dm.dq_summary_dist` — распределенная таблица чтения и записи.'
- '`dm.dq_summary_stage_rep` — общий донор дневных партиций.' - '`dm.dq_summary_stage_rep` — промежуточная таблица для результатов всех проверок за день.'
- '`dm.dq_summary_stage_dist` — распределенная поверхность донора.' - '`dm.dq_summary_stage_dist` — распределенная таблица для записи и чтения промежуточных данных.'
- '`dm.dq_summary_v` — публичное представление.' - '`dm.dq_summary_v` — представление для чтения готовых данных.'
fill: fill:
- Даг `dq_check` при каждом прогоне сначала выполняет все пять проверок, затем заменяет готовые дневные партиции. - Даг `dq_check` сначала рассчитывает результаты всех пяти проверок в промежуточной таблице, затем заменяет ими дневные партиции сводки.
- '`sessions_vs_reference` — `sql/dq/sessions_vs_reference.sql`.' - '`sessions_vs_reference` — `sql/dq/sessions_vs_reference.sql`.'
- '`classes_vs_inventory` — `sql/dq/classes_vs_inventory.sql` и паспорт мира.' - '`classes_vs_inventory` — `sql/dq/classes_vs_inventory.sql` и эталонные числа из паспорта мира `data/world-inventory.json`.'
- '`revenue_daily_vs_dds` — `sql/dq/revenue_daily_vs_dds.sql`.' - '`revenue_daily_vs_dds` — `sql/dq/revenue_daily_vs_dds.sql`.'
- '`purchase_vs_orders_vs_dds` — `sql/dq/purchase_vs_orders_vs_dds.sql`.' - '`purchase_vs_orders_vs_dds` — `sql/dq/purchase_vs_orders_vs_dds.sql`.'
- '`daily_traffic_vs_dds` — `sql/dq/daily_traffic_vs_dds.sql`.' - '`daily_traffic_vs_dds` — `sql/dq/daily_traffic_vs_dds.sql`.'
@@ -430,15 +428,15 @@ nodes:
layer: dic layer: dic
order: 0 order: 0
streams: [orders] streams: [orders]
what: Словарь товаров вне цепочки слоев. По артикулу доступны название, бренд, категория и цена. what: "Словарь товаров. По артикулу можно получить название, бренд, категорию и цену. Эти данные доступны для расчетов в разных слоях."
read: 'Читайте `dic.products`: у словаря одна поверхность без суффикса.' read: "Читайте `dic.products`: к словарю обращаются напрямую, суффикса `_v` у него нет."
parts: parts:
- '`dic.products_file` — чтец CSV-каталога из файла репозитория.' - "`dic.products_file` — таблица, которая читает каталог товаров из CSV-файла репозитория."
- '`dic.products` — словарь в памяти поверх подложки.' - "`dic.products` — словарь в памяти, загруженный из этой таблицы."
keys: keys:
- '`sku` — тот же артикул, что `item_sku` в позиции заказа DDS.' - '`sku` — тот же артикул, что `item_sku` в позиции заказа DDS.'
fill: fill:
- ClickHouse перечитывает подложку каждые 60–90 секунд по настройке `LIFETIME`. - "ClickHouse обновляет словарь из `dic.products_file` каждые 60–90 секунд согласно настройке `LIFETIME`."
links: links:
- label: Справочник - label: Справочник
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/storage.md#user-content-справочники url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/storage.md#user-content-справочники
@@ -452,8 +450,8 @@ edges:
mechanism: materialized_view mechanism: materialized_view
streams: [events] streams: [events]
rhythm: непрерывно, по мере прихода сообщений rhythm: непрерывно, по мере прихода сообщений
actor: Чтец `stg.hits_raw_kafka` и матвью `stg.hits_raw_mv`. actor: "Таблица `stg.hits_raw_kafka` и материализованное представление `stg.hits_raw_mv`."
join: 'Соединения нет: сообщение кладется строкой целиком.' join: "Каждое сообщение Kafka записывается целой строкой, без разбора на поля."
links: links:
- label: SQL - label: SQL
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/40-stg-views.sql url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/40-stg-views.sql
@@ -466,8 +464,8 @@ edges:
mechanism: materialized_view mechanism: materialized_view
streams: [events] streams: [events]
rhythm: непрерывно, по мере прихода сообщений rhythm: непрерывно, по мере прихода сообщений
actor: Матвью `ods.event_mv` разбирает годные строки. actor: Материализованное представление `ods.event_mv` разбирает сообщения без ошибок.
join: 'Соединения нет: матвью разбирает строку в колонки.' join: "Материализованное представление разбирает сообщение из STG на колонки с нужными типами данных."
links: links:
- label: SQL - label: SQL
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/30-ods-views.sql url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/30-ods-views.sql
@@ -480,8 +478,8 @@ edges:
mechanism: materialized_view mechanism: materialized_view
streams: [events] streams: [events]
rhythm: непрерывно, по мере прихода сообщений rhythm: непрерывно, по мере прихода сообщений
actor: Матвью `ods.event_errors_mv` отбирает строки, которые не прошли строгий прием. actor: "Материализованное представление `ods.event_errors_mv` отбирает сообщения с ошибками."
join: 'Соединения нет: негодная строка уходит целиком вместе с классом ошибки.' join: "Сообщение, которое не удалось разобрать по правилам ODS, сохраняется целиком вместе с классом ошибки."
links: links:
- label: SQL - label: SQL
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/30-ods-views.sql url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/30-ods-views.sql
@@ -493,9 +491,9 @@ edges:
to: stg_orders to: stg_orders
mechanism: airflow_dag mechanism: airflow_dag
streams: [orders] streams: [orders]
rhythm: пачкой, раз в модельные сутки rhythm: одной выгрузкой, раз в модельные сутки
actor: Задача `pull_batch` дага `orders_ingest` читает `stg.orders_raw_kafka`. actor: Задача `pull_batch` дага `orders_ingest` читает `stg.orders_raw_kafka`.
join: 'Соединения нет: сообщение слепка кладется целиком, пачка помечена `_load_id`.' join: "Строки выгрузки сохраняются целиком. Общий `_load_id` позволяет найти все строки одной загрузки."
links: links:
- label: SQL - label: SQL
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/stg/orders_raw_load.sql url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/stg/orders_raw_load.sql
@@ -507,9 +505,9 @@ edges:
to: ods_order to: ods_order
mechanism: airflow_dag mechanism: airflow_dag
streams: [orders] streams: [orders]
rhythm: пачкой, раз в модельные сутки rhythm: одной выгрузкой, раз в модельные сутки
actor: Задача `parse_batch` дага `orders_ingest` пишет годные версии заказа. actor: Задача `parse_batch` дага `orders_ingest` записывает версии заказов без ошибок разбора.
join: 'Соединения нет: строка разбирается в колонки, версия задается `snapshot_date`.' join: "Строка разбирается на колонки заказа. Версия определяется датой слепка `snapshot_date`."
links: links:
- label: SQL - label: SQL
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ods/order_parse.sql url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ods/order_parse.sql
@@ -521,9 +519,9 @@ edges:
to: ods_order_errors to: ods_order_errors
mechanism: airflow_dag mechanism: airflow_dag
streams: [orders] streams: [orders]
rhythm: пачкой, раз в модельные сутки rhythm: одной выгрузкой, раз в модельные сутки
actor: Задача `parse_batch` дага `orders_ingest` пишет негодные строки той же пачки. actor: Задача `parse_batch` дага `orders_ingest` записывает строки с ошибками из загруженной выгрузки.
join: 'Соединения нет: негодная строка пачки уходит целиком.' join: "Строка с ошибкой сохраняется целиком вместе с классом ошибки и идентификатором загрузки."
links: links:
- label: SQL - label: SQL
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ods/order_parse.sql url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ods/order_parse.sql
@@ -535,9 +533,9 @@ edges:
to: dds_event to: dds_event
mechanism: view mechanism: view
streams: [events] streams: [events]
rhythm: без копии, данные читаются на месте rhythm: при запросе к представлению, без копирования данных
actor: Запрос `dds.event_v` читает данные из `ods.event_v` и переименовывает поля на языке бизнеса. actor: "Представление `dds.event_v` читает данные из `ods.event_v`."
join: 'Соединения нет: представление переименовывает колонки той же строки.' join: "Колонки события переименовываются по правилам DDS. Данные не копируются в отдельную таблицу."
links: links:
- label: SQL - label: SQL
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/50-dds-views.sql url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/ddl/50-dds-views.sql
@@ -549,9 +547,9 @@ edges:
to: dds_session to: dds_session
mechanism: airflow_dag mechanism: airflow_dag
streams: [events] streams: [events]
rhythm: пачкой, при каждом прогоне `dds_transform` rhythm: при каждом запуске `dds_transform`
actor: Задачи `session.rebuild` и `session.replace` дага `dds_transform`. actor: Задачи `session.rebuild` и `session.replace` дага `dds_transform`.
join: 'События одной `client_id` за `event_date` режутся по паузам; `session_id` - `cityHash64(client_id, min(event_time))`.' join: "События одного `client_id` за день `event_date` упорядочиваются по времени. Пауза дольше 30 минут начинает новую сессию. Ее `session_id` рассчитывается как `cityHash64(client_id, min(event_time))`."
links: links:
- label: SQL сборки - label: SQL сборки
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dds/session_rebuild.sql url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dds/session_rebuild.sql
@@ -565,9 +563,9 @@ edges:
to: dds_order to: dds_order
mechanism: airflow_dag mechanism: airflow_dag
streams: [orders] streams: [orders]
rhythm: пачкой, при каждом прогоне `dds_transform` rhythm: при каждом запуске `dds_transform`
actor: Задачи `order.rebuild` и `order.replace` дага `dds_transform`. actor: Задачи `order.rebuild` и `order.replace` дага `dds_transform`.
join: 'Соединения нет: из заказа берутся поля и позиции, `order_date` выводится из `created_at`.' join: "Поля заказа приводятся к типам DDS, а позиции из JSON разбираются в массивы. Дата `order_date` рассчитывается из `created_at`."
links: links:
- label: SQL сборки - label: SQL сборки
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dds/order_rebuild.sql url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dds/order_rebuild.sql
@@ -581,9 +579,9 @@ edges:
to: dds_identity to: dds_identity
mechanism: airflow_dag mechanism: airflow_dag
streams: [events] streams: [events]
rhythm: пачкой, при каждом прогоне `dds_transform` rhythm: при каждом запуске `dds_transform`
actor: Задача `identity_map` дага `dds_transform` соединяет покупки с заказами. actor: Задача `identity_map` дага `dds_transform` соединяет покупки с заказами.
join: 'Строки покупки и заказа соединяются по `purchase_id[1] = order_id`.' join: "Событие покупки соединяется с заказом по `purchase_id[1] = order_id`. В результате связываются посетитель сайта и пользователь магазина."
links: links:
- label: SQL - label: SQL
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dds/identity_map_rebuild.sql url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dds/identity_map_rebuild.sql
@@ -595,9 +593,9 @@ edges:
to: dds_identity to: dds_identity
mechanism: airflow_dag mechanism: airflow_dag
streams: [orders] streams: [orders]
rhythm: пачкой, при каждом прогоне `dds_transform` rhythm: при каждом запуске `dds_transform`
actor: Задача `identity_map` дага `dds_transform` соединяет заказы с покупками. actor: Задача `identity_map` дага `dds_transform` соединяет заказы с покупками.
join: 'Строки заказа и покупки соединяются по `order_id = purchase_id[1]`; берется `user_id`.' join: "Заказ соединяется с событием покупки по `order_id = purchase_id[1]`. Из заказа берется `user_id` пользователя магазина."
links: links:
- label: SQL - label: SQL
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dds/identity_map_rebuild.sql url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dds/identity_map_rebuild.sql
@@ -609,9 +607,9 @@ edges:
to: dm_daily_traffic to: dm_daily_traffic
mechanism: airflow_dag mechanism: airflow_dag
streams: [events] streams: [events]
rhythm: пачкой, при каждом прогоне `dm_transform` rhythm: при каждом запуске `dm_transform`
actor: Задача `daily_traffic` дага `dm_transform` считает посетителей по сессиям. actor: Задача `daily_traffic` дага `dm_transform` считает посетителей по сессиям.
join: 'Соединения нет: сессии группируются по `session_date`, который становится `report_date`.' join: "К сессиям присоединяется карта идентичностей по `client_id`, затем данные группируются по `session_date`. Эта дата записывается в `report_date`."
links: links:
- label: SQL - label: SQL
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dm/daily_traffic_rebuild.sql url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dm/daily_traffic_rebuild.sql
@@ -623,9 +621,9 @@ edges:
to: dm_daily_traffic to: dm_daily_traffic
mechanism: airflow_dag mechanism: airflow_dag
streams: [events] streams: [events]
rhythm: пачкой, при каждом прогоне `dm_transform` rhythm: при каждом запуске `dm_transform`
actor: Задача `daily_traffic` дага `dm_transform` считает известных пользователей по `client_id`. actor: Задача `daily_traffic` дага `dm_transform` считает известных пользователей по `client_id`.
join: 'GLOBAL LEFT JOIN по `client_id`; сессии без пары остаются в витрине.' join: "`GLOBAL LEFT JOIN` присоединяет карту идентичностей к сессиям по `client_id`. Сессии без известного пользователя тоже участвуют в расчете посетителей."
links: links:
- label: SQL - label: SQL
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dm/daily_traffic_rebuild.sql url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dm/daily_traffic_rebuild.sql
@@ -637,9 +635,9 @@ edges:
to: dm_revenue to: dm_revenue
mechanism: airflow_dag mechanism: airflow_dag
streams: [orders] streams: [orders]
rhythm: пачкой, при каждом прогоне `dm_transform` rhythm: при каждом запуске `dm_transform`
actor: Задачи `revenue_daily.rebuild` и `revenue_daily.replace` дага `dm_transform`. actor: Задачи `revenue_daily.rebuild` и `revenue_daily.replace` дага `dm_transform`.
join: 'Соединения таблиц нет: ARRAY JOIN раскрывает `item_sku`, `item_quantity`, `item_price`; `order_date` становится `report_date`.' join: "`ARRAY JOIN` разворачивает массивы `item_sku`, `item_quantity`, `item_price` в строки позиций заказа. Выручка считается по оплаченным заказам; `order_date` записывается в `report_date`."
links: links:
- label: SQL сборки - label: SQL сборки
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dm/revenue_daily_rebuild.sql url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dm/revenue_daily_rebuild.sql
@@ -653,9 +651,9 @@ edges:
to: dm_revenue to: dm_revenue
mechanism: airflow_dag mechanism: airflow_dag
streams: [orders] streams: [orders]
rhythm: пачкой, при каждом прогоне `dm_transform` rhythm: при каждом запуске `dm_transform`
actor: Даг `dm_transform` получает категорию через `dictGet` по `item_sku`; ключ словаря — `sku`. actor: "Задача `revenue_daily.rebuild` дага `dm_transform`."
join: 'Категория читается через `dictGet` по `item_sku`; ключ справочника - `sku`.' join: "Функция `dictGet` получает категорию товара по артикулу `item_sku` из заказа. В словаре этому артикулу соответствует ключ `sku`."
links: links:
- label: SQL - label: SQL
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dm/revenue_daily_rebuild.sql url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dm/revenue_daily_rebuild.sql
@@ -667,9 +665,9 @@ edges:
to: dm_purchase to: dm_purchase
mechanism: airflow_dag mechanism: airflow_dag
streams: [events] streams: [events]
rhythm: пачкой, при каждом прогоне `dm_transform` rhythm: при каждом запуске `dm_transform`
actor: Задачи `purchase_vs_orders.rebuild` и `purchase_vs_orders.replace` дага `dm_transform` читают покупки. actor: Задачи `purchase_vs_orders.rebuild` и `purchase_vs_orders.replace` дага `dm_transform` читают покупки.
join: 'Покупки группируются по `purchase_id[1]` как `purchase_order_id`; `event_date` становится `purchase_day`, затем GLOBAL FULL OUTER JOIN `purchase_order_id = backend_order_id`.' join: "События покупки группируются по номеру `purchase_id[1]`, который получает имя `purchase_order_id`. Дата `event_date` записывается в `purchase_day`. Затем `GLOBAL FULL OUTER JOIN` сопоставляет покупки с заказами по `purchase_order_id = backend_order_id`."
links: links:
- label: SQL сборки - label: SQL сборки
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dm/purchase_vs_orders_rebuild.sql url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dm/purchase_vs_orders_rebuild.sql
@@ -683,9 +681,9 @@ edges:
to: dm_purchase to: dm_purchase
mechanism: airflow_dag mechanism: airflow_dag
streams: [orders] streams: [orders]
rhythm: пачкой, при каждом прогоне `dm_transform` rhythm: при каждом запуске `dm_transform`
actor: Задачи `purchase_vs_orders.rebuild` и `purchase_vs_orders.replace` дага `dm_transform` читают заказы. actor: Задачи `purchase_vs_orders.rebuild` и `purchase_vs_orders.replace` дага `dm_transform` читают заказы.
join: 'GLOBAL FULL OUTER JOIN: `purchase_order_id = backend_order_id` (`purchase_id[1]` и `order_id`); отсутствующая сторона получает NULL.' join: "`order_id` заказа получает имя `backend_order_id`, а `purchase_id[1]` покупки — `purchase_order_id`. `GLOBAL FULL OUTER JOIN` соединяет их по `purchase_order_id = backend_order_id`. Покупки без заказа и заказы без покупки сохраняются; поля отсутствующей стороны содержат `NULL`."
links: links:
- label: SQL сборки - label: SQL сборки
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dm/purchase_vs_orders_rebuild.sql url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/sql/dm/purchase_vs_orders_rebuild.sql
+7 -7
View File
@@ -243,9 +243,9 @@
details.append(heading, meta); details.append(heading, meta);
addSection("Что это", node.what); addSection("Что это", node.what);
addSection("Как читать", node.read); addSection("Как читать", node.read);
addSection("Из чего состоит", node.parts); addSection("Связанные таблицы и представления", node.parts);
addSection("Ключи", node.keys); addSection("Как связать данные", node.keys);
addSection("Кто наполняет и как часто", node.fill); addSection("Как обновляются данные", node.fill);
addLinks(node.links); addLinks(node.links);
} }
function renderEdgeCard(edge) { function renderEdgeCard(edge) {
@@ -253,10 +253,10 @@
const heading = document.createElement("h2"); const heading = document.createElement("h2");
heading.textContent = `${nodeById.get(edge.from).name} → ${nodeById.get(edge.to).name}`; heading.textContent = `${nodeById.get(edge.from).name} → ${nodeById.get(edge.to).name}`;
details.append(heading); details.append(heading);
addSection("Механизм", mechanismById.get(edge.mechanism).label); addSection("Способ обработки", mechanismById.get(edge.mechanism).label);
addSection("Ритм", edge.rhythm); addSection("Когда выполняется", edge.rhythm);
addSection("Кто именно", edge.actor); addSection("Что выполняет обработку", edge.actor);
addSection("Как соединяет", edge.join); addSection("Что происходит с данными", edge.join);
addLinks(edge.links); addLinks(edge.links);
} }
function layoutMap() { function layoutMap() {
+34 -26
View File
@@ -1,42 +1,49 @@
# Генератор: откуда берутся данные # Генератор: откуда берутся данные
Генератор изображает две системы интернет-магазина. Первая — счетчик на сайте, Генератор имитирует работу двух систем интернет-магазина. Счетчик
он шлет события в топик `hits`. Вторая — бэкенд магазина, он раз в модельные на сайте отправляет записи о действиях посетителей в топик `hits`. Серверная часть
сутки выгружает слепок заказов в топик `orders`. магазина раз в модельные сутки выгружает состояние заказов в топик `orders`.
Эта дневная выгрузка называется слепком.
## Что он производит ## Что он производит
Событие — широкая строка по образцу выгрузки Яндекс Метрики. Генератор создает Событие содержит сведения о действии посетителя: например, время,
три вида событий: просмотр страницы, корзину и покупку. Все колонки перечислены адрес страницы и идентификатор посетителя. Формат соответствует образцу
выгрузки Яндекс Метрики. Генератор создает три вида событий: просмотр
страницы, добавление в корзину и покупку. Все колонки перечислены
в [описании выгрузки](../formats/clickstream-event.md). в [описании выгрузки](../formats/clickstream-event.md).
Заказ — та же покупка, но записанная бэкендом магазина, с суммами в деньгах. Заказ содержит сведения о покупке со стороны магазина: товары, суммы
Семь модельных дней после оформления заказ еще может измениться, поэтому каждый и состояние заказа. После оформления он еще может измениться, например
дневной слепок несет заказы за последние семь дней в их текущем состоянии. при отмене. Каждый дневной слепок содержит текущее состояние заказов,
Устройство и форма слепка разобраны в [отдельном оформленных за последние семь модельных дней. Состав и формат слепка
разобраны в [отдельном
справочнике](../architecture/orders/snapshot.md). справочнике](../architecture/orders/snapshot.md).
## Почему данные всегда одни и те же ## Почему данные всегда одни и те же
Генератор не берет случайность извне. Все его случайные решения выводятся из Случайные решения генератора зависят от заданного числа —
[зерна](../architecture/generator/seeds.md) — одного числа в настройках — и из [зерна генерации](../architecture/generator/seeds.md) — и номера дня.
номера дня. То же зерно и тот же номер дня дают ровно те же события, поэтому При неизменных настройках и версии генератора одинаковые зерно и день
стартовый мир одинаков при каждом `make up`. дают одинаковые события. Поэтому при создании стенда вы каждый раз
получаете те же начальные данные. Повторный `make up` на работающем стенде
сохраняет уже загруженные данные.
Рядом лежит паспорт мира, `data/world-inventory.json`: зерно, версия генератора В файле `data/world-inventory.json` записаны зерно, версия генератора,
и по строке на каждый день с числом событий и контрольной суммой. По нему число событий и контрольная сумма за каждый из восьми начальных дней.
проверяют, что генератор собрал тот же мир, что и раньше, и что в ODS все В документации этот файл называется паспортом мира. С ним можно сверить
доехало без потерь. Паспорт описывает только восемь дней стартового мира. результат генерации и проверить, все ли события загрузились в ODS.
## Кто его запускает ## Кто его запускает
На стенде генератором управляют [даги модельного На стенде генератором управляют [даги модельного
времени](airflow-dags.md#даги-мира). Его можно запустить и напрямую. Команды времени](airflow-dags.md#даги-мира). Его можно запустить и напрямую. Команды
собраны в [справочнике мира](../architecture/world.md#как-запустить-генератор). собраны в [справочнике генератора и модельного
времени](../architecture/world.md#как-запустить-генератор).
У генератора два режима. Пакетный отправляет весь день разом. Живой шлет У генератора два режима. Пакетный отправляет все события дня без пауз.
события по ходу модельных суток; сутки идут ускоренно и занимают около двадцати В живом режиме события поступают постепенно, как в работающем магазине,
четырех минут. но время идет ускоренно: модельные сутки занимают около 24 минут.
## Посмотрите сами ## Посмотрите сами
@@ -50,13 +57,14 @@ GROUP BY EventDate
ORDER BY EventDate; ORDER BY EventDate;
``` ```
Сравните первые восемь дней с восемью строками в [паспорте Сравните число событий за каждый из первых восьми дней со значением
мира](../../data/world-inventory.json): числа совпадут. Дни, сыгранные после `events` за ту же дату в [паспорте мира](../../data/world-inventory.json).
стартового мира, в паспорте не записаны. Так и задумано. Числа должны совпасть. Если вы уже генерировали следующие дни,
в результате запроса будут дополнительные даты; в паспорте их нет.
## Куда дальше ## Куда дальше
Подробнее генератор разобран в [README папки Подробнее генератор разобран в [README папки
`generator/`](../../generator/README.md) и в [справочнике его `generator/`](../../generator/README.md) и в [справочнике его
устройства](../architecture/generator/README.md). На этом путеводитель устройства](../architecture/generator/README.md). Теперь можно переходить
заканчивается: дальше справочники и [учебные задания](../labs/README.md). к [учебным заданиям](../labs/README.md).
+43 -31
View File
@@ -1,52 +1,61 @@
# Мониторинг: куда смотреть # Мониторинг: куда смотреть
В Grafana три дашборда, и у каждого своя тема. «Данные» показывает, все ли в В Grafana настроены три дашборда. «Данные» показывает загрузку и обработку
порядке с потоком данных. «Кластер» — состояние машин и самого ClickHouse. событий и заказов, «Кластер» — состояние узлов и самого ClickHouse,
«Запросы» — какие запросы выполнялись и сколько ресурсов они заняли. а «Запросы» — выполненные запросы и затраченные на них ресурсы.
Prometheus собирает метрики с двух узлов ClickHouse, keeper и экспортера Kafka. Prometheus собирает метрики с двух узлов ClickHouse, ClickHouse Keeper
Grafana читает и Prometheus, и ClickHouse напрямую. На панелях нет порогов и и экспортера Kafka — службы, которая предоставляет метрики Kafka.
цветовой подсветки: оценивать картинку придется самому. Автоматические Grafana получает данные из Prometheus и напрямую из ClickHouse.
проверки, которые выносят вердикт, собраны в контуре DQ. На панелях не заданы пороги для цветовых предупреждений: оценивать показания
вы будете самостоятельно. Качество данных автоматически проверяет
даг `dq_check`.
## Дашборд «Данные» ## Дашборд «Данные»
«Свежесть» показывает, сколько времени прошло с последнего события и с «Свежесть» показывает, сколько времени прошло с последнего события и с
последнего заказа в ODS. Свежесть заказов растет весь модельный день и последнего заказа в ODS. Показатель для заказов растет весь модельный день
сбрасывается, когда на границе суток приходит слепок. Это нормальное поведение и снижается, когда поступает очередная дневная выгрузка — слепок.
источника, а не поломка. Это ожидаемо: заказы загружаются раз в день.
«Поток» показывает число сырых сообщений в STG за выбранный интервал. «Брак» «Поток» показывает число сырых сообщений в STG за выбранный интервал. «Брак»
рядом — доля строк, которые не удалось разобрать. показывает долю строк, которые не удалось разобрать.
«Сходимость» сравнивает число событий в Kafka и в ClickHouse. Тревожный «Сходимость» сравнивает число событий в Kafka и в ClickHouse. Тревожный
признак — устойчивый разрыв между линиями. Расхождение в одном столбце ничего признак — устойчивый разрыв между рядами. Разовое расхождение еще не говорит
не значит. о потере данных: метрики Kafka и строки ClickHouse могут попадать в соседние
интервалы на графике.
«Отставание» показывает, сколько сообщений в Kafka еще не прочитано «Отставание» показывает, сколько сообщений в Kafka еще не прочитано
потребителями. «Дневной слепок» перечисляет последние десять слепков заказов в потребителями. «Дневной слепок» перечисляет последние десять слепков заказов в
STG: по нему видно, пришла ли выгрузка вовремя. STG: по нему видно, поступила ли очередная выгрузка.
Источники и правила чтения всех панелей разобраны в [справочнике дашборда Источники и правила чтения всех панелей разобраны в [справочнике дашборда
«Данные»](../architecture/monitoring/data.md). «Данные»](../architecture/monitoring/data.md).
## Почему у панелей разные источники ## Почему у панелей разные источники
Свежесть считается по ODS: если разбор остановился, это видно именно там. Поток Свежесть считается по ODS: если новые сообщения перестали разбираться,
и брак считаются по STG, где сохраняется каждое доставленное сообщение. В ODS показатель будет расти. Число поступивших сообщений берется из STG,
повторы события и старые версии заказа схлопываются, поэтому счетчик там был бы где сохраняются все сообщения, включая повторы. Для доли брака это число
занижен. Подробности — в [разделе о слоях сравнивается с числом строк в таблицах ошибок ODS.
В основных таблицах ODS повторы событий и старые версии заказов удаляются
при слияниях. Поэтому число строк в них не подходит для подсчета
всех доставленных сообщений. Подробности — в [разделе о слоях
панелей](../architecture/monitoring/data.md#почему-слои-у-панелей-разные). панелей](../architecture/monitoring/data.md#почему-слои-у-панелей-разные).
## Два других дашборда ## Два других дашборда
Открывайте «Кластер», когда нужно понять, что происходит с процессором, памятью Открывайте «Кластер», чтобы проверить нагрузку на процессор, использование
и дисками, с кусками данных и их слияниями в ClickHouse, с keeper или очередью памяти и дисков, число частей таблиц и ход их слияний в ClickHouse. Здесь же
DDL. Устройство панелей описано в [справочнике дашборда показаны состояние ClickHouse Keeper и очередь распределенных DDL-команд.
Устройство панелей описано в [справочнике дашборда
«Кластер»](../architecture/monitoring/cluster.md). «Кластер»](../architecture/monitoring/cluster.md).
«Запросы» пригодится, когда вы ищете тяжелые запросы, ошибки или расход «Запросы» поможет найти медленные запросы, ошибки и пользователей,
ресурсов по пользователям. Как читать списки, объясняет [справочник дашборда чьи запросы потребляют много ресурсов. Как читать панели, объясняет
[справочник дашборда
«Запросы»](../architecture/monitoring/queries.md). «Запросы»](../architecture/monitoring/queries.md).
## Посмотрите сами ## Посмотрите сами
@@ -57,15 +66,18 @@ Airflow [`world_live_day`](airflow-dags.md#даги-мира): модельны
около двадцати четырех минут. около двадцати четырех минут.
Смотрите свежесть событий и свежесть заказов по отдельности. Пока день идет, Смотрите свежесть событий и свежесть заказов по отдельности. Пока день идет,
свежесть событий держится на секундах, а свежесть заказов растет. На границе дня со времени последнего события проходят секунды, а время с последней
слепок отправляется сразу: свежесть заказов сбрасывается, а свежесть событий загрузки заказов растет. После завершения генерации событий отправляется
начинает расти, потому что новых событий до следующего дня не будет. Сам даг слепок заказов. Когда он загружен, показатель свежести заказов снижается.
остается в работе и после конца дня, потому что ждет дневной конвейер. Показатель для событий начинает расти: новые события поступят только
при генерации следующего дня. Даг пока продолжает работу, ожидая завершения
обработки данных в дневном конвейере.
## Куда дальше ## Куда дальше
Состав мониторинга собран в [справочнике Устройство мониторинга описано в [техническом
зоны](../architecture/monitoring/README.md), детали главного экрана — в справочнике](../architecture/monitoring/README.md), панели дашборда «Данные» —
[справочнике дашборда «Данные»](../architecture/monitoring/data.md), а границы в [отдельном разделе](../architecture/monitoring/data.md).
решения — в [ADR 0002](../adr/0002-monitoring-scope.md). Следующая глава — Почему выбраны именно эти показатели, объясняет
[ADR 0002](../adr/0002-monitoring-scope.md). Следующая глава —
[«Немного Superset»](superset.md). [«Немного Superset»](superset.md).
+51 -52
View File
@@ -2,7 +2,8 @@
В хранилище приходят два потока из Kafka: в топике `hits` — события В хранилище приходят два потока из Kafka: в топике `hits` — события
кликстрима, в `orders` — заказы магазина. Оба проходят один путь: STG → ODS → кликстрима, в `orders` — заказы магазина. Оба проходят один путь: STG → ODS →
DDS → DM. Каждый слой отвечает на свой вопрос. DDS → DM. В STG сообщения сохраняются как есть, в ODS разбираются по полям,
в DDS из них собираются события, заказы и сессии, а в DM — витрины для отчетов.
```mermaid ```mermaid
flowchart LR flowchart LR
@@ -11,54 +12,54 @@ flowchart LR
## Как читать имена ## Как читать имена
Для обычного чтения выбирайте объекты с суффиксом `_v`. Это представления: Для обычного чтения выбирайте представления с суффиксом `_v`. Они позволяют
их устройство может поменяться, а ваш запрос останется прежним. Суффиксы менять устройство таблиц, сохраняя привычные запросы к данным.
`_rep`, `_dist`, `_kafka` и `_mv` относятся к внутреннему Суффиксы `_rep`, `_dist`, `_kafka` и `_mv` относятся к внутреннему устройству
устройству хранилища. Они нужны для загрузки, обслуживания и диагностики. хранилища. Эти объекты нужны для загрузки, обслуживания и диагностики.
У словарей есть исключение: у `dic.products` нет суффикса, потому что У словаря товаров `dic.products` суффикса нет.
различать нечего — объект один.
Подробный разбор имен есть в [справочнике Подробный разбор имен есть в [справочнике
хранилища](../architecture/storage.md#имена-объектов). хранилища](../architecture/storage.md#имена-объектов).
## STG: как поступило ## STG: как поступило
STG — сырой слой. Он сохраняет сообщение целой строкой, без разбора. Рядом STG — сырой слой. Он сохраняет сообщение целой строкой, без разбора. Вместе
лежат метаданные доставки: топик, партиция, смещение и время приема. Поэтому в с ним записываются сведения о доставке: топик, партиция, смещение и время
STG можно увидеть, что именно приехало из Kafka, даже если сообщение приема. Содержимое сообщения сохраняется в STG, даже если в нем есть ошибки.
испорчено.
Срок хранения — трое суток реального времени. Этого хватает для расследования Сырые сообщения хранятся трое суток реального времени. В течение этого срока
и повторного разбора, но старые строки не занимают диск вечно. Точное правило их можно использовать для поиска ошибок и повторной обработки. Затем они
— в [справочнике удаляются по правилу из [справочника
хранилища](../architecture/storage.md#срок-жизни-сырых-данных). хранилища](../architecture/storage.md#срок-жизни-сырых-данных).
## ODS: язык источника ## ODS: язык источника
ODS — слой разобранных данных. Строка получает типы: дата становится датой, В ODS содержимое сообщения разбирается на колонки с нужными типами данных:
число — числом. Имена полей остаются на языке источника. Например, событие датами, числами, строками. Колонки сохраняют имена из источника. Например,
сохраняет колонки `WatchID`, `ClientID` и `EventDate`. в таблице событий есть `WatchID`, `ClientID` и `EventDate`.
Испорченное сообщение не останавливает загрузку. Оно попадает в таблицу Испорченное сообщение не останавливает загрузку. Оно попадает в таблицу
`*_errors` вместе с сырой строкой и классом ошибки. Так брак остается виден, а `*_errors` вместе с исходной строкой и классом ошибки. Правильно записанные
годные строки продолжают движение. сообщения обрабатываются дальше.
## DDS: язык бизнеса ## DDS: язык бизнеса
DDS — детальный слой на языке бизнеса. Здесь есть событие, заказ, сессия и DDS — детальный слой: в нем данные подготовлены для анализа событий,
карта идентичностей. Карта связывает посетителя, то есть куку `ClientID`, с заказов и сессий. Здесь же хранится карта идентичностей — таблица связей
пользователем магазина из заказа. между идентификатором посетителя `ClientID` и пользователем магазина
из заказа. `ClientID` хранится в cookie браузера: по нему можно узнать
браузер посетителя, но не всегда самого человека.
В стенде визитом считают подряд идущие события одной куки без пауз В стенде визитом, или сессией, считают подряд идущие события одного
дольше 30 минут. Визит не пересекает границу модельных суток. `ClientID` без пауз дольше 30 минут. Визит не пересекает границу модельных суток.
Аналитик называет такой визит сессией.
В бою Метрика нарезает визиты сама и добавляет готовый `VisitID` в Источник передает готовый идентификатор визита `VisitID` по образцу
события. Стенд играет ее роль: считает сессии из событий заново, а `VisitID` Яндекс Метрики. В учебном хранилище сессии рассчитываются заново по времени
оставляет эталоном для проверки. Иначе сборка сессий свелась бы к группировке событий; каждой присваивается `session_id`. Поле `VisitID` остается
по чужому полю, и учиться было бы не на чем. Идентификатор сессии эталоном для проверки. Так в заданиях вы сможете самостоятельно определить
`session_id` чеканит стенд: в событии его нет. Готовые сессии читайте через границы сессий и сравнить их с результатом источника.
`dds.session_v`. В `dds.event_v` поля `VisitID` нет: так сессии можно собрать
самому и сравнить с эталоном. Готовые сессии читайте через `dds.session_v`. События для самостоятельного
расчета доступны в `dds.event_v`: в этом представлении `VisitID` скрыт.
## DM: данные для дашбордов ## DM: данные для дашбордов
@@ -70,32 +71,31 @@ DM — слой витрин. Витрина — готовый набор по
путь расчета. Состав готовых витрин смотрите в [карте путь расчета. Состав готовых витрин смотрите в [карте
таблиц](../architecture/storage.md#карта-таблиц). таблиц](../architecture/storage.md#карта-таблиц).
## Сбоку цепочки: зона `dic` ## Справочные данные: зона `dic`
Зона `dic` стоит вне пути STG → ODS → DDS → DM. В ней живут справочные данные, В базе `dic` хранятся готовые справочные данные. Они загружаются отдельно
которые хранилище получает готовыми. Ни один слой их не производит, а читать от событий и заказов и могут использоваться в расчетах любого слоя.
может любой.
Первый справочник стенда — каталог товаров `dic.products`. Его ключ `sku` — Каталог товаров `dic.products` — один из таких справочников. Его ключ `sku` —
артикул товара. По нему можно узнать название, бренд и категорию товара. Как артикул товара. По нему можно узнать название, бренд и категорию товара. Как
устроена зона, смотрите в [разделе о устроена зона, смотрите в [разделе о
справочниках](../architecture/storage.md#справочники). справочниках](../architecture/storage.md#справочники).
## Сквозные ключи ## Сквозные ключи
Один и тот же смысл проходит через слои под разными именами: имя меняется Идентификаторы позволяют связать данные из разных слоев. При переходе в DDS
вместе с языком слоя. Таблица показывает исходное имя и то, где искать тот же часть колонок переименовывается: например, `WatchID` становится `event_id`.
смысл дальше. В таблице указано, как найти соответствующие колонки и соединить по ним данные.
| Колонка | Что означает | Что соединяет | | Колонка | Что означает | Что соединяет |
|---|---|---| |---|---|---|
| `WatchID` | Идентификатор события | Событие ODS с событием DDS, где колонка называется `event_id` | | `WatchID` | Идентификатор события | Событие ODS с событием DDS, где колонка называется `event_id` |
| `ClientID` | Посетитель, то есть кука | События ODS с событиями, сессиями и картой идентичностей DDS; в DDS колонка называется `client_id` | | `ClientID` | Идентификатор посетителя из cookie браузера | События ODS с событиями, сессиями и картой идентичностей DDS; в DDS колонка называется `client_id` |
| `order_id` | Идентификатор заказа | Заказ ODS с заказом DDS и витриной сверки `dm.purchase_vs_orders_v` | | `order_id` | Идентификатор заказа | Заказ ODS с заказом DDS и витриной сверки `dm.purchase_vs_orders_v` |
| `sku` | Артикул товара в каталоге | Справочник `dic.products` с позициями заказа DDS, где артикулы лежат в `item_sku`; через категорию товара — с витриной выручки DM | | `sku` | Артикул товара в каталоге | Справочник `dic.products` с позициями заказа DDS, где артикулы лежат в `item_sku`; через категорию товара — с витриной выручки DM |
| `EventDate` | День на [оси модельного времени](../../CONTEXT.md), который счетчик сайта проставляет в [своем часовом поясе](../architecture/storage.md#часовые-пояса) | Событие ODS с событием DDS, где колонка называется `event_date`, и дневными расчетами DM | | `EventDate` | Дата события в [модельном времени](../../CONTEXT.md), указанная счетчиком сайта в [его часовом поясе](../architecture/storage.md#часовые-пояса) | Событие ODS с событием DDS, где колонка называется `event_date`, и дневными расчетами DM |
Что здесь значит «посетитель» и чем кука отличается от человека, объясняет Почему идентификатор посетителя не равнозначен человеку, объясняет
[словарь понятий](../../CONTEXT.md#язык). Точные имена объектов и полей сверяйте с [словарь понятий](../../CONTEXT.md#язык). Точные имена объектов и полей сверяйте с
[картой таблиц](../architecture/storage.md#карта-таблиц). [картой таблиц](../architecture/storage.md#карта-таблиц).
@@ -104,10 +104,10 @@ DM — слой витрин. Витрина — готовый набор по
Подключитесь к ClickHouse по инструкции из разделов [«Адреса и Подключитесь к ClickHouse по инструкции из разделов [«Адреса и
пароли»](../../README.md#адреса-и-пароли) и [«Подключиться из пароли»](../../README.md#адреса-и-пароли) и [«Подключиться из
DBeaver»](../../README.md#подключиться-из-dbeaver). Во всех трех примерах DBeaver»](../../README.md#подключиться-из-dbeaver). Во всех трех примерах
`2026-06-01` — это D0, первый день [стартового `2026-06-01` — первый день работы учебного магазина. В документации он
мира](../../CONTEXT.md). Чтобы посмотреть другой день, замените обозначается D0; подробнее — в [словаре понятий](../../CONTEXT.md).
эту дату в условии. Первый запрос покажет Чтобы посмотреть другой день, замените дату в условии.
события одного модельного дня: Первый запрос покажет 20 событий за выбранный день:
```sql ```sql
SELECT SELECT
@@ -122,8 +122,8 @@ ORDER BY UTCEventTime
LIMIT 20; LIMIT 20;
``` ```
Второй запрос покажет сессии того же дня. Они рассчитаны из событий Второй запрос покажет 20 сессий того же дня с наибольшим числом событий.
первого запроса: Сессии рассчитаны по всем событиям дня:
```sql ```sql
SELECT SELECT
@@ -153,14 +153,13 @@ WHERE report_date = '2026-06-01'
ORDER BY revenue DESC; ORDER BY revenue DESC;
``` ```
Все три запроса читают поверхности `_v`. Первый показывает данные на языке Все три запроса читают представления `_v`.
источника, второй — расчетную сущность, третий — готовый ответ витрины.
## Куда дальше ## Куда дальше
Откройте [интерактивную карту](architecture-map.html) из своей копии Откройте [интерактивную карту](architecture-map.html) из своей копии
репозитория и проследите путь данных. репозитория и проследите путь данных.
Список объектов, сроки хранения и устройство таблиц — в [справочнике Список объектов, сроки хранения и устройство таблиц — в [справочнике
хранилища](../architecture/storage.md). Как данные переносят между слоями — в хранилища](../architecture/storage.md). Как данные обрабатываются между слоями — в
следующей главе: [потоки Airflow и даги модельного следующей главе: [потоки Airflow и даги модельного
времени](airflow-dags.md). времени](airflow-dags.md).
+31 -24
View File
@@ -1,42 +1,49 @@
# Немного Superset # Немного Superset
Superset показывает витрины слоя DM в виде графиков. К ClickHouse он Superset показывает витрины слоя DM в виде графиков. К ClickHouse он
подключается пользователем `bi`. На дашборде стенда «Бизнес-сюжеты» три подключается пользователем `bi`. Дашборд стенда «Бизнес-сюжеты» собран из трех
графика: витрин:
| График | Витрина | | Витрина | Что показывает |
|---|---| |---|---|
| «Выручка по дням и категориям» | `dm.revenue_daily_v` | | `dm.revenue_daily_v` | Выручку, число заказов и средний чек; выручку по дням и категориям товаров, долю каждой категории |
| «Сверка purchase и заказов» | `dm.purchase_vs_orders_v` | | `dm.purchase_vs_orders_v` | Результат сверки покупок из кликстрима с заказами: сколько заказов за день относится к каждому виду расхождения |
| «Посетители и известные пользователи» | `dm.daily_traffic_v` | | `dm.daily_traffic_v` | Число посетителей и известных пользователей по дням, долю известных пользователей |
Известные пользователи — те, кого удалось связать с посетителями сайта
по заказам. Эти связи хранятся в карте идентичностей DDS.
Слева от панелей стоит фильтр «Период». Он задает даты сразу для всех панелей.
## Как витрина становится графиком ## Как витрина становится графиком
От витрины до экрана три объекта Superset: датасет → график → дашборд. Датасет В Superset сначала создается датасет — описание таблицы или представления,
описывает витрину: задает русские имена колонок и формат денег. График из которого будут браться данные. В нем задаются подписи колонок, формат
ссылается на эти имена и задает способ показа. Дашборд собирает готовые графики чисел и показатели, например средний чек.
на одном экране.
Затем на основе датасета создается график: выбираются показатели и способ
их отображения. Несколько графиков можно разместить на одном дашборде.
## Стендовые объекты и ваши ## Стендовые объекты и ваши
Датасеты, графики и дашборд из поставки стенда лежат файлами в репозитории и Готовые датасеты, графики и дашборд сохранены в файлах репозитория
заново импортируются при каждом `make up`. Если поправить такой объект в и импортируются при каждом `make up`. Если изменить их через интерфейс,
интерфейсе, правка проживет до следующего `make up`, а потом без предупреждения следующий `make up` заменит изменения версией из файлов без предупреждения.
заменится версией из файла.
Ваши собственные объекты хранятся в той же базе метаданных Superset, но импорт Созданные вами датасеты, графики и дашборды хранятся в базе метаданных
их не трогает: они переживают и `make up`, и `make rebuild-storage`. Стирает их Superset. Импорт их не меняет: они сохраняются после `make up`
только `make clean` вместе с остальным постоянным состоянием стенда. Поэтому и `make rebuild-storage`. Команда `make clean` удалит их вместе со всеми
стендовые объекты оставляйте как есть, а свои отчеты стройте рядом. Устройство данными стенда. Для учебных отчетов создавайте собственные объекты,
чтобы повторный запуск не перезаписал вашу работу. Устройство
импорта описано в [справочнике импорта описано в [справочнике
Superset](../architecture/superset.md#импорт-при-каждой-инициализации). Superset](../architecture/superset.md#импорт-при-каждой-инициализации).
## Почему края графиков не совпадают ## Почему выручка отстает на день
Правый край графика выручки всегда на день позади графиков кликстрима. Слепок Последняя дата с выручкой на день раньше последней даты с событиями
заказов за день приходит только со следующим модельным днем. Так устроен кликстрима. Выгрузка заказов за день D поступает при генерации следующего
источник, а не витрина. Вы уже видели этот дня D+1, а затем обрабатывается конвейером. Вы уже видели эту разницу
сдвиг в [главе об Airflow](airflow-dags.md#посмотрите-сами). Начальное состояние в [главе об Airflow](airflow-dags.md#посмотрите-сами). Начальное состояние
показано в разделе [«Стартовый мир»](../architecture/world.md#стартовый-мир). показано в разделе [«Стартовый мир»](../architecture/world.md#стартовый-мир).
## Посмотрите сами ## Посмотрите сами
@@ -56,7 +63,7 @@ LIMIT 7;
## Куда дальше ## Куда дальше
Устройство BI-зоны описано в [справочнике Подключение и настройка Superset описаны в [справочнике
Superset](../architecture/superset.md), а состав витрин — в [справочнике Superset](../architecture/superset.md), а состав витрин — в [справочнике
DM](../architecture/dm.md). Следующая глава — [«Генератор: откуда берутся DM](../architecture/dm.md). Следующая глава — [«Генератор: откуда берутся
данные»](generator.md). данные»](generator.md).
+18 -14
View File
@@ -1,13 +1,15 @@
# Задание 1: `dm.events_enriched_v` # Задание 1: `dm.events_enriched_v`
**Чему учит:** строить слой как договор о данных, а не как их копию. **Чему учит:** подготовить удобный для анализа набор полей события
Оставлять витрину представлением, если зерно источника не меняется. и обойтись представлением, сохранив одну строку на событие.
## Сначала прочитать ## Сначала прочитать
- [«Хранилище и его слои»](../handbook/storage-layers.md). - [«Хранилище и его слои»](../handbook/storage-layers.md).
- [Как применить свой DDL](README.md#как-применить-свой-ddl): файл для решения
и подключение с правами на создание витрины.
## Роль витрины ## Для чего нужна витрина
В `dds.event_v` 44 колонки, включая технические поля источника и массивы В `dds.event_v` 44 колонки, включая технические поля источника и массивы
товаров. Для анализа событий в Superset нужны деловые поля и три готовых товаров. Для анализа событий в Superset нужны деловые поля и три готовых
@@ -70,6 +72,13 @@ DDS читают из ODS без отдельной загрузки. Эти с
## Как проверить себя ## Как проверить себя
После применения DDL откройте Airflow по [адресу из README](../../README.md#адреса-и-пароли),
найдите даг `homework_check` и запустите его вручную. В новом запуске откройте
задачу `events_enriched`: она должна завершиться успешно. Если задача
красная, откройте ее журнал (`Logs`) — там будет причина ошибки, а при
расхождениях с источником — дни и примеры несовпадений. Задачи еще не
созданных витрин будут красными; это ожидаемо и не мешает проверке первой.
Деловой ключ — `event_id` внутри дня. Получите результат прямо из Деловой ключ — `event_id` внутри дня. Получите результат прямо из
`dds.event_v` за тот же день. Соедините его с витриной полным внешним `dds.event_v` за тот же день. Соедините его с витриной полным внешним
соединением по `event_id`. Посчитайте строки, где ключ отсутствует с одной соединением по `event_id`. Посчитайте строки, где ключ отсутствует с одной
@@ -79,15 +88,10 @@ DDS читают из ODS без отдельной загрузки. Эти с
Отдельно сверьте `event_hour`, `page_path` и `referer_host` с теми же Отдельно сверьте `event_hour`, `page_path` и `referer_host` с теми же
выражениями, рассчитанными прямо по `dds.event_v`. выражениями, рассчитанными прямо по `dds.event_v`.
Если строк в витрине в разы меньше, чем в источнике, и остались только ## Что должно получиться
события корзины и покупки, это признак развернутых массивов: `ARRAY JOIN`
исключил все события с пустым массивом товаров.
## Приемка - [ ] у вас есть `dm.events_enriched_v` с явно перечисленными колонками;
- [ ] вы сверили число строк и набор `event_id` с `dds.event_v` за каждый
- [ ] представление `dm.events_enriched_v` создано без физических таблиц; прожитый день; расхождений нет;
- [ ] список колонок в представлении задан явно; - [ ] проверка `events_enriched` в `homework_check` прошла успешно;
- [ ] число строк и набор `event_id` совпадают с `dds.event_v` за каждый - [ ] витрина работает без физической таблицы и задачи в `dm_transform`.
прожитый день;
- [ ] задача `events_enriched` в `homework_check` завершилась успешно;
- [ ] задачи для этой витрины в `dm_transform` нет.
+12 -12
View File
@@ -1,14 +1,14 @@
# Задание 2: `dm.top_pages_daily` # Задание 2: `dm.top_pages_daily`
**Чему учит:** создавать витрину с новым зерном: пять объектов, донор и **Чему учит:** собрать дневные показатели страниц и настроить их
замена дневных партиций. Включать ее пересборку в `dm_transform`. обновление в `dm_transform` через замену партиций.
## Сначала прочитать ## Сначала прочитать
- [«Хранилище и его слои»](../handbook/storage-layers.md); - [«Хранилище и его слои»](../handbook/storage-layers.md);
- [«Потоки Airflow и даги модельного времени»](../handbook/airflow-dags.md). - [«Потоки Airflow и даги модельного времени»](../handbook/airflow-dags.md).
## Роль витрины ## Для чего нужна витрина
По дневным показателям страниц аналитик сравнивает просмотры, посетителей и По дневным показателям страниц аналитик сравнивает просмотры, посетителей и
действия с товарами. Добавление в корзину происходит на карточке товара, действия с товарами. Добавление в корзину происходит на карточке товара,
@@ -98,13 +98,13 @@
Сумма `visitors` по страницам повторно учитывает посетителя, открывшего Сумма `visitors` по страницам повторно учитывает посетителя, открывшего
несколько страниц. Число посетителей за день берите из `dm.daily_traffic_v`. несколько страниц. Число посетителей за день берите из `dm.daily_traffic_v`.
## Приемка ## Что должно получиться
- [ ] созданы пять объектов, донор объявлен через `AS` и шардируется ключом - [ ] у вас есть пять объектов витрины; донор объявлен через `AS` и
цели; использует тот же ключ шардирования, что и целевая таблица;
- [ ] в `dm.top_pages_daily_v` явно перечислены колонки, включая `_load_id` - [ ] в `dm.top_pages_daily_v` явно перечислены колонки, включая
и `_load_ts`; `_load_id` и `_load_ts`;
- [ ] объем прогона определяется по состоянию витрины без ручного задания; - [ ] ваш запрос сам определяет дни пересборки по состоянию витрины;
- [ ] задача `top_pages_daily` в `homework_check` завершилась успешно; - [ ] проверка `top_pages_daily` в `homework_check` прошла успешно;
- [ ] задача витрины добавлена в `dm_transform`, повторный прогон конвейера - [ ] вы включили витрину в `dm_transform` и убедились, что повторный
не меняет результат. прогон конвейера не меняет результат.
+14 -17
View File
@@ -1,24 +1,21 @@
# Задание 3: `dm.session_overview` # Задание 3: `dm.session_overview`
**Чему учит:** хранить слагаемые, а не готовые средние и доли. Понимать, **Чему учит:** хранить слагаемые для расчета средних и долей и понимать,
почему сумма `visitors` по строкам сейчас сходится, но начнет завышать когда сложение числа посетителей по устройствам завышает аудиторию.
аудиторию, если у одной куки появятся визиты с двух устройств.
## Сначала прочитать ## Сначала прочитать
- [«Хранилище и его слои»](../handbook/storage-layers.md); - [«Хранилище и его слои»](../handbook/storage-layers.md);
- [«Потоки Airflow и даги модельного времени»](../handbook/airflow-dags.md). - [«Потоки Airflow и даги модельного времени»](../handbook/airflow-dags.md).
## Роль витрины ## Для чего нужна витрина
Витрина нужна, чтобы по дням сравнивать визиты с телефона и компьютера: их Витрина нужна, чтобы по дням сравнивать визиты с телефона и компьютера: их
число, длительность, просмотры страниц и частоту покупок. число, длительность, просмотры страниц и частоту покупок.
На прошлом стенде витрина выделяла визиты сама. Теперь их рассчитывают в Сессии уже рассчитаны в DDS. Из них вы соберете дневные показатели по
DDS, а повторять зерно сессий ради представления с переименованными устройствам. Почему такие итоги хранят в отдельной таблице, объясняет
колонками незачем. Здесь вы учитесь дневной агрегации, поэтому зерно — день [справочник DM](../architecture/dm.md).
и устройство. Выбор описан в разделе «Форма: по зерну, не по цене запроса»
[справочника DM](../architecture/dm.md).
## Что сделать ## Что сделать
@@ -107,12 +104,12 @@ DDS, а повторять зерно сессий ради представле
повторного учета визитов; повторного учета визитов;
- сумма `events_total` равна сумме `events_total` по сессиям того же дня. - сумма `events_total` равна сумме `events_total` по сессиям того же дня.
## Приемка ## Что должно получиться
- [ ] созданы пять объектов, донор объявлен через `AS` и шардируется ключом - [ ] у вас есть пять объектов витрины; донор объявлен через `AS` и
цели; использует ключ шардирования целевой таблицы;
- [ ] в `dm.session_overview_v` колонки перечислены явно; - [ ] вы явно перечислили колонки в `dm.session_overview_v` и собрали
- [ ] источник — `dds.session_v`, повторного выделения визитов нет; показатели из готовых сессий `dds.session_v`;
- [ ] физическая таблица не хранит средние и доли; - [ ] в таблице хранятся слагаемые для средних и долей;
- [ ] задача `session_overview` в `homework_check` завершилась успешно; - [ ] проверка `session_overview` в `homework_check` прошла успешно;
- [ ] задача витрины добавлена в `dm_transform`. - [ ] витрина обновляется при запуске `dm_transform`.
+13 -15
View File
@@ -1,15 +1,14 @@
# Задание 4: `dm.dq_errors_daily` # Задание 4: `dm.dq_errors_daily`
**Чему учит:** считать брак по дню загрузки и сохранять сводку дольше **Чему учит:** считать брак по дню загрузки и сохранять сводку дольше
исходных строк. Пересборка всех партиций после удаления источника по `TTL` исходных строк.
стирает историю, которую уже не восстановить.
## Сначала прочитать ## Сначала прочитать
- [«Хранилище и его слои»](../handbook/storage-layers.md); - [«Хранилище и его слои»](../handbook/storage-layers.md);
- [«Потоки Airflow и даги модельного времени»](../handbook/airflow-dags.md). - [«Потоки Airflow и даги модельного времени»](../handbook/airflow-dags.md).
## Роль витрины ## Для чего нужна витрина
Сводку открывают при росте брака в потоке: смотрят, каких ошибок стало Сводку открывают при росте брака в потоке: смотрят, каких ошибок стало
больше и сколько строк забраковано. Причину конкретной ошибки исследуют больше и сколько строк забраковано. Причину конкретной ошибки исследуют
@@ -97,17 +96,16 @@
`ods.event_errors_dist` и `ods.order_errors_dist`. Если она примерно вдвое `ods.event_errors_dist` и `ods.order_errors_dist`. Если она примерно вдвое
меньше, вероятная причина — чтение `_rep` вместо `_dist`. меньше, вероятная причина — чтение `_rep` вместо `_dist`.
Пустая витрина на чистом стенде — верный ответ: обе стороны пусты, Если в источниках нет брака, витрина должна быть пустой. В этом случае
расхождений ноль, проверка успешна. Требование хотя бы одной строки дало бы расхождений тоже ноль и проверка успешна.
постоянную ошибку, которую со временем стали бы игнорировать.
## Приемка ## Что должно получиться
- [ ] созданы пять объектов, донор объявлен через `AS` и шардируется ключом - [ ] у вас есть пять объектов витрины; донор объявлен через `AS` и
цели; использует ключ шардирования целевой таблицы;
- [ ] источники — таблицы `_dist` обоих видов брака; - [ ] сводка включает ошибки событий и заказов из таблиц `_dist`;
- [ ] прогон заменяет партиции только дней, еще имеющихся в источниках, и - [ ] ваш запрос пересобирает только дни, еще доступные в источниках,
сохраняет дни за пределами их срока хранения; сохраняя более старые итоги;
- [ ] задача `dq_errors_daily` в `homework_check` завершилась успешно, в том - [ ] проверка `dq_errors_daily` в `homework_check` прошла успешно;
числе на чистом стенде без брака; отсутствие брака тоже дает успешный результат;
- [ ] задача витрины добавлена в `dm_transform`. - [ ] витрина обновляется при запуске `dm_transform`.
+12 -13
View File
@@ -1,15 +1,14 @@
# Задание 5: `dm.utm_effectiveness` # Задание 5: `dm.utm_effectiveness`
**Чему учит:** понимать предупреждение в приставке `declared_`: это сумма со **Чему учит:** различать сумму покупки со слов клиента (`declared_`) и
слов клиента, а не деньги отчетности. Самостоятельно находить причины серверную выручку, находить причины их расхождения.
расхождения двух сумм выручки.
## Сначала прочитать ## Сначала прочитать
- [«Хранилище и его слои»](../handbook/storage-layers.md); - [«Хранилище и его слои»](../handbook/storage-layers.md);
- [«Потоки Airflow и даги модельного времени»](../handbook/airflow-dags.md). - [«Потоки Airflow и даги модельного времени»](../handbook/airflow-dags.md).
## Роль витрины ## Для чего нужна витрина
Аналитик сравнивает рекламные каналы по дням: трафик, добавления в корзину, Аналитик сравнивает рекламные каналы по дням: трафик, добавления в корзину,
покупки и заявленные клиентом суммы. покупки и заявленные клиентом суммы.
@@ -44,7 +43,7 @@
| `_load_ts` | `DateTime64(3, 'UTC')` | Время сборки | | `_load_ts` | `DateTime64(3, 'UTC')` | Время сборки |
Для `declared_revenue` сложите `purchase_revenue[1]` событий `purchase`. В Для `declared_revenue` сложите `purchase_revenue[1]` событий `purchase`. В
параллельных массивах `purchase*` по контракту хранятся один номер заказа и параллельных массивах `purchase*` хранятся один номер заказа и
одна сумма на событие. В сверке `purchase_vs_orders` используют тот же одна сумма на событие. В сверке `purchase_vs_orders` используют тот же
индекс. индекс.
@@ -127,12 +126,12 @@
Сумма `visitors` по строкам повторно учитывает одного посетителя, Сумма `visitors` по строкам повторно учитывает одного посетителя,
приходящего по разным меткам. Для числа посетителей дня она не подходит. приходящего по разным меткам. Для числа посетителей дня она не подходит.
## Приемка ## Что должно получиться
- [ ] созданы пять объектов, донор объявлен через `AS` и шардируется ключом - [ ] у вас есть пять объектов витрины; донор объявлен через `AS` и
цели; использует ключ шардирования целевой таблицы;
- [ ] сохранены строки без UTM-меток; - [ ] строки без UTM-меток сохранены, а `declared_revenue` хранится
- [ ] `declared_revenue` хранится в `Decimal(18, 2)`; в `Decimal(18, 2)`;
- [ ] расхождение с `dm.revenue_daily_v` объяснено письменно; - [ ] вы записали, почему сумма отличается от `dm.revenue_daily_v`;
- [ ] задача `utm_effectiveness` в `homework_check` завершилась успешно; - [ ] проверка `utm_effectiveness` в `homework_check` прошла успешно;
- [ ] задача витрины добавлена в `dm_transform`. - [ ] витрина обновляется при запуске `dm_transform`.
+15 -14
View File
@@ -8,7 +8,7 @@
- [«Немного Superset»](../handbook/superset.md); - [«Немного Superset»](../handbook/superset.md);
- [задание 3: `dm.session_overview`](1-3-session-overview.md). - [задание 3: `dm.session_overview`](1-3-session-overview.md).
## Роль графика ## На какой вопрос ответит график
В задании 3 вы собрали витрину `dm.session_overview_v`. Теперь покажите, с В задании 3 вы собрали витрину `dm.session_overview_v`. Теперь покажите, с
каких устройств чаще уходят после первой страницы. Постройте столбиковый каких устройств чаще уходят после первой страницы. Постройте столбиковый
@@ -45,10 +45,10 @@
укажите «Категория устройства», а «Dimensions» не заполняйте. Дашборд укажите «Категория устройства», а «Dimensions» не заполняйте. Дашборд
создавать не нужно: сохраненного графика достаточно. создавать не нужно: сохраненного графика достаточно.
По умолчанию в графике выбран период «No filter». Оставьте его таким или Для этого задания оставьте период «No filter»: график и проверочный запрос
задайте явные даты. Относительный период, например «Last week», даст пустой ниже считают долю отказов за весь период витрины. Относительный период,
график: модельный календарь стенда отстает от настоящего. Последнюю дату например «Last week», может дать пустой график: модельный календарь стенда
витрины посмотрите в SQL Lab: отстает от настоящего. Последнюю дату витрины посмотрите в SQL Lab:
```sql ```sql
SELECT max(report_date) SELECT max(report_date)
@@ -60,8 +60,8 @@ FROM dm.session_overview_v;
## Как проверить себя ## Как проверить себя
График проверяется глазами, поэтому задачи для него в `homework_check` нет. Автоматической проверки этого задания нет. Сверьте числа на графике
В SQL Lab выполните запрос: с результатом запроса в SQL Lab:
```sql ```sql
SELECT device_category, SELECT device_category,
@@ -76,15 +76,16 @@ ORDER BY device_category;
проценты с одним знаком, как формат `.1%`, поэтому числа должны совпасть проценты с одним знаком, как формат `.1%`, поэтому числа должны совпасть
один в один. один в один.
## Приемка ## Что должно получиться
- [ ] создан физический датасет `dm.session_overview_v` в подключении - [ ] у вас есть физический датасет `dm.session_overview_v` в подключении
`ClickHouse`; `ClickHouse`;
- [ ] в датасете есть метрика «Доля отказов» с выражением - [ ] в датасете есть метрика «Доля отказов» с выражением
`SUM(bounce_sessions) / SUM(sessions)` и форматом `.1%`; `SUM(bounce_sessions) / SUM(sessions)` и форматом `.1%`;
- [ ] для `device_category` и `report_date` заданы русские имена «Категория - [ ] колонки `device_category` и `report_date` подписаны «Категория
устройства» и «Дата отчета»; устройства» и «Дата отчета»;
- [ ] сохранен столбиковый график доли отказов по категориям устройств без - [ ] сохраненный столбиковый график показывает долю отказов по категориям
относительного фильтра по времени; устройств за весь период витрины;
- [ ] график сверен с запросом из SQL Lab; - [ ] числа на графике совпадают с результатом запроса из SQL Lab;
- [ ] стендовые объекты не изменены, дашборд не создан. - [ ] вы сохранили график отдельно, без создания дашборда и изменения
стендовых объектов.
+15 -12
View File
@@ -1,14 +1,14 @@
# Нарежь сессии сам # Нарежьте сессии сами
**Чему учит:** собрать сессии из событий и сверить их число с внешним **Чему учит:** собрать сессии из событий, сравнить их число с `VisitID`
эталоном. Найти причину расхождения в отдельных строках. источника и найти события, из-за которых расчеты расходятся.
## Сначала прочитать ## Сначала прочитать
- [Хранилище и его слои, раздел DDS](../handbook/storage-layers.md#dds-язык-бизнеса); - [Хранилище и его слои, раздел DDS](../handbook/storage-layers.md#dds-язык-бизнеса);
- [формат события: Идентификаторы и время](../formats/clickstream-event.md#идентификаторы-и-время). - [формат события: Идентификаторы и время](../formats/clickstream-event.md#идентификаторы-и-время).
## Роль сессии ## Для чего нужны сессии
Визит, или сессия, объединяет действия посетителя за один заход на сайт. Визит, или сессия, объединяет действия посетителя за один заход на сайт.
По визитам аналитик считает длительность и глубину просмотра. По визитам аналитик считает длительность и глубину просмотра.
@@ -71,19 +71,22 @@ ORDER BY EventDate, ClientID;
## Как проверить себя ## Как проверить себя
Результат задания проверяется глазами, поэтому задачи в `homework_check`
для него нет. Таблицы и представления создавать не нужно.
Сохраните исходный и уточненный расчеты, результаты сверки и найденные строки. Сохраните исходный и уточненный расчеты, результаты сверки и найденные строки.
Таблицы и представления создавать не нужно. Задачи в `homework_check`
для этого задания нет: вы проверяете расчет своим запросом.
**Предупреждение.** В `sql/dds/session_rebuild.sql` и В `sql/dds/session_rebuild.sql` и
`sql/dq/sessions_vs_reference.sql` лежат готовые запросы нарезки и сверки. `sql/dq/sessions_vs_reference.sql` лежат готовые запросы нарезки и сверки.
Откройте их после собственного решения и расследования. Сравните запросы Откройте их после собственного решения и расследования. Сравните запросы
со своими: копирование готового SQL пропускает расследование. со своими.
Сопоставление со стендовой нарезкой по `session_id` остается на ступень 4. Сопоставление со стендовой нарезкой по `session_id` остается на ступень 4.
## Приемка ## Что должно получиться
- [ ] запрос нарезает все события `ods.event_v` за все доступные дни; - [ ] ваш запрос собирает сессии из всех событий `ods.event_v` за все
- [ ] сверка через `FULL OUTER JOIN` дает сводку по дням и список кук; доступные дни;
- [ ] при разборе расхождения указаны кука, день, два события и причина; - [ ] у вас есть сводка расхождений по дням и список кук, полученные через
`FULL OUTER JOIN`;
- [ ] вы объяснили причину одного расхождения и записали куку, день и два
события, на которых оно видно;
- [ ] уточненный расчет дает ноль расхождений во всех днях. - [ ] уточненный расчет дает ноль расхождений во всех днях.
+17 -18
View File
@@ -1,16 +1,16 @@
# Перепиши нарезку стенда # Перепишите нарезку стенда
**Чему учит:** изменить расчетную сущность внутри готового конвейера и **Чему учит:** переписать расчет сессий в готовом конвейере и проверить
проверить результат внешним эталоном. результат по исходным событиям.
## Сначала прочитать ## Сначала прочитать
- [задание «Нарежь сессии сам»](3-1-sessions-by-hand.md); - [задание «Нарежьте сессии сами»](3-1-sessions-by-hand.md);
- [модель сессии DDS](../architecture/dds/session.md); - [модель сессии DDS](../architecture/dds/session.md);
- [дневной конвейер трансформаций](../architecture/etl.md#объём-прогона); - [дневной конвейер трансформаций](../architecture/etl.md#объём-прогона);
- [раскладка по шардам](../architecture/storage.md#раскладка-по-шардам). - [раскладка по шардам](../architecture/storage.md#раскладка-по-шардам).
## Граница задания ## Что предстоит сделать
Даг, таблицы сессий и проверка их числа уже созданы. Даг, таблицы сессий и проверка их числа уже созданы.
Перепишите только три файла: Перепишите только три файла:
@@ -24,7 +24,7 @@
служебная таблица, где день собирается перед заменой. В служебная таблица, где день собирается перед заменой. В
`sessions_vs_reference.sql` число сессий сравнивается с `VisitID` из ODS. `sessions_vs_reference.sql` число сессий сравнивается с `VisitID` из ODS.
## Договор с дагом ## Как даг запускает ваши запросы
Объем прогона — дни, которые соберет один запуск. Его вычисляет первый файл. Объем прогона — дни, которые соберет один запуск. Его вычисляет первый файл.
@@ -74,7 +74,7 @@ ClickHouse в `ALTER ... PARTITION` здесь не работает.
Состав и типы строки заданы в `dds.session_rep`. Заполните начало, конец и Состав и типы строки заданы в `dds.session_rep`. Заполните начало, конец и
длительность. Посчитайте все события, просмотры, добавления в корзину и длительность. Посчитайте все события, просмотры, добавления в корзину и
покупки. Возьмите входную и выходную страницы, UTM первого события, устройство покупки. Возьмите входную и выходную страницы, UTM первого события, устройство
и город. Служебные колонки тоже входят в договор таблицы. и город. Заполните также служебные колонки из схемы таблицы.
Пишите через `dds.session_stage_dist`: место строки определит ключ Пишите через `dds.session_stage_dist`: место строки определит ключ
`client_id`. При записи прямо в локальную таблицу оно зависело бы от ноды, `client_id`. При записи прямо в локальную таблицу оно зависело бы от ноды,
@@ -178,15 +178,14 @@ Distributed-таблицу и такую ошибку не заметит.
партицию. Расчет объема должен пропустить группу сессий: новых прожитых дней и партицию. Расчет объема должен пропустить группу сессий: новых прожитых дней и
дыр в слое нет. дыр в слое нет.
## Приемка ## Что должно получиться
- [ ] переписаны только три файла `sql/dds/session_*.sql`, инфраструктура - [ ] вы переписали три файла `sql/dds/session_*.sql`, сохранив даг,
упражнения не изменена; таблицы и проверку стенда;
- [ ] запуск после удаления партиции восстановил выбранный день и завершил - [ ] ваш расчет восстановил удаленный день, `etl_pipeline` завершился успешно;
`etl_pipeline` успешно; - [ ] вы сверили десять сохраненных строк: `session_id`, границы,
- [ ] сохраненные `session_id`, границы, длительность и счетчики десяти строк длительность и счетчики совпали после пересборки;
совпали после пересборки; - [ ] вы получили ноль расхождений в `sessions_vs_reference` за все
- [ ] `sessions_vs_reference` дает ноль расхождений во всех прожитых днях; прожитые дни и вручную сверили границы и атрибуты сессии одной куки;
- [ ] для одной куки вручную сверены границы и атрибуты сессии; - [ ] ваш запрос с `hostName()` не нашел строк на чужом шарде;
- [ ] пересчет ключа по `hostName()` не нашел строк на чужом шарде; - [ ] повторный прогон без удаления данных пропустил группу сессий.
- [ ] повторный прогон без новой дыры пропустил группу сессий.
+26 -26
View File
@@ -1,7 +1,7 @@
# Сначала сторож, потом ломай: заказы # Научите проверки замечать ошибки в заказах
**Чему учит:** до рискованного изменения задавать проверке явный договор и **Чему учит:** написать проверки заказов до изменения их обработки
испытывать его управляемой ошибкой. и убедиться на намеренной ошибке, что проверки ее замечают.
## Сначала прочитать ## Сначала прочитать
@@ -13,30 +13,30 @@
и на ее включение в [`dq_check`](../../dags/dq_check.py). Готового ответа для и на ее включение в [`dq_check`](../../dags/dq_check.py). Готового ответа для
двух проверок из этого задания в репозитории нет. двух проверок из этого задания в репозитории нет.
## Граница задания ## Что предстоит сделать
Добавьте два сторожа: Добавьте две проверки и включите их в даг:
- `sql/dq/order_items_total.sql` проверяет сумму позиций заказа; - `sql/dq/order_items_total.sql` проверяет сумму позиций заказа;
- `sql/dq/orders_vs_ods.sql` сверяет DDS с его источником; - `sql/dq/orders_vs_ods.sql` сверяет DDS с его источником;
- `dags/dq_check.py` запускает обе проверки, публикует их строки в - `dags/dq_check.py` запускает обе проверки, публикует их строки в
`dm.dq_summary` и создает для них отдельные задачи. `dm.dq_summary` и создает для них отдельные задачи.
На этом шаге не меняйте разбор ODS и сборку DDS. Даги, DDL, таблицы-доноры, Разбор ODS и сборку DDS вы перепишете в следующих заданиях. Здесь сохраните
Kafka и службы остаются несущей частью упражнения. их, даги загрузки, DDL, таблицы-доноры, Kafka и службы как есть.
Перед опытами остановите автоматическое движение мира: поставьте `world_live` Перед опытами остановите автоматическое движение мира: поставьте `world_live`
на паузу и дождитесь завершения активных прогонов. До чистой точки остановки не на паузу и дождитесь завершения активных прогонов. До завершения опытов не
запускайте руками `world_next_day`, `world_live_day`, `world_initialize` и запускайте руками `world_next_day`, `world_live_day`, `world_initialize` и
`world_recreate`. Это условие стабильного модельного времени: одной паузы `world_recreate`. Это условие стабильного модельного времени: одной паузы
`world_live` для него мало. `world_live` для него мало.
## Два договора качества ## Что будут проверять ваши запросы
Обе проверки следуют договору `dq_check`: запрос возвращает по одной строке Чтобы `dq_check` мог запустить проверку, ее запрос возвращает по одной строке
на день с колонками `data_date`, `reference_rows`, `actual_rows`, на день с колонками `data_date`, `reference_rows`, `actual_rows`,
`failed_rows`, `diagnostics`. Даг сохраняет в `dm.dq_summary` только счетчики, `failed_rows`, `diagnostics`. Даг сохраняет в `dm.dq_summary` только счетчики,
статус и координаты запуска. Ограниченные примеры из `diagnostics` ищите в статус и координаты запуска. Примеры из `diagnostics` ищите в
журнале упавшей задачи Airflow. Ограничьте их тем же малым числом, что и журнале упавшей задачи Airflow. Ограничьте их тем же малым числом, что и
существующие запросы в `sql/dq/`. существующие запросы в `sql/dq/`.
@@ -46,17 +46,18 @@ Kafka и службы остаются несущей частью упражн
посчитайте сумму `item_price * item_quantity` и сравните ее с `items_total`. посчитайте сумму `item_price * item_quantity` и сравните ее с `items_total`.
`reference_rows` считает ключи со стороны `items_total`, `actual_rows` - ключи `reference_rows` считает ключи со стороны `items_total`, `actual_rows` - ключи
со стороны вычисленной суммы, а `failed_rows` - несовпавшие значения. В со стороны вычисленной суммы, а `failed_rows` - несовпавшие значения. В
исправном наборе первые два счетчика равны по построению; сам инвариант выражен исправном наборе первые два счетчика равны; несовпадения сумм считает
через `failed_rows`. Для диагностики достаточно `order_id`, ожидаемой суммы и `failed_rows`. Для диагностики достаточно `order_id`, ожидаемой суммы и
суммы позиций. суммы позиций.
Проверка относится к типизированному разбору массива `items` на границе Проверка относится к типизированному разбору массива `items` на границе
ODS -> DDS. Скидка, доставка и равенство `total` в этот договор не входят. ODS -> DDS. Скидку, доставку и равенство `total` здесь проверять не нужно.
### DDS против ODS ### DDS против ODS
Имя проверки - `orders_vs_ods`. Источник модели DDS здесь - текущее состояние Имя проверки - `orders_vs_ods`. Источник модели DDS здесь - текущее состояние
`ods.order_v`. Внутреннее состояние генератора в этот договор не входит. `ods.order_v`. С ним вы и сравниваете DDS; обращаться к внутреннему состоянию
генератора для этой проверки не нужно.
Одним запросом соберите обе стороны на зерне дня заказа и сравните: Одним запросом соберите обе стороны на зерне дня заказа и сравните:
- число заказов; - число заказов;
@@ -131,18 +132,17 @@ ORDER BY check_name;
Проверка суммы должна охватывать каждый день `dds.order_v`. Сверка слоев Проверка суммы должна охватывать каждый день `dds.order_v`. Сверка слоев
должна охватывать объединение дней ODS и DDS, включая дни только одной стороны. должна охватывать объединение дней ODS и DDS, включая дни только одной стороны.
## Когда можно остановиться ## Завершите опыт
После двух восстановлений выбранный заказ снова совпадает с ODS. Все семь После восстановления заказа и успешного `dq_check` можно закончить работу или
проверок дают зеленый результат. В таком состоянии можно закончить работу или
перейти к разбору заказов, оставив модельное время на паузе. перейти к разбору заказов, оставив модельное время на паузе.
## Приемка ## Что должно получиться
- [ ] две новые проверки встроены в `dq_check` и публикуют дневные строки по - [ ] две новые проверки запускаются в `dq_check`, а их дневные результаты
принятому договору; появляются в `dm.dq_summary`;
- [ ] каждая проверка дала красный результат на своей управляемой ошибке; - [ ] вы увидели ошибку каждой проверки после соответствующего изменения
- [ ] журналы упавших задач содержали ограниченную диагностику; заказа, а в журнале нашли примеры заказов с расхождениями;
- [ ] `etl_pipeline` восстановил обе измененные строки DDS; - [ ] вы восстановили заказ через `etl_pipeline` после обоих опытов;
- [ ] `order_items_total`, `orders_vs_ods` и пять прежних DQ зелены; - [ ] `order_items_total`, `orders_vs_ods` и пять прежних проверок успешны;
- [ ] SQL разбора ODS и сборки DDS на этом шаге не менялся. - [ ] разбор ODS и сборка DDS остались прежними.
+20 -21
View File
@@ -1,7 +1,7 @@
# Перепишите разбор заказов в ODS # Перепишите разбор заказов в ODS
**Чему учит:** сначала измерить обе ветви разбора на управляемом срезе, затем **Чему учит:** переписать разбор заказов и на пробных сообщениях проверить,
переписать переход и повторить тот же опыт. что годные строки и брак попадают в свои таблицы без потерь.
## Сначала прочитать ## Сначала прочитать
@@ -11,9 +11,9 @@
- [`orders_ingest`](../../dags/orders_ingest.py); - [`orders_ingest`](../../dags/orders_ingest.py);
- [`order_parse.sql`](../../sql/ods/order_parse.sql). - [`order_parse.sql`](../../sql/ods/order_parse.sql).
Готового запроса для сверки управляемого среза в репозитории нет. Запрос для проверки пробных строк вы напишете сами.
## Граница задания ## Что предстоит сделать
Сначала проведите опыт на готовом `order_parse.sql`. Затем удалите его SQL, Сначала проведите опыт на готовом `order_parse.sql`. Затем удалите его SQL,
соберите разбор заново и повторите опыт с другим маркером. соберите разбор заново и повторите опыт с другим маркером.
@@ -72,7 +72,7 @@ jq -cn --arg marker "$LAB_MARKER" --arg day "$LAB_DAY" '
' > "$LAB_BATCH" ' > "$LAB_BATCH"
``` ```
Отправьте файл в обычный топик заказов штатным производителем контейнера: Отправьте файл в обычный топик заказов утилитой из контейнера Kafka:
```bash ```bash
docker compose exec -T kafka /opt/kafka/bin/kafka-console-producer.sh \ docker compose exec -T kafka /opt/kafka/bin/kafka-console-producer.sh \
@@ -123,7 +123,7 @@ SETTINGS mutations_sync = 2;
В ClickHouse 26.3 мутации выполняются в фоне. С `mutations_sync = 2` команда В ClickHouse 26.3 мутации выполняются в фоне. С `mutations_sync = 2` команда
ждет их завершения на всех репликах. Запросами к трем Distributed-таблицам ждет их завершения на всех репликах. Запросами к трем Distributed-таблицам
докажите, что строк маркера не осталось, затем удалите временный файл. убедитесь, что строк маркера не осталось, затем удалите временный файл.
Группа `clickstream_orders` уже подтвердила эти сообщения, поэтому обычный Группа `clickstream_orders` уже подтвердила эти сообщения, поэтому обычный
повтор `orders_ingest` их не прочитает. В Kafka они остаются до удаления по повтор `orders_ingest` их не прочитает. В Kafka они остаются до удаления по
@@ -137,8 +137,8 @@ SETTINGS mutations_sync = 2;
## Перепишите разбор ## Перепишите разбор
Удалите готовый SQL из `sql/ods/order_parse.sql` и сохраните его договор с Удалите готовый SQL из `sql/ods/order_parse.sql` и напишите свой. Чтобы
`orders_ingest`: `orders_ingest` мог его использовать, сохраните следующие условия:
- вход - строки `stg.orders_raw_dist` одного `{load_id:String}`; - вход - строки `stg.orders_raw_dist` одного `{load_id:String}`;
- каждая строка среза попадает ровно в один результат: годный заказ или брак; - каждая строка среза попадает ровно в один результат: годный заказ или брак;
@@ -161,21 +161,20 @@ SETTINGS mutations_sync = 2;
шард. Затем очистите второй срез теми же адресными командами и запустите шард. Затем очистите второй срез теми же адресными командами и запустите
`dq_check`. `dq_check`.
## Когда можно остановиться ## Завершите опыт
Оба маркера отсутствуют в STG, ODS, таблице брака и DDS; временные файлы Убедитесь, что оба маркера отсутствуют в STG, ODS, таблице брака и DDS,
удалены. Семь DQ дают зеленый результат. В таком состоянии можно перейти к временные файлы удалены, а семь проверок успешны. Теперь можно перейти к
сборке DDS, не снимая паузу с модельного времени. Если вы прерываетесь и до сборке DDS, не снимая паузу с модельного времени. Если вы прерываетесь и до
опыта `world_live` работал, снимите его с паузы. опыта `world_live` работал, снимите его с паузы.
## Приемка ## Что должно получиться
- [ ] до переписывания зафиксированы баланс 4 = 1 + 3, три класса брака, - [ ] у вас сохранены результаты до переписывания: четыре строки дали
деловые поля текущей версии и фактический шард; один заказ и три класса брака; вы сверили поля текущей версии и шард;
- [ ] переписан только `sql/ods/order_parse.sql`, несущая инфраструктура не - [ ] вы переписали `sql/ods/order_parse.sql`, сохранив остальные части стенда;
изменена; - [ ] ваш разбор дал тот же результат на новой пробной пачке;
- [ ] после переписывания новый срез дал тот же результат; - [ ] вы проверили разные значения `created_at` и `updated_at` у годной
- [ ] обе годные строки различали `created_at` и `updated_at`; строки в обоих опытах и подтвердили правильный шард через `hostName()`;
- [ ] проверки `hostName()` не нашли строки ODS на чужом шарде; - [ ] строки обоих опытов удалены по их маркерам;
- [ ] строки обоих опытов удалены только по своим маркерам; - [ ] после очистки конвейер и все семь проверок успешны.
- [ ] после очистки общий конвейер и семь DQ находятся в зеленом состоянии.
+20 -21
View File
@@ -1,7 +1,7 @@
# Перепишите сборку заказов в DDS # Перепишите сборку заказов в DDS
**Чему учит:** переписать пересборку заказов по состоянию слоев и доказать ее **Чему учит:** настроить пересборку заказов по состоянию ODS и DDS
результат на пропущенном закрытом дне. и проверить, что она восстанавливает пропущенный день за пределами текущего окна.
## Сначала прочитать ## Сначала прочитать
@@ -11,7 +11,7 @@
- [дневной конвейер](../architecture/etl.md#объём-прогона); - [дневной конвейер](../architecture/etl.md#объём-прогона);
- [раскладку по шардам](../architecture/storage.md#раскладка-по-шардам). - [раскладку по шардам](../architecture/storage.md#раскладка-по-шардам).
## Граница задания ## Что предстоит сделать
Удалите готовый SQL и перепишите только три файла: Удалите готовый SQL и перепишите только три файла:
@@ -19,8 +19,8 @@
- `sql/dds/order_rebuild.sql`; - `sql/dds/order_rebuild.sql`;
- `sql/dds/order_replace.sql`. - `sql/dds/order_replace.sql`.
Даг `dds_transform`, DDL, донор, ODS и обе проверки из первого задания уже Даг `dds_transform`, DDL, донор, ODS и обе проверки из первого задания
задают границу работы. Перед началом убедитесь, что пробные строки предыдущего оставьте как есть. Перед началом убедитесь, что пробные строки предыдущего
задания удалены, а семь DQ дают зеленый результат. задания удалены, а семь DQ дают зеленый результат.
Поставьте `world_live` на паузу и дождитесь завершения активных прогонов. До Поставьте `world_live` на паузу и дождитесь завершения активных прогонов. До
@@ -28,7 +28,7 @@
`world_recreate`. `world_next_day` понадобится только в явно указанном ниже `world_recreate`. `world_next_day` понадобится только в явно указанном ниже
случае. случае.
## Договор с дагом ## Как даг запускает ваши запросы
Сохраните интерфейс `dds_transform`: Сохраните интерфейс `dds_transform`:
@@ -103,22 +103,21 @@ DROP PARTITION 'YYYY-MM-DD';
заказов снова должна пересобрать текущее изменяемое окно. Контрольные суммы заказов снова должна пересобрать текущее изменяемое окно. Контрольные суммы
деловых полей остаются прежними, а все семь проверок дают зеленый результат. деловых полей остаются прежними, а все семь проверок дают зеленый результат.
## Когда можно остановиться ## Завершите опыт
Удаленная партиция восстановлена, ее число строк и контрольная сумма совпали После восстановления партиции и успешных проверок снимите `world_live`
с исходными. На чужих шардах строк нет, повторный прогон не изменил деловые с паузы, если он работал до упражнения.
данные, семь DQ зелены. Если до упражнения `world_live` работал, теперь его
можно снять с паузы.
## Приемка ## Что должно получиться
- [ ] переписаны только три файла `sql/dds/order_*.sql`, инфраструктура - [ ] вы переписали три файла `sql/dds/order_*.sql`, сохранив остальные
упражнения не изменена; части стенда;
- [ ] удален ближайший к окну закрытый день, а красный DQ зафиксирован до - [ ] вы удалили ближайший к окну закрытый день и увидели ошибку DQ до
восстановления; восстановления;
- [ ] `first_day` начался с удаленного дня, задачи замены покрыли непрерывный - [ ] ваш расчет выбрал `first_day` с удаленного дня и заменил партиции
отрезок до конца окна; всего отрезка до конца окна;
- [ ] число заказов и контрольная сумма восстановленного дня совпали; - [ ] вы сверили число заказов и контрольную сумму восстановленного дня
- [ ] `order_items_total`, `orders_vs_ods` и пять прежних DQ зелены; с сохраненными значениями; они совпали;
- [ ] проверка `hostName()` не нашла заказов DDS на чужом шарде; - [ ] `order_items_total`, `orders_vs_ods` и пять прежних проверок успешны;
- [ ] повторный `etl_pipeline` сохранил деловые данные и зеленые проверки. - [ ] ваш запрос с `hostName()` не нашел заказов на чужом шарде;
- [ ] повторный `etl_pipeline` сохранил деловые данные, проверки прошли.
+53 -48
View File
@@ -15,7 +15,7 @@
Готовый стендовый разбор находится в `sql/ddl/30-ods-views.sql`. Откройте его Готовый стендовый разбор находится в `sql/ddl/30-ods-views.sql`. Откройте его
после собственной сверки: в нем лежит ответ на основную часть задания. после собственной сверки: в нем лежит ответ на основную часть задания.
## Граница задания ## Что предстоит сделать
Создайте в базе `default` одну свою таблицу годных событий и наполните ее одним Создайте в базе `default` одну свою таблицу годных событий и наполните ее одним
пакетным `INSERT SELECT` из `stg.hits_raw_dist`. Работайте с первой нодой под пакетным `INSERT SELECT` из `stg.hits_raw_dist`. Работайте с первой нодой под
@@ -72,11 +72,10 @@ Airflow. При включенном `world_live` посмотрите его т
`ods.event_rep`, а также `_load_ts`. Тип `purchaseRevenue` возьмите из ODS: `ods.event_rep`, а также `_load_ts`. Тип `purchaseRevenue` возьмите из ODS:
`Array(Decimal(18, 2))`. `Array(Decimal(18, 2))`.
Схлопните повтор доставки по договору ODS. Возьмите Обработайте повторные доставки так же, как ODS. Возьмите
`ReplacingMergeTree(_load_ts)`, партицию `EventDate` и полный ключ замены: `ReplacingMergeTree(_load_ts)`, партицию `EventDate` и полный ключ замены:
`(CounterID, EventDate, intHash32(ClientID), WatchID)`. Достаточно одной `(CounterID, EventDate, intHash32(ClientID), WatchID)`. Достаточно одной
локальной таблицы: отдельная Distributed-таблица не добавит урока к пакетному локальной таблицы: загрузка и сверка в этом опыте выполняются на одной ноде.
опыту на одной ноде.
`WatchID` — логический идентификатор события, уникальный по `WatchID` — логический идентификатор события, уникальный по
[договору генератора](../specs/2026-08-01-generator.md). Физический ключ ODS [договору генератора](../specs/2026-08-01-generator.md). Физический ключ ODS
@@ -85,8 +84,7 @@ Airflow. При включенном `world_live` посмотрите его т
## Разберите годные строки ## Разберите годные строки
Для каждой строки среза примите решение по тому же строгому входу, что и Для каждой строки среза проверьте те же условия приема, что и на стенде:
стенд:
1. корень `raw` — JSON-объект; 1. корень `raw` — JSON-объект;
2. набор верхнеуровневых ключей в точности равен 47 ключам формата; 2. набор верхнеуровневых ключей в точности равен 47 ключам формата;
@@ -95,29 +93,31 @@ Airflow. При включенном `world_live` посмотрите его т
Напишите разбор, который не прерывает пакет из-за одной грязной строки. Сами Напишите разбор, который не прерывает пакет из-за одной грязной строки. Сами
выберите способ безопасного извлечения пяти опорных полей, форму общих выражений выберите способ безопасного извлечения пяти опорных полей, форму общих выражений
и место фильтра. Остальные поля извлеките по договору формата. и место фильтра. Остальные поля извлеките по описанию формата.
Перенесите `_load_ts` из STG без пересчета: это время исходной доставки и Перенесите `_load_ts` из STG без пересчета: это время исходной доставки и
колонка версии. Время запуска вашего запроса сюда не входит. Вставьте в свою колонка версии. Время запуска вашего запроса сюда не входит. Вставьте в свою
таблицу только годные строки выбранного среза. таблицу только годные строки выбранного среза.
Считайте отвергнутые строки прямо в исходном срезе: отдельная таблица брака в Считайте отвергнутые строки прямо в исходном срезе, без отдельной таблицы
задание не входит. Проверьте работу строгого входа одним запросом. Посчитайте: брака. Одним запросом посчитайте:
- все доставки; - все доставки;
- строки, прошедшие ваш предикат; - строки, прошедшие ваше условие отбора;
- строки, отвергнутые его отрицанием. - строки, для которых это условие ложно.
Проверьте равенство общего числа строк сумме прошедших и отвергнутых. Так каждая Проверьте равенство общего числа строк сумме прошедших и отвергнутых. Так каждая
исходная строка учитывается ровно в одной группе. Если отвергнутые строки есть, исходная строка учитывается ровно в одной группе. Если отвергнутые строки есть,
сохраните их `raw`, `_load_ts` и координаты Kafka. Найдите те же сохраните их `raw`, `_load_ts` и координаты Kafka. Найдите те же
координаты в `ods.event_errors_dist` и запишите стендовый `error_class`. Если координаты в `ods.event_errors_dist` и запишите стендовый `error_class`. Если
брака нет, так и зафиксируйте: опыт проверил успешную ветвь и выполнение брака нет, так и зафиксируйте: опыт проверил успешную ветвь и выполнение
предиката на этом срезе, но не классификатор ошибок. Подкладывать грязные условия отбора на этом срезе, но не классификатор ошибок. Подкладывать грязные
сообщения ради задания не требуется. сообщения ради задания не требуется.
## Приведите стороны к одному зерну ## Приведите стороны к одному зерну
### Учтите повторные доставки
В STG одна строка представляет доставку. Повтор сообщения остается второй В STG одна строка представляет доставку. Повтор сообщения остается второй
строкой. В `ods.event_v` одна строка представляет актуальное событие, потому строкой. В `ods.event_v` одна строка представляет актуальное событие, потому
что представление читает физическую таблицу с `FINAL`. что представление читает физическую таблицу с `FINAL`.
@@ -126,17 +126,20 @@ Airflow. При включенном `world_live` посмотрите его т
полных ключей среди годных доставок. Разница между числом доставок и этим полных ключей среди годных доставок. Разница между числом доставок и этим
счетом показывает повторы в исходном срезе. счетом показывает повторы в исходном срезе.
Область эталона постройте независимо от своего предиката и результата вставки. ### Выберите события ODS для сравнения
Снова возьмите весь исходный STG-срез. По координатам Kafka исключите доставки,
которые есть в `ods.event_errors_dist`. У оставшихся доставок извлеките
`WatchID` без риска прервать запрос и выберите эти события из свежего
`ods.event_v`.
Свяжите сырую доставку с ODS по `WatchID`: по договору генератора это уникальный Теперь нужно найти в ODS события из выбранного среза. Список стройте по
идентификатор события. Он задает только область эталона. Дедупликация и исходным доставкам: если взять его из своей таблицы, ошибочно отвергнутое
последующее соединение используют полный физический ключ. Не берите область из событие исчезнет с обеих сторон сверки и потеря останется незамеченной.
своей таблицы и не применяйте к ней свой предикат. Иначе ошибочно отвергнутая
строка исчезнет с обеих сторон сверки. Снова возьмите весь исходный STG-срез. По координатам Kafka исключите
доставки, которые есть в `ods.event_errors_dist`. У оставшихся извлеките
`WatchID` без риска прервать запрос и выберите эти события из свежего
`ods.event_v`. Свое условие отбора здесь не применяйте: решение о годности
доставки вы берете из результата стендового разбора.
`WatchID` связывает доставку с событием в ODS. Для удаления повторов и
сравнения полей дальше используйте полный физический ключ из схемы таблицы.
## Сверьте 47 полей ## Сверьте 47 полей
@@ -171,44 +174,46 @@ ClickHouse по умолчанию заполняет отсутствующую
Для измененной строки назовите поля, которые расходятся, и сравните каждое с Для измененной строки назовите поля, которые расходятся, и сравните каждое с
сырой строкой. Для пропавшей или лишней строки проверьте пять опорных полей и сырой строкой. Для пропавшей или лишней строки проверьте пять опорных полей и
решение строгого предиката. Если координаты доставки есть в результат условия отбора. Если координаты доставки есть в
`ods.event_errors_dist`, посмотрите `error_class`. `ods.event_errors_dist`, посмотрите `error_class`.
Исправьте разбор, удалите и заново создайте только собственную таблицу, затем Исправьте разбор, удалите и заново создайте только собственную таблицу, затем
повторите вставку того же среза. Остановитесь, когда счет исходных доставок по повторите вставку того же среза. Остановитесь, когда счет исходных доставок по
прежним границам не изменился, баланс строгого входа сошелся, а построчная прежним границам не изменился, число годных и отвергнутых строк в сумме дает
сверка пуста. число исходных доставок, а построчная сверка пуста.
## Где заканчивается проверка ## Чего не подтверждает сравнение с ODS
`ods.event_v` — образец для сравнения. Оба разбора могли повторить одну `ods.event_v` — образец для сравнения. Оба разбора могли повторить одну
ошибку. Для пяти опорных полей у вас есть проверка разбираемости и исходный ошибку. Для пяти опорных полей у вас есть проверка преобразования в нужный тип
JSON. Остальные 42 поля можно вручную сопоставить с `raw`, однако второго и исходный JSON. Остальные 42 поля можно вручную сопоставить с `raw`, однако
независимого источника для всех значений на стенде нет. второго независимого источника для всех значений на стенде нет.
## Уберите свой объект ## Уберите свой объект
Сохраните DDL таблицы, запрос разбора, границы среза, баланс строгого входа и Сохраните DDL таблицы, запрос разбора, границы среза, число исходных, годных
пустую итоговую сверку. Затем удалите свою таблицу командой `DROP TABLE` с ее и отвергнутых строк и итоговую сверку без расхождений. Запишите также, чего
точным именем. Убедитесь через `system.tables`, что объекта больше нет. не подтверждает сравнение с ODS и как TTL ограничивает время для повторения
опыта. Затем удалите свою таблицу командой `DROP TABLE` с ее точным именем.
Убедитесь через `system.tables`, что объекта больше нет.
Прикладные строки стенда вы не меняли, поэтому удалять данные из STG, ODS или Прикладные строки стенда вы не меняли, поэтому удалять данные из STG, ODS или
Kafka после опыта не нужно. Kafka после опыта не нужно.
## Приемка ## Что должно получиться
- [ ] записаны границы малого завершенного среза, его счет и координаты - [ ] у вас сохранены границы выбранного среза, число строк и координаты
доставки по партициям Kafka; доставки по партициям Kafka;
- [ ] создана одна собственная таблица со всеми 47 полями, перенесенным - [ ] ваша таблица содержит все 47 полей и `_load_ts` и обрабатывает
`_load_ts` и физическим договором дедупликации ODS; повторы по тем же правилам, что и ODS;
- [ ] пакетный разбор принимает только JSON-объекты с точным набором ключей и - [ ] ваш разбор принимает только JSON-объекты с точным набором ключей и
пятью разбираемыми опорными полями; пятью опорными полями, которые преобразуются в нужные типы;
- [ ] счет исходных строк равен сумме годных и отвергнутых, а найденный брак - [ ] вы сверили число исходных строк с суммой годных и отвергнутых,
рассмотрен по `raw`, координатам и стендовому классу; а найденный брак разобрали по `raw`, координатам и классу ошибки стенда;
- [ ] область ODS получена из всех координат исходного среза и стендовой - [ ] вы выбрали события ODS по исходному срезу и таблице ошибок,
таблицы ошибок, независимо от собственного предиката и таблицы; независимо от своей таблицы и условия отбора;
- [ ] обе стороны приведены к зерну полного физического ключа; - [ ] вы сравнили обе стороны по полному физическому ключу через
- [ ] `FULL OUTER JOIN` сравнивает все 47 полей, а числа пропавших, лишних и `FULL OUTER JOIN`: во всех 47 полях нет расхождений,
измененных строк равны нулю; пропавших и лишних строк тоже нет;
- [ ] отдельно подтвержден перенос `_load_ts` и учтены повторы доставки; - [ ] вы отдельно проверили перенос `_load_ts` и учли повторные доставки;
- [ ] границы стендового оракула и TTL записаны вместе с результатом; - [ ] вы записали ограничения сравнения с ODS и срок повторения опыта;
- [ ] собственная таблица удалена, объекты и данные стенда не изменены. - [ ] после опыта вы удалили свою таблицу, сохранив объекты и данные стенда.
+15 -16
View File
@@ -99,21 +99,20 @@
[«Данные»](../architecture/monitoring/data.md). Устройство живого дня и [«Данные»](../architecture/monitoring/data.md). Устройство живого дня и
доставки слепка описано в [главе об Airflow](../handbook/airflow-dags.md). доставки слепка описано в [главе об Airflow](../handbook/airflow-dags.md).
## Приемка ## Что должно получиться
- [ ] для каждого из трех дашбордов записан вопрос, на который он отвечает; - [ ] для каждого из трех дашбордов записан вопрос, на который он отвечает;
- [ ] сохранены окно, текст и показатели верхнего запроса, определены его - [ ] у вас сохранены окно, текст и показатели верхнего запроса;
источник и назначение; вы выяснили, кто его отправил и зачем;
- [ ] среди запросов открытых дашбордов найден тот, который прочитал больше - [ ] вы нашли запрос открытых дашбордов, который прочитал больше всего
всего строк, и соответствующая панель; строк, и панель, которая его отправила;
- [ ] объяснено, как журнал собирают с двух нод и какие записи учитывают для - [ ] вы можете объяснить, зачем журнал собирают с двух нод и какие записи
счета запросов; учитывают при подсчете запросов;
- [ ] до наблюдения нарисован прогноз свежести событий и заказов, а форма - [ ] у вас есть прогноз свежести, сделанный до наблюдения, и несколько
линии объяснена через метку последней доставки; замеров; рост и сброс на рисунке объяснены через время последней доставки;
- [ ] записаны несколько наблюдений, на наброске видны рост и сброс свежести - [ ] вы подтвердили доставку заказов в Airflow и на панели слепков,
заказов, а доставка подтверждена в Airflow и на панели слепков; подписали модельную дату отдельно от реального времени наблюдений;
- [ ] модельная дата отделена от реального времени наблюдения; - [ ] вы можете объяснить выбор слоев для свежести, потока и доли брака
- [ ] объяснен выбор слоев для свежести, потока и доли брака, а также общее и почему числитель и знаменатель берут за одно окно;
окно числителя и знаменателя; - [ ] после наблюдений службы продолжают работать, данные и стендовые
- [ ] службы не остановлены, данные вручную не правились, стендовые дашборды дашборды остались без изменений.
не изменены.
+43 -28
View File
@@ -10,28 +10,28 @@
|---|---|---| |---|---|---|
| 1 | Пять витрин слоя DM | построить результат для потребителя хранилища; уроки каждой витрины — в таблице ниже | | 1 | Пять витрин слоя DM | построить результат для потребителя хранилища; уроки каждой витрины — в таблице ниже |
| 2 | [Свой график в Superset](2-1-superset-chart.md) | превратить свой результат из хранилища в отчет в BI | | 2 | [Свой график в Superset](2-1-superset-chart.md) | превратить свой результат из хранилища в отчет в BI |
| 3 | [Нарежь сессии сам](3-1-sessions-by-hand.md) | сверить число сессий в своей нарезке с эталоном и найти события, из-за которых оно расходится | | 3 | [Нарежьте сессии сами](3-1-sessions-by-hand.md) | сверить число сессий в своей нарезке с эталоном и найти события, из-за которых оно расходится |
| 4 | [Перепиши нарезку стенда](4-1-session-pipeline.md) | изменить расчетную сущность внутри готового конвейера и проверить ее внешним эталоном | | 4 | [Перепишите нарезку стенда](4-1-session-pipeline.md) | переписать расчет сессий в готовом конвейере и проверить по исходным событиям |
| 5 | [Сначала сторож, потом ломай: заказы](5-1-order-checks.md)<br>[Перепишите разбор заказов в ODS](5-2-order-parsing.md)<br>[Перепишите сборку заказов в DDS](5-3-order-pipeline.md) | написать проверки качества до рискованного изменения, затем переписать разбор и сборку заказов | | 5 | [Научите проверки замечать ошибки в заказах](5-1-order-checks.md)<br>[Перепишите разбор заказов в ODS](5-2-order-parsing.md)<br>[Перепишите сборку заказов в DDS](5-3-order-pipeline.md) | написать проверки качества до рискованного изменения, затем переписать разбор и сборку заказов |
| 6 | [Разбери сырые события и сверь результат со стендом](6-1-event-parsing.md) | изучить разбор потока отдельно от живого приема и сверить его со стендовым результатом | | 6 | [Разберите сырые события рядом со стендом](6-1-event-parsing.md) | изучить разбор потока отдельно от живого приема и сверить его со стендовым результатом |
| 7 | [Мониторинг: куда смотреть](7-1-monitoring.md) | выбрать, куда и на что смотреть в живой системе | | 7 | [Мониторинг: куда смотреть](7-1-monitoring.md) | выбрать, куда и на что смотреть в живой системе |
## Ступень 1: пять витрин слоя DM ## Ступень 1: пять витрин слоя DM
Слой DM в стенде построен наполовину: витрины трех сюжетов дашборда и контроль Слой DM в стенде построен наполовину: витрины трех сюжетов дашборда и контроль
качества уже построены, остальные пять витрин строите вы. Каждая из них — качества уже построены, остальные пять витрин строите вы. Каждая из них —
самостоятельное ТЗ: в нем есть состав витрины, зерно, источники и способ самостоятельное задание: в нем есть состав витрины, зерно, источники и способ
проверки. проверки.
| № | Результат | Чему учит | | № | Результат | Чему учит |
|---|---|---| |---|---|---|
| 1 | [`events_enriched_v`](1-1-events-enriched.md) | строить слой как договор о данных, а не как их копию | | 1 | [`events_enriched_v`](1-1-events-enriched.md) | подготовить удобный для анализа набор полей, сохранив одну строку на событие |
| 2 | [`top_pages_daily`](1-2-top-pages-daily.md) | создавать материализованную витрину с дневной пересборкой в конвейере | | 2 | [`top_pages_daily`](1-2-top-pages-daily.md) | создавать материализованную витрину с дневной пересборкой в конвейере |
| 3 | [`session_overview`](1-3-session-overview.md) | хранить слагаемые для расчета средних и долей | | 3 | [`session_overview`](1-3-session-overview.md) | хранить слагаемые для расчета средних и долей |
| 4 | [`dq_errors_daily`](1-4-dq-errors-daily.md) | считать брак по дню загрузки и хранить итоги дольше источника | | 4 | [`dq_errors_daily`](1-4-dq-errors-daily.md) | считать брак по дню загрузки и хранить итоги дольше источника |
| 5 | [`utm_effectiveness`](1-5-utm-effectiveness.md) | читать `declared_` как предупреждение о сумме со слов клиента | | 5 | [`utm_effectiveness`](1-5-utm-effectiveness.md) | читать `declared_` как предупреждение о сумме со слов клиента |
Общее устройство слоя — [дока DM](../architecture/dm.md). Она отвечает на Общее устройство слоя — [справочник DM](../architecture/dm.md). Он отвечает на
вопросы «почему так», задание отвечает на вопрос «что сделать». вопросы «почему так», задание отвечает на вопрос «что сделать».
## Как принято на ступени 1 ## Как принято на ступени 1
@@ -64,8 +64,10 @@ DM](../architecture/dm.md).
Пример обоснования — `dm.revenue_daily_dist` в Пример обоснования — `dm.revenue_daily_dist` в
[`sql/ddl/55-dm-tables.sql`](../../sql/ddl/55-dm-tables.sql). [`sql/ddl/55-dm-tables.sql`](../../sql/ddl/55-dm-tables.sql).
**Где лежит код.** DDL заданий — в `sql/ddl/70-labs.sql`: он применяется **Где лежит код.** DDL заданий — в `sql/ddl/70-labs.sql`.
после стендовых, поэтому ваши объекты не мешаются со стендовыми. В При создании мира он применяется после стендовых файлов.
В уже работающем стенде сохранение файла и повторный `make up` новый DDL
не применят — выполните его сами, как описано ниже. В
`sql/dm/<витрина>_scope.sql` определяются дни прогона, в `_rebuild.sql` они `sql/dm/<витрина>_scope.sql` определяются дни прогона, в `_rebuild.sql` они
собираются в донор, в `_replace.sql` заменяется партиция. Задачу сборки собираются в донор, в `_replace.sql` заменяется партиция. Задачу сборки
добавьте в [`dags/dm_transform.py`](../../dags/dm_transform.py): без нее добавьте в [`dags/dm_transform.py`](../../dags/dm_transform.py): без нее
@@ -76,6 +78,20 @@ DM](../architecture/dm.md).
образец всего порядка пересборки. Состав колонок берите из своего ТЗ, иначе образец всего порядка пересборки. Состав колонок берите из своего ТЗ, иначе
получите показатели другой витрины. получите показатели другой витрины.
## Как применить свой DDL
В DBeaver создайте подключение по [инструкции в README](../../README.md#подключиться-из-dbeaver),
но укажите пользователя `etl` и пароль из `CLICKHOUSE_ETL_PASSWORD` в `.env`.
Пользователь `analyst` из той инструкции предназначен для чтения;
у `etl` есть права создавать таблицы и представления в `dm`.
Создайте файл `sql/ddl/70-labs.sql` и сохраняйте в нем DDL своих витрин.
Откройте его в SQL-редакторе этого подключения и выполните DDL текущего
задания. Создавайте объекты с
`ON CLUSTER clickstream_cluster`, чтобы они появились на обеих нодах;
образец для представления — [`sql/ddl/60-dm-views.sql`](../../sql/ddl/60-dm-views.sql).
Сохраненный файл позволит воссоздать ваши объекты при пересборке мира.
## Самопроверка ступени 1 ## Самопроверка ступени 1
Даг `homework_check` запускается руками, расписания у него нет. Задача Даг `homework_check` запускается руками, расписания у него нет. Задача
@@ -83,9 +99,8 @@ DM](../architecture/dm.md).
собрано верно. В сводку качества конвейера `dm.dq_summary` он не пишет: это собрано верно. В сводку качества конвейера `dm.dq_summary` он не пишет: это
инструмент лабы, а не история качества стенда. инструмент лабы, а не история качества стенда.
Проверка пяти заданий по витринам устроена одинаково и золотых констант не Для каждой витрины проверка независимо рассчитывает ожидаемый результат
содержит: рядом с витриной независимым запросом собирается тот же ответ — прямо по ее источнику. У четырех заданий источник
прямо из источника витрины, минуя ее саму. У четырех заданий источник
эталона тот же, что у витрины, `dds.session_v` или `dds.event_v`; у задания эталона тот же, что у витрины, `dds.session_v` или `dds.event_v`; у задания
4 это таблицы брака ODS, потому что в DDS нет сущности брака. Обе стороны 4 это таблицы брака ODS, потому что в DDS нет сущности брака. Обе стороны
соединяются по деловому ключу через `FULL OUTER JOIN`, и считается число соединяются по деловому ключу через `FULL OUTER JOIN`, и считается число
@@ -93,16 +108,13 @@ DM](../architecture/dm.md).
соединение сохраняет ключи обеих сторон: при внутреннем пропавший день или соединение сохраняет ключи обеих сторон: при внутреннем пропавший день или
ключ не попал бы в результат, и потеря осталась бы незамеченной. ключ не попал бы в результат, и потеря осталась бы незамеченной.
Раздел «Как проверить себя» каждого ТЗ по витрине называет деловой ключ и Раздел «Как проверить себя» каждого задания по витрине называет деловой ключ и
сравниваемые величины. Тот же прием в стендовом исполнении — [даг сравниваемые величины. Тот же прием в стендовом исполнении — [даг
`dq_check`](../../dags/dq_check.py) и запросы в [`sql/dq/`](../../sql/dq). `dq_check`](../../dags/dq_check.py) и запросы в [`sql/dq/`](../../sql/dq).
**Предупреждение.** Запросы самой проверки лежат в `sql/dq/` открыто, и Запросы проверок лежат в `sql/dq/`. Откройте их после собственного решения:
имена называют витрину и роль сверки. Проверка на инвариантах неизбежно они заново рассчитывают показатели витрины, поэтому содержат ответ на
близка к ответу: чтобы сверить витрину, надо ее пересчитать. Если открыть существенную часть задания. Сравните свой способ расчета с готовым.
эти файлы до выполнения задания, можно скопировать готовый SQL, не
разобравшись в расчете самостоятельно. Смотреть туда стоит после своей
сборки: сравнить два решения полезно, списать одно — нет.
## Факультативы ## Факультативы
@@ -122,9 +134,12 @@ DM](../architecture/dm.md).
### Superset и BI ### Superset и BI
Открывается после ступени 2. В задании Открывается после ступени 2. Два самостоятельных задания:
[Свой дашборд в Superset](optional/superset-dashboard.md) вы собираете
отдельный дашборд, не меняя объекты стенда. - [Свой дашборд в Superset](optional/superset-dashboard.md): собрать отчет
из своих витрин и подтвердить выводы запросами.
- [В DBeaver работает, в Superset нет](optional/superset-missing-object.md):
выяснить, почему клиенты по-разному видят один объект хранилища.
### Инженерия стенда ### Инженерия стенда
@@ -133,12 +148,12 @@ DM](../architecture/dm.md).
### Что делать, когда стенд сломался ### Что делать, когда стенд сломался
Открывается после ступени 7. Поломки стенда и runbook по итогам Открывается после ступени 7. Поломки стенда и инструкции по восстановлению
расследований; весь раздел — повышенная сложность. по итогам расследований; весь раздел — повышенная сложность.
## Уборка ## Уборка
Черновики и опыты за собой сносить: правило проверок стенда — После опытов удаляйте свои временные объекты. Если удалили учебную витрину,
[дока тестирования](../architecture/testing.md). Снесенная витрина обязана ее задача в `homework_check` должна снова завершиться ошибкой: отсутствие
снова покраснить свою задачу в `homework_check` — если не покраснила, витрины тоже проверяется. Подробнее — в
проверка ничего и не проверяла. [справочнике проверок](../architecture/testing.md).
+43 -18
View File
@@ -1,13 +1,12 @@
# Свой дашборд в Superset # Свой дашборд в Superset
**Чему учит:** довести свои витрины до потребителя — завести в Superset **Чему учит:** собрать из своих витрин отчет о поведении посетителей,
датасеты поверх публичных представлений, подобрать графики под вопросы и подобрать графики под вопросы и разместить их на одном понятном экране.
собрать из них один связный экран.
Это факультатив раздела «Superset и BI». Его можно пройти после ступени 2, Это факультатив раздела «Superset и BI». Его можно пройти после ступени 2,
когда готовы витрины ступени 1. когда готовы витрины ступени 1.
## Роль дашборда ## На какие вопросы ответит дашборд
Соберите обзор трафика и поведения посетителей. Он должен отвечать на три Соберите обзор трафика и поведения посетителей. Он должен отвечать на три
вопроса: что происходило в магазине, какие страницы, устройства и рекламные вопроса: что происходило в магазине, какие страницы, устройства и рекламные
@@ -26,14 +25,17 @@
таких объектов описан в [доке Superset](../../architecture/superset.md), «Импорт таких объектов описан в [доке Superset](../../architecture/superset.md), «Импорт
при каждой инициализации». при каждой инициализации».
Заведите отдельные датасеты и графики, соберите из них свой дашборд. Используйте свои датасеты и графики, соберите из них свой дашборд.
Назовите его, например, «Мои витрины DM». Это то же правило, по которому DDL Назовите его, например, «Мои витрины DM». Это то же правило, по которому DDL
домашек лежит в `sql/ddl/70-labs.sql`: стендовые объекты уже есть, свои домашек лежит в `sql/ddl/70-labs.sql`: стендовые объекты уже есть, свои
создаете вы. создаете вы.
## Датасеты ## Датасеты
Заведите в подключении `ClickHouse` пять физических датасетов схемы `dm`: Датасет `session_overview_v`, его метрика доли отказов и график уже готовы
после [ступени 2](../2-1-superset-chart.md). Используйте их здесь. Добавьте
недостающие датасеты, чтобы в подключении `ClickHouse` были представлены
пять публичных представлений схемы `dm`:
- `events_enriched_v`; - `events_enriched_v`;
- `top_pages_daily_v`; - `top_pages_daily_v`;
@@ -56,14 +58,25 @@
Для долей используйте слагаемые из витрин. Например, доля отказов считается Для долей используйте слагаемые из витрин. Например, доля отказов считается
как сумма `bounce_sessions`, деленная на сумму `sessions`. Среднее по готовым как сумма `bounce_sessions`, деленная на сумму `sessions`. Среднее по готовым
долям для такого графика не подходит — урок задания 3 остается в силе. долям для такого графика не подходит.
Названия графиков, осей и показателей должны объяснять экран без чтения SQL. Названия графиков, осей и показателей должны объяснять экран без чтения SQL.
## Период отчета
Выберите явный период из модельного календаря и задайте его в графиках
поведения посетителей. Последнюю доступную дату посмотрите в своих витринах.
Относительный период вроде `Last week` отсчитывается от настоящего времени
и может дать пустой экран.
Ошибки приема считаются по `load_date`, дню загрузки, а поведение посетителей
по модельному времени. Для панели ошибок выберите период загрузки отдельно
и укажите его в подписи. Общий фильтр времени для всего дашборда не требуется.
## Как проверить себя ## Как проверить себя
Эта лаба проверяется глазами. У расположения панелей и понятных подписей нет Автоматической проверки этого задания нет. Числа вы сверите запросами,
счетного инварианта, поэтому задачи для нее в `homework_check` нет. а понятность графиков проверите по подписям.
Откройте список дашбордов и проверьте: Откройте список дашбордов и проверьте:
@@ -75,14 +88,26 @@
серверную; серверную;
- пустой график ошибок нормален, если в таблицах брака сейчас нет строк. - пустой график ошибок нормален, если в таблицах брака сейчас нет строк.
Последний проход сделайте как читатель: найдите на экране самый заметный По готовому экрану запишите три коротких вывода за выбранный период:
канал, страницу и устройство, затем проверьте, понятно ли из подписей, что
именно сравнивается. Если ответ приходится угадывать, поправьте график или
его название.
## Приемка - какой рекламный канал дал больше покупок;
- какая страница получила больше просмотров;
- у какой категории устройств выше доля отказов.
- [ ] пять датасетов читают публичные представления витрин заданий 1–5; Для каждого вывода укажите период и число, на которое опираетесь. Проверьте
- [ ] создан отдельный дашборд, стендовые объекты не изменены; его запросом к своей витрине в SQL Lab с тем же периодом и способом расчета.
- [ ] каждая витрина представлена хотя бы одним графиком; Сохраните запросы рядом с выводами. При равенстве значений назовите всех
- [ ] дашборд проверен глазами по разделу выше. лидеров. Если данных нет, это тоже результат: подтвердите отсутствие строк
запросом.
Проверьте, можно ли найти эти ответы по одним подписям графиков. Если есть
возможность, попросите другого человека прочитать экран без ваших пояснений.
## Что должно получиться
- [ ] у вас есть пять датасетов поверх публичных представлений витрин заданий 1–5;
- [ ] вы собрали свой дашборд, сохранив стендовые объекты без изменений;
- [ ] на дашборде есть хотя бы один график от каждой витрины;
- [ ] вы проверили, что графики и подписи понятны по вопросам из раздела выше;
- [ ] вы записали три вывода и сверили числа с SQL; в отчете указаны периоды
поведения посетителей и ошибок приема.
@@ -0,0 +1,70 @@
# В DBeaver работает, в Superset нет
**Чему учит:** найти причину, по которой один клиент читает объект
хранилища, а другой его не видит.
Факультатив доступен после [ступени 2](../2-1-superset-chart.md).
Свой дашборд для опыта не нужен. Используйте готовую витрину стенда
`dm.daily_traffic_v`.
## Подготовьте опыт
В DBeaver подключитесь к первой ноде пользователем `etl`, как описано
в разделе [«Как применить свой DDL»](../README.md#как-применить-свой-ddl).
Для опыта создайте отдельное представление:
```sql
CREATE VIEW dm.bi_node_probe_v AS
SELECT * FROM dm.daily_traffic_v;
```
Запрос выполнится успешно. Теперь проверьте, откуда можно прочитать
созданное представление. Начните с того же подключения:
```sql
SELECT count() FROM dm.bi_node_probe_v;
```
Повторите запрос в SQL Lab Superset, выбрав подключение `ClickHouse`.
Сохраните текст ошибки.
## Найдите причину
Выясните, к каким серверам обращаются DBeaver и Superset. Адреса подключения
показывают настройки клиентов, а имя ответившего сервера можно спросить
запросом `SELECT hostName()`.
Проверьте наличие `dm.bi_node_probe_v` на обеих нодах. Для второй ноды
заведите подключение DBeaver с портом из
[README](../../../README.md#подключиться-из-dbeaver). Запишите, где объект
есть, и объясните, почему успешного создания в первом подключении оказалось
недостаточно.
Исправьте DDL так, чтобы представление появилось на обеих нодах. Подключение
Superset должно остаться прежним. Сохраните исправленный DDL и повторите
исходный запрос в обоих клиентах.
Если застряли, прочитайте раздел «Устройство зоны» в
[справочнике Superset](../../architecture/superset.md#устройство-зоны) и
[описание распределенного DDL ClickHouse](https://clickhouse.com/docs/reference/statements/distributed-ddl).
## Как проверить себя
В обоих клиентах запрос к вашему представлению должен вернуть столько же
строк, сколько `SELECT count() FROM dm.daily_traffic_v`. Сравнивайте результаты
при завершенном прогоне конвейера, пока мир не продвигается.
Объясните причину ошибки через три факта: куда пришел DDL, куда пришел
запрос Superset и на каких нодах существовало представление.
После опыта удалите только `dm.bi_node_probe_v` на обеих нодах. Не включайте
его в постоянный DDL домашних витрин: это временный объект расследования.
## Что должно получиться
- [ ] у вас сохранены успешный запрос из DBeaver и ошибка из SQL Lab;
- [ ] вы можете объяснить, к каким серверам подключены клиенты, где было
создано представление и почему Superset его не видел;
- [ ] у вас есть исправленный DDL; оба клиента возвращают столько же строк,
сколько исходная витрина;
- [ ] после опыта вы удалили учебное представление с обеих нод.
+34 -8
View File
@@ -40,6 +40,20 @@
- *расследование* — данные уже лежат на стенде или стенд намеренно ломается, а - *расследование* — данные уже лежат на стенде или стенд намеренно ломается, а
менти ищет причину и объясняет ее. менти ищет причину и объясняет ее.
### Как писать задания
Пишите для учащегося: объясняйте, чему он научится, зачем нужны действия
и как понять результат. Историю разработки оставляйте за пределами задания.
«Как проверить себя» описывает действия для проверки и поиска ошибок.
«Что должно получиться» — результат работы и то, что учащийся теперь может
объяснить. Если проверка уже встроена в шаги упражнения, повторять ее
отдельным разделом не нужно.
Остальные заголовки и структуру выбирайте по содержанию. Образцы — задания
про [дашборд Superset](../labs/optional/superset-dashboard.md) и
[расследование подключения](../labs/optional/superset-missing-object.md).
## 3. Основная программа — семь ступеней ## 3. Основная программа — семь ступеней
### Ступень 1. Построй пять витрин слоя DM ### Ступень 1. Построй пять витрин слоя DM
@@ -64,7 +78,7 @@
ступени предстоит написать при реализации программы. Полный собственный дашборд ступени предстоит написать при реализации программы. Полный собственный дашборд
остается факультативом в разделе «Superset и BI». остается факультативом в разделе «Superset и BI».
### Ступень 3. Нарежь сессии сам ### Ступень 3. Нарежьте сессии сами
До того как открыть стендовый SQL нарезки сессий в DDS, менти самостоятельно До того как открыть стендовый SQL нарезки сессий в DDS, менти самостоятельно
делит события на визиты. Сначала он сравнивает число своих сессий с эталоном делит события на визиты. Сначала он сравнивает число своих сессий с эталоном
@@ -75,7 +89,7 @@
Форма — построить свое по ТЗ и провести расследование. Обратная связь состоит Форма — построить свое по ТЗ и провести расследование. Обратная связь состоит
из счетной сверки с `VisitID` и ручного сопоставления границ. из счетной сверки с `VisitID` и ручного сопоставления границ.
### Ступень 4. Перепиши нарезку стенда ### Ступень 4. Перепишите нарезку стенда
Менти удаляет и заново пишет SQL нарезки сессий DDS: расчет объема, пересборку Менти удаляет и заново пишет SQL нарезки сессий DDS: расчет объема, пересборку
и замену. Урок — изменить расчетную сущность внутри готового конвейера и и замену. Урок — изменить расчетную сущность внутри готового конвейера и
@@ -86,7 +100,7 @@
Форма — вычитание. Обратную связь дают `dq_check` и поштучная сверка, Форма — вычитание. Обратную связь дают `dq_check` и поштучная сверка,
построенная на третьей ступени. построенная на третьей ступени.
### Ступень 5. Сначала сторож, потом ломай: заказы ### Ступень 5. Научите проверки замечать ошибки в заказах
Сначала менти строит две недостающие DQ-проверки. Первая утверждает равенство Сначала менти строит две недостающие DQ-проверки. Первая утверждает равенство
«цена × количество = items_total». Вторая сверяет слой с источником по дням. Их «цена × количество = items_total». Вторая сверяет слой с источником по дням. Их
@@ -171,11 +185,23 @@
Открывается после ступени 2 основной программы. Открывается после ступени 2 основной программы.
- **Свой дашборд.** Готовая лаба - **[Свой дашборд](../labs/optional/superset-dashboard.md).** Менти использует
[`docs/labs/optional/superset-dashboard.md`](../labs/optional/superset-dashboard.md) учит результат ступени 2, собирает отчет из пяти своих витрин и подтверждает
хранить пользовательские объекты отдельно от стендовых. выводы запросами. Пользовательские объекты остаются отдельно от стендовых.
- **Почему дашборд не видит объект на второй ноде.** Забытый `ON CLUSTER` может - **[В DBeaver работает, в Superset нет](../labs/optional/superset-missing-object.md).**
остаться незаметным до тех пор, пока дашборд не обратится ко второй ноде. Менти создает отдельное представление на первой ноде и расследует ошибку
чтения из Superset, подключенного ко второй. Опыт заканчивается объяснением,
исправлением DDL и удалением учебного объекта.
Задания самостоятельны: расследование использует готовую витрину стенда.
Отсутствие объекта проверяется в SQL Lab, поскольку создание физического
датасета может остановиться раньше, чем появится график. Чтение только одного
шарда остается материалом инженерного раздела: это отдельная причина
неполного результата, которую добавление `ON CLUSTER` само по себе не устраняет.
Технический опыт проверен 2026-09-13 на ClickHouse 26.3.17.56: локальное
представление недоступно пользователю `bi` на второй ноде; после создания
на кластере обе ноды возвращают тот же счет, что исходная витрина.
### Инженерия стенда ### Инженерия стенда
@@ -0,0 +1,19 @@
slice_name: Средний чек, ₽
description: null
certified_by: null
certification_details: null
viz_type: big_number_total
params:
viz_type: big_number_total
metric: avg_check
adhoc_filters:
- expressionType: SIMPLE
subject: report_date
operator: TEMPORAL_RANGE
comparator: No filter
clause: WHERE
header_font_size: 0.3
cache_timeout: null
uuid: 19600000-0000-4000-8000-000000000106
version: 1.0.0
dataset_uuid: 19600000-0000-4000-8000-000000000001
@@ -0,0 +1,19 @@
slice_name: Заказы
description: null
certified_by: null
certification_details: null
viz_type: big_number_total
params:
viz_type: big_number_total
metric: orders
adhoc_filters:
- expressionType: SIMPLE
subject: report_date
operator: TEMPORAL_RANGE
comparator: No filter
clause: WHERE
header_font_size: 0.3
cache_timeout: null
uuid: 19600000-0000-4000-8000-000000000105
version: 1.0.0
dataset_uuid: 19600000-0000-4000-8000-000000000001
@@ -0,0 +1,19 @@
slice_name: Выручка, ₽
description: null
certified_by: null
certification_details: null
viz_type: big_number_total
params:
viz_type: big_number_total
metric: revenue
adhoc_filters:
- expressionType: SIMPLE
subject: report_date
operator: TEMPORAL_RANGE
comparator: No filter
clause: WHERE
header_font_size: 0.3
cache_timeout: null
uuid: 19600000-0000-4000-8000-000000000104
version: 1.0.0
dataset_uuid: 19600000-0000-4000-8000-000000000001
@@ -0,0 +1,19 @@
slice_name: Посетители
description: null
certified_by: null
certification_details: null
viz_type: big_number_total
params:
viz_type: big_number_total
metric: visitors
adhoc_filters:
- expressionType: SIMPLE
subject: report_date
operator: TEMPORAL_RANGE
comparator: No filter
clause: WHERE
header_font_size: 0.3
cache_timeout: null
uuid: 19600000-0000-4000-8000-000000000107
version: 1.0.0
dataset_uuid: 19600000-0000-4000-8000-000000000003
@@ -0,0 +1,31 @@
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:
- known_share
groupby: []
adhoc_filters:
- expressionType: SIMPLE
subject: report_date
operator: TEMPORAL_RANGE
comparator: No filter
clause: WHERE
row_limit: 10000
seriesType: line
show_legend: false
rich_tooltip: true
show_value: false
markerEnabled: true
markerSize: 6
y_axis_format: .1%
x_axis_time_format: '%d.%m'
tooltipTimeFormat: '%d.%m.%Y'
cache_timeout: null
uuid: 19600000-0000-4000-8000-000000000109
version: 1.0.0
dataset_uuid: 19600000-0000-4000-8000-000000000003
@@ -6,9 +6,9 @@ viz_type: pivot_table_v2
params: params:
viz_type: pivot_table_v2 viz_type: pivot_table_v2
groupbyRows: groupbyRows:
- order_day
groupbyColumns:
- mismatch_class - mismatch_class
groupbyColumns:
- order_day
metrics: metrics:
- order_count - order_count
adhoc_filters: adhoc_filters:
@@ -23,8 +23,7 @@ params:
colOrder: key_a_to_z colOrder: key_a_to_z
order_desc: true order_desc: true
row_limit: 10000 row_limit: 10000
valueFormat: SMART_NUMBER date_format: '%d.%m.%Y'
date_format: smart_date
cache_timeout: null cache_timeout: null
uuid: 19600000-0000-4000-8000-000000000102 uuid: 19600000-0000-4000-8000-000000000102
version: 1.0.0 version: 1.0.0
@@ -1,4 +1,4 @@
slice_name: Выручка по дням и категориям slice_name: Выручка по дням и категориям, ₽
description: null description: null
certified_by: null certified_by: null
certification_details: null certification_details: null
@@ -20,10 +20,13 @@ params:
stack: Stack stack: Stack
orientation: vertical orientation: vertical
show_legend: true show_legend: true
legendType: plain
legendOrientation: bottom
rich_tooltip: true rich_tooltip: true
show_value: false show_value: false
y_axis_format: ',.2f' y_axis_format: ',d'
x_axis_time_format: smart_date x_axis_time_format: '%d.%m'
tooltipTimeFormat: '%d.%m.%Y'
cache_timeout: null cache_timeout: null
uuid: 19600000-0000-4000-8000-000000000101 uuid: 19600000-0000-4000-8000-000000000101
version: 1.0.0 version: 1.0.0
@@ -0,0 +1,29 @@
slice_name: Доля категорий в выручке
description: null
certified_by: null
certification_details: null
viz_type: pie
params:
viz_type: pie
groupby:
- product_category
metric: revenue
adhoc_filters:
- expressionType: SIMPLE
subject: report_date
operator: TEMPORAL_RANGE
comparator: No filter
clause: WHERE
row_limit: 10000
sort_by_metric: true
donut: true
innerRadius: 30
outerRadius: 70
show_labels: true
labels_outside: false
label_type: percent
show_legend: false
cache_timeout: null
uuid: 19600000-0000-4000-8000-000000000108
version: 1.0.0
dataset_uuid: 19600000-0000-4000-8000-000000000001
@@ -17,12 +17,16 @@ params:
comparator: No filter comparator: No filter
clause: WHERE clause: WHERE
row_limit: 10000 row_limit: 10000
seriesType: echarts_timeseries_line seriesType: line
show_legend: true show_legend: true
legendOrientation: bottom
rich_tooltip: true rich_tooltip: true
show_value: false show_value: false
markerEnabled: true
markerSize: 6
y_axis_format: ',d' y_axis_format: ',d'
x_axis_time_format: smart_date x_axis_time_format: '%d.%m'
tooltipTimeFormat: '%d.%m.%Y'
cache_timeout: null cache_timeout: null
uuid: 19600000-0000-4000-8000-000000000103 uuid: 19600000-0000-4000-8000-000000000103
version: 1.0.0 version: 1.0.0
@@ -19,9 +19,79 @@ position:
parents: parents:
- ROOT_ID - ROOT_ID
children: children:
- ROW-kpi
- ROW-revenue - ROW-revenue
- ROW-pvo - ROW-pvo
- ROW-traffic - ROW-traffic
ROW-kpi:
id: ROW-kpi
type: ROW
parents:
- ROOT_ID
- GRID_ID
children:
- CHART-kpi-revenue
- CHART-kpi-orders
- CHART-kpi-avg-check
- CHART-kpi-visitors
meta:
background: BACKGROUND_TRANSPARENT
CHART-kpi-revenue:
id: CHART-kpi-revenue
type: CHART
parents:
- ROOT_ID
- GRID_ID
- ROW-kpi
children: []
meta:
chartId: 4
height: 20
width: 3
sliceName: Выручка, ₽
uuid: 19600000-0000-4000-8000-000000000104
CHART-kpi-orders:
id: CHART-kpi-orders
type: CHART
parents:
- ROOT_ID
- GRID_ID
- ROW-kpi
children: []
meta:
chartId: 5
height: 20
width: 3
sliceName: Заказы
uuid: 19600000-0000-4000-8000-000000000105
CHART-kpi-avg-check:
id: CHART-kpi-avg-check
type: CHART
parents:
- ROOT_ID
- GRID_ID
- ROW-kpi
children: []
meta:
chartId: 6
height: 20
width: 3
sliceName: Средний чек, ₽
uuid: 19600000-0000-4000-8000-000000000106
CHART-kpi-visitors:
id: CHART-kpi-visitors
type: CHART
parents:
- ROOT_ID
- GRID_ID
- ROW-kpi
children: []
meta:
chartId: 7
height: 20
width: 3
sliceName: Посетители
uuid: 19600000-0000-4000-8000-000000000107
ROW-revenue: ROW-revenue:
id: ROW-revenue id: ROW-revenue
type: ROW type: ROW
@@ -30,6 +100,7 @@ position:
- GRID_ID - GRID_ID
children: children:
- CHART-revenue - CHART-revenue
- CHART-revenue-share
meta: meta:
background: BACKGROUND_TRANSPARENT background: BACKGROUND_TRANSPARENT
CHART-revenue: CHART-revenue:
@@ -43,9 +114,23 @@ position:
meta: meta:
chartId: 1 chartId: 1
height: 50 height: 50
width: 12 width: 8
sliceName: Выручка по дням и категориям sliceName: Выручка по дням и категориям, ₽
uuid: 19600000-0000-4000-8000-000000000101 uuid: 19600000-0000-4000-8000-000000000101
CHART-revenue-share:
id: CHART-revenue-share
type: CHART
parents:
- ROOT_ID
- GRID_ID
- ROW-revenue
children: []
meta:
chartId: 8
height: 50
width: 4
sliceName: Доля категорий в выручке
uuid: 19600000-0000-4000-8000-000000000108
ROW-pvo: ROW-pvo:
id: ROW-pvo id: ROW-pvo
type: ROW type: ROW
@@ -66,7 +151,7 @@ position:
children: [] children: []
meta: meta:
chartId: 2 chartId: 2
height: 50 height: 44
width: 12 width: 12
sliceName: Сверка purchase и заказов sliceName: Сверка purchase и заказов
uuid: 19600000-0000-4000-8000-000000000102 uuid: 19600000-0000-4000-8000-000000000102
@@ -78,6 +163,7 @@ position:
- GRID_ID - GRID_ID
children: children:
- CHART-traffic - CHART-traffic
- CHART-known-share
meta: meta:
background: BACKGROUND_TRANSPARENT background: BACKGROUND_TRANSPARENT
CHART-traffic: CHART-traffic:
@@ -91,15 +177,57 @@ position:
meta: meta:
chartId: 3 chartId: 3
height: 50 height: 50
width: 12 width: 6
sliceName: Посетители и известные пользователи sliceName: Посетители и известные пользователи
uuid: 19600000-0000-4000-8000-000000000103 uuid: 19600000-0000-4000-8000-000000000103
CHART-known-share:
id: CHART-known-share
type: CHART
parents:
- ROOT_ID
- GRID_ID
- ROW-traffic
children: []
meta:
chartId: 9
height: 50
width: 6
sliceName: Доля известных пользователей
uuid: 19600000-0000-4000-8000-000000000109
metadata: metadata:
color_scheme: supersetColors color_scheme: supersetColors
label_colors:
Товары для дома: '#1FA8C9'
Текстиль: '#454E7C'
Посуда: '#5AC189'
Бытовая техника: '#FF7F44'
Детские товары: '#E04355'
Одежда и обувь: '#A868B7'
visitors: '#1FA8C9'
known_users: '#454E7C'
known_share: '#454E7C'
cross_filters_enabled: false cross_filters_enabled: false
chart_configuration: {} chart_configuration: {}
expanded_slices: {} expanded_slices: {}
default_filters: '{}' default_filters: '{}'
native_filter_configuration: [] native_filter_configuration:
- id: NATIVE_FILTER-period
name: Период
filterType: filter_time
type: NATIVE_FILTER
targets:
- {}
controlValues:
enableEmptyFilter: false
defaultDataMask:
extraFormData: {}
filterState: {}
ownState: {}
cascadeParentIds: []
scope:
rootPath:
- ROOT_ID
excluded: []
description: ''
theme_uuid: null theme_uuid: null
version: 1.0.0 version: 1.0.0
@@ -36,6 +36,15 @@ metrics:
currency: null currency: null
extra: null extra: null
warning_text: null warning_text: null
- metric_name: known_share
verbose_name: Доля известных
metric_type: null
expression: SUM(known_users) / nullIf(SUM(visitors), 0)
description: null
d3format: .1%
currency: null
extra: null
warning_text: null
columns: columns:
- column_name: _load_ts - column_name: _load_ts
verbose_name: Время загрузки verbose_name: Время загрузки
@@ -23,10 +23,26 @@ metrics:
metric_type: null metric_type: null
expression: SUM(revenue) expression: SUM(revenue)
description: null description: null
d3format: ',.2f' d3format: ',d'
currency: currency: null
symbol: ₽ extra: null
symbolPosition: suffix warning_text: null
- metric_name: orders
verbose_name: Заказы
metric_type: null
expression: SUM(orders)
description: null
d3format: ',d'
currency: null
extra: null
warning_text: null
- metric_name: avg_check
verbose_name: Средний чек
metric_type: null
expression: SUM(revenue) / nullIf(SUM(orders), 0)
description: null
d3format: ',d'
currency: null
extra: null extra: null
warning_text: null warning_text: null
columns: columns:
@@ -44,7 +60,7 @@ columns:
datetime_format: null datetime_format: null
extra: null extra: null
- column_name: aov - column_name: aov
verbose_name: Средний чек verbose_name: Средний чек за день и категорию
is_dttm: false is_dttm: false
is_active: true is_active: true
type: DECIMAL(18, 2) type: DECIMAL(18, 2)
+24 -42
View File
@@ -1,35 +1,20 @@
-- DDS: нарезка событий на визиты и сборка дней в таблицу-двойник. -- DDS: расчет сессий за дни от first_day до last_day включительно.
-- Сначала очищаем промежуточную таблицу session_stage_rep, затем записываем
-- в нее рассчитанные строки через session_stage_dist. Основная session_rep
-- остается прежней до выполнения session_replace.sql.
-- --
-- Двойник очищается целиком в начале, дни собираются одной вставкой на весь -- Первое окно помечает событие единицей, если с предыдущего события того же
-- отрезок — по правилу заказа (sql/dds/order_rebuild.sql). -- посетителя за тот же день прошло больше 1800 секунд. Второе окно суммирует
-- эти единицы: получается номер визита для каждого посетителя за день.
-- PARTITION BY (client_id, event_date) считает каждый модельный день отдельно:
-- события разных дней не попадут в одну сессию даже при короткой паузе.
-- Учитываются все типы событий.
-- --
-- Нарезка — центральный расчёт слоя, и стоит она на двух окнах подряд. -- У первого события lagInFrame возвращает его собственное время: разница
-- Первое спрашивает у каждого события, сколько прошло с предыдущего события -- равна нулю, opens_visit = 0, и первый visit_no тоже равен нулю.
-- той же куки в тот же день, и ставит единицу там, где пауза длиннее -- Оба окна используют (event_time, event_id), чтобы при равном времени
-- таймаута: это шов между визитами. Второе считает нарастающий итог этих -- отметка начала и номер визита относились к одним событиям.
-- единиц — номер визита внутри дня куки. Дальше визит становится обычной -- Порог 1800 секунд — правило визита; он не передается параметром.
-- группировкой.
--
-- Границу суток окна не проверяют: она уже в разбиении PARTITION BY
-- (client_id, event_date), поэтому визит через полночь не склеится и при
-- короткой паузе. Так правило визита ложится в запрос целиком: одна кука,
-- один день, паузы не длиннее таймаута (CONTEXT.md, «Визит»).
--
-- Первое событие куки за день сравнивается само с собой — третий аргумент
-- lagInFrame. Паузы у него нет, шов ему не нужен, и номер первого визита
-- получается нулём: номера различают визиты внутри дня куки, а не считают их
-- от единицы.
--
-- Порядок в окнах полный — метка времени и event_id. Окна считаются двумя
-- разными сортировками (видно в EXPLAIN PIPELINE), и при равных метках они
-- вправе разойтись: шов достался бы одной строке, а нарастающий итог другой,
-- и сессия разъехалась бы надвое.
--
-- Тридцать минут написаны числом, а не приехали параметром: это правило
-- аналитики, а не константа мира. Генератор режет визиты тем же порогом;
-- разойдись эти два числа — увидит счётная сверка с VisitID
-- (docs/architecture/dds/session.md, «Сверка с эталоном»).
ALTER TABLE dds.session_stage_rep ON CLUSTER clickstream_cluster DROP PARTITION ALL; ALTER TABLE dds.session_stage_rep ON CLUSTER clickstream_cluster DROP PARTITION ALL;
INSERT INTO dds.session_stage_dist INSERT INTO dds.session_stage_dist
@@ -93,8 +78,8 @@ WITH
FROM seams FROM seams
) )
SELECT SELECT
-- Чекан визита: кука и его начало. Время входит в хеш секундой эпохи, -- Идентификатор зависит от посетителя и начала визита.
-- поэтому id держится за содержание сессии, а не за порядок сборки. -- Повторная сборка тех же событий дает тот же session_id.
cityHash64(client_id, min(event_time)), cityHash64(client_id, min(event_time)),
client_id, client_id,
event_date, event_date,
@@ -107,8 +92,10 @@ SELECT
countIf(event_type = 'purchase'), countIf(event_type = 'purchase'),
argMin(url, event_time), argMin(url, event_time),
argMax(url, event_time), argMax(url, event_time),
-- Атрибуция и паспорт куки берутся с первого события визита. У паспорта -- UTM, устройство и город берутся по минимальному event_time.
-- любое событие дало бы то же: устройство и город у куки постоянны. -- Как и у страниц выше, при равном времени выбор между событиями
-- не определен: event_id в эти argMin/argMax не входит.
-- Устройство и город на стенде постоянны для одной куки.
argMin(utm_source, event_time), argMin(utm_source, event_time),
argMin(utm_medium, event_time), argMin(utm_medium, event_time),
argMin(utm_campaign, event_time), argMin(utm_campaign, event_time),
@@ -120,12 +107,7 @@ SELECT
now64(3, 'UTC') now64(3, 'UTC')
FROM numbered FROM numbered
GROUP BY client_id, event_date, visit_no GROUP BY client_id, event_date, visit_no
-- Капкана parallel_distributed_insert_select здесь нет, и это выигрыш -- Источник и цель распределены по cityHash64(client_id): локальная вставка
-- ко-локации: ключ шардирования у источника и цели один — cityHash64 от -- сохраняет нужную раскладку, parallel_distributed_insert_select менять
-- куки, — поэтому визит ляжет туда же, где лежали его события, исполнись -- не требуется. Перед заменой партиций ждем доставки строк на шарды.
-- вставка хоть на инициаторе, хоть локально на шардах. Заказу приходится
-- называть ноль вслух: совпадение ключей у него случайное
-- (sql/dds/order_rebuild.sql). Здесь выбор сделан обратный и он тоже
-- осознанный: настройку не называть, чтобы не притворяться, будто её значение
-- что-то решает.
SETTINGS distributed_foreground_insert = 1; SETTINGS distributed_foreground_insert = 1;
+8 -8
View File
@@ -1,11 +1,11 @@
-- DDS: подмена одного дня сессий собранной партицией двойника. -- DDS: заменяем раздел session_rep за выбранный день готовым разделом
-- промежуточной session_stage_rep. Раздел таблицы называется партицией.
-- ON CLUSTER выполняет замену на локальных таблицах каждого шарда.
-- На одном шарде раздел меняется целиком; общей транзакции для кластера нет.
-- Повтор с теми же строками session_stage_rep не добавляет копий в session_rep.
-- --
-- Механика и доводы целиком — sql/dds/order_replace.sql: замена идёт по -- Дату из session_scope.sql подставляет Jinja: параметр ClickHouse в
-- локальным таблицам, шард за шардом, атомарность пошардовая, день -- ALTER ... PARTITION здесь не поддерживается. Поиск дней следующего
-- подставляет Jinja, потому что параметров ALTER … PARTITION не берёт. -- запуска не обнаружит неполный день, если часть его строк уже есть в DDS.
--
-- Разница одна, и она про отказ. Несобранный день заказа вернуло бы в работу
-- следующее окно; сессиям возвращать нечем, и упавшая замена оставляет дыру
-- до тех пор, пока её не найдёт счёт хвоста (sql/dds/session_scope.sql).
ALTER TABLE dds.session_rep ON CLUSTER clickstream_cluster ALTER TABLE dds.session_rep ON CLUSTER clickstream_cluster
REPLACE PARTITION '{{ params.day }}' FROM dds.session_stage_rep; REPLACE PARTITION '{{ params.day }}' FROM dds.session_stage_rep;
+16 -35
View File
@@ -1,50 +1,31 @@
-- DDS: объём пересборки сессий — какие дни собирает этот запуск. -- DDS: дни для сборки сессий, от first_day до last_day включительно.
-- Без событий ODS строк в ответе нет; NULL в first_day — дней для расчета нет.
-- --
-- Ответ — две даты, границы непрерывного отрезка; список дней между ними -- position — номер следующего дня для генерации. Последний прожитый день
-- разворачивает даг. Пустой ответ означает пустой источник. Пустой первый -- равен min(EventDate) + position - 1. В отличие от max(EventDate), эта
-- день — что собирать нечего: все прожитые дни уже в слое, и прогон честно -- граница исключает еще идущий день. Ранние события ODS не удаляются по TTL,
-- пропускает сборку. -- поэтому min(EventDate) сохраняет начало модельного календаря.
-- --
-- Файл кончается без точки с запятой: почему — sql/dds/order_scope.sql. -- До этого запроса даг ждет чтения hits и опустошения очереди ods.event_dist.
-- Позиция мира сама по себе не подтверждает доставку событий.
-- --
-- Правый край — последний прожитый день, и знает его только позиция мира. -- Точки с запятой в конце нет: драйвер добавляет FORMAT к этому запросу.
-- События живой день от прожитого не отличают: и посреди дня D, и сразу после
-- его конца max(EventDate) = D. А разница несущая — визит, начатый в 23:50,
-- посреди дня ещё идёт, и собранная сессия оказалась бы обрезанной навсегда:
-- пересобирать замёрзший день слой не станет
-- (docs/architecture/dds/session.md, «Движок и наполнение»).
--
-- Позиция — номер первого несыгранного дня, счётчик прожитого. Своей копии
-- начала оси стенд из-за неё не заводит: начало он спрашивает у данных, и
-- дата дня N в них — константа, начало оси плюс N. Держится якорь на том, что
-- ранние дни из ods.event не исчезают: срок жизни назначен сырым данным и таблицам
-- ошибок, самому событию — нет (sql/ddl/20-ods-tables.sql).
--
-- За «доехал» позиция не отвечает — только за «прожит». Перед расчётом объёма
-- первый шаг дага дожидается хвоста потока и очереди доставки: иначе
-- недожеванный хвост заморозил бы недособранный день
-- (docs/architecture/etl.md, «Готовность источника»).
WITH WITH
(SELECT min(EventDate) FROM ods.event_dist) AS first_source_day, (SELECT min(EventDate) FROM ods.event_dist) AS first_source_day,
first_source_day + ({position:UInt16} - 1) AS last_lived_day, first_source_day + ({position:UInt16} - 1) AS last_lived_day,
-- Хвост считается от первой дыры, а не от последнего собранного дня: -- Ищем первую дату событий ODS, за которую еще нет сессий DDS.
-- замены партиций падают порознь, и счёт по максимуму собранного дня -- Поиск после max(session_date) пропустил бы более раннюю потерянную дату.
-- дыру позади себя не заметил бы (доводы целиком — sql/dds/order_scope.sql). -- Если за день есть хотя бы одна сессия, он уже не считается пропущенным.
-- У сессий это единственный способ дыру найти: дышащего окна, которое -- GLOBAL берет даты сессий со всех шардов.
-- пересобрало бы день заодно, здесь нет. -- minOrNull возвращает NULL, если пропусков нет.
--
-- GLOBAL обязателен: обе таблицы распределённые, и обычный NOT IN сверял
-- бы дни источника с локальной частью слоя на каждом шарде.
-- minOrNull отличает «дыр нет» от даты по умолчанию.
( (
SELECT minOrNull(day) SELECT minOrNull(day)
FROM (SELECT DISTINCT EventDate AS day FROM ods.event_dist) FROM (SELECT DISTINCT EventDate AS day FROM ods.event_dist)
WHERE day GLOBAL NOT IN (SELECT session_date FROM dds.session_dist) WHERE day GLOBAL NOT IN (SELECT session_date FROM dds.session_dist)
) AS first_missing_day ) AS first_missing_day
SELECT SELECT
-- Живой день в слое отсутствует всегда, и работой это не считается: -- Отсутствующий день попадет в сборку только после его завершения.
-- дырой он станет, только когда мир его проживёт.
if(first_missing_day <= last_lived_day, first_missing_day, NULL) AS first_day, if(first_missing_day <= last_lived_day, first_missing_day, NULL) AS first_day,
last_lived_day AS last_day last_lived_day AS last_day
-- FINAL не нужен: повтор доставки не двигает ни край оси, ни набор дней. -- FINAL не нужен: повтор доставки не меняет начало календаря и набор дат.
WHERE (SELECT count() FROM ods.event_dist) > 0 WHERE (SELECT count() FROM ods.event_dist) > 0
+12 -8
View File
@@ -1,7 +1,10 @@
-- DM: сборка выручки в донор перед заменой дневных партиций. -- DM: расчет выручки за дни от first_day до last_day включительно.
-- DROP PARTITION ALL очищает локальные таблицы на каждом узле; причина не -- DROP PARTITION ALL очищает промежуточную revenue_daily_stage_rep.
-- применять TRUNCATE разобрана в соседнем sql/dds/order_rebuild.sql. -- Затем INSERT записывает по одной строке итогов на день и категорию товара.
-- Основная revenue_daily_rep остается прежней до revenue_daily_replace.sql.
-- В расчет входят только оплаченные заказы. ARRAY JOIN превращает артикул,
-- количество и цену с одним индексом в массивах в одну строку позиции заказа.
-- Причина выбора DROP вместо TRUNCATE — в sql/dds/order_rebuild.sql.
ALTER TABLE dm.revenue_daily_stage_rep ON CLUSTER clickstream_cluster ALTER TABLE dm.revenue_daily_stage_rep ON CLUSTER clickstream_cluster
DROP PARTITION ALL; DROP PARTITION ALL;
@@ -22,8 +25,8 @@ SELECT
orders, orders,
units, units,
CAST(revenue, 'Decimal(18, 2)'), CAST(revenue, 'Decimal(18, 2)'),
-- Обычное деление Decimal усекает доли копейки вместо округления; -- Делим с шестью знаками после запятой, затем округляем до копеек.
-- запас масштаба сохраняет их до денежного результата. -- Так дробная часть сохраняется до округления среднего чека.
CAST( CAST(
round(divideDecimal(revenue, toDecimal128(orders, 0), 6), 2), round(divideDecimal(revenue, toDecimal128(orders, 0), 6), 2),
'Decimal(18, 2)' 'Decimal(18, 2)'
@@ -49,7 +52,8 @@ FROM
report_date, report_date,
product_category product_category
) )
-- Источник лежит по заказу, цель — по категории. Ноль заставляет инициатор -- Источник распределен по заказу, цель — по категории. Значение 0 оставляет
-- слить частичные агрегаты и разложить готовые строки по ключу цели. -- итоговую агрегацию и распределение по ключу цели на ноде, принявшей запрос.
-- distributed_foreground_insert = 1 ждет доставки строк перед заменой.
SETTINGS distributed_foreground_insert = 1, SETTINGS distributed_foreground_insert = 1,
parallel_distributed_insert_select = 0; parallel_distributed_insert_select = 0;
+7 -6
View File
@@ -1,8 +1,9 @@
-- DM: подмена одного дня выручки готовой партицией донора. -- DM: заменяем раздел revenue_daily_rep за выбранный день готовым разделом
-- -- промежуточной revenue_daily_stage_rep. Раздел таблицы называется партицией.
-- День приходит из результата revenue_daily_scope.sql, а не от пользователя. -- Дату из revenue_daily_scope.sql подставляет Jinja: параметр ClickHouse
-- Jinja нужна потому, что ClickHouse не подставляет параметр в PARTITION. -- в ALTER ... PARTITION здесь не поддерживается.
-- Замена идет на локальной таблице ON CLUSTER: граница атомарности и повтор -- ON CLUSTER выполняет замену на локальных таблицах каждого шарда.
-- разобраны на таком же шаге в sql/dds/order_replace.sql. -- На одном шарде раздел меняется целиком; общей транзакции для кластера нет.
-- Повтор с теми же строками промежуточной таблицы не добавляет копий в основной.
ALTER TABLE dm.revenue_daily_rep ON CLUSTER clickstream_cluster ALTER TABLE dm.revenue_daily_rep ON CLUSTER clickstream_cluster
REPLACE PARTITION '{{ params.day }}' FROM dm.revenue_daily_stage_rep; REPLACE PARTITION '{{ params.day }}' FROM dm.revenue_daily_stage_rep;
+10 -14
View File
@@ -1,14 +1,11 @@
-- DM: объем пересборки выручки. -- DM: диапазон пересборки выручки, first_day и last_day включительно.
-- Последний слепок ODS задает даты оформления заказов, которые еще могут
-- измениться. Саму выручку считаем по DDS; ODS нужен только для выбора дат.
-- Левая граница расширяется до первого дня с оплаченными заказами DDS,
-- которого нет в витрине. Так первый запуск охватывает историю выручки.
-- По наличию даты нельзя обнаружить частично заполненный день.
-- --
-- Последний слепок ODS называет дни, состояние которых источник еще может -- Точки с запятой в конце нет: драйвер добавляет FORMAT к этому запросу.
-- изменить. Сам агрегат читает только DDS; ODS здесь нужен лишь для границ.
-- Первый запуск и повтор после частичного отказа расширяют левую границу до
-- первого дня оплаченных заказов, которого еще нет в витрине.
-- Почему хвост начинается с первого пропуска и чем за это платит ODS,
-- разобрано в sql/dds/order_scope.sql.
--
-- Файл кончается без точки с запятой: даг читает его одним запросом и ждет
-- одну строку с границами.
WITH WITH
(SELECT max(snapshot_date) FROM ods.order_dist) AS last_snapshot, (SELECT max(snapshot_date) FROM ods.order_dist) AS last_snapshot,
( (
@@ -24,8 +21,7 @@ WITH
( (
SELECT minOrNull(report_date) SELECT minOrNull(report_date)
FROM (SELECT DISTINCT order_date AS report_date FROM dds.order_v WHERE status = 'paid') FROM (SELECT DISTINCT order_date AS report_date FROM dds.order_v WHERE status = 'paid')
-- Правая сторона тоже распределенная: GLOBAL собирает множество дней -- GLOBAL собирает список дней витрины со всех шардов.
-- на инициаторе и не превращает запрос в запрещенное двойное чтение.
WHERE report_date GLOBAL NOT IN ( WHERE report_date GLOBAL NOT IN (
SELECT report_date FROM dm.revenue_daily_dist SELECT report_date FROM dm.revenue_daily_dist
) )
@@ -33,7 +29,7 @@ WITH
SELECT SELECT
least(window_start, coalesce(first_missing_day, window_start)) AS first_day, least(window_start, coalesce(first_missing_day, window_start)) AS first_day,
window_end AS last_day window_end AS last_day
-- Без ODS неизвестно окно. Без готового DDS донор останется пустым, а замена -- Для выбора диапазона нужны заказы в ODS, для расчета — в DDS.
-- сотрет уже собранные дни выручки. Поэтому для прогона нужны оба источника. -- Эта проверка наличия строк не подтверждает полноту источников.
WHERE (SELECT count() FROM dds.order_v) > 0 WHERE (SELECT count() FROM dds.order_v) > 0
AND (SELECT count() FROM ods.order_dist) > 0 AND (SELECT count() FROM ods.order_dist) > 0