Compare commits
33
Commits
3ad377e02e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
453a1d6ddf | ||
|
|
9394b789e5 | ||
|
|
4693bc5af9 | ||
|
|
3c3eb825e1 | ||
|
|
d6b172e511 | ||
|
|
b839a671a1 | ||
|
|
c1d3a6db9b | ||
|
|
bb8605eed7 | ||
|
|
3e9249933b | ||
|
|
bb087f4477 | ||
|
|
515f386e5f | ||
|
|
2502277af1 | ||
|
|
ddb186f841 | ||
|
|
090df8a715 | ||
|
|
7a002f4d5a | ||
|
|
d15f4fe4d8 | ||
|
|
431fed72ad | ||
|
|
75f28fe99f | ||
|
|
83434102d5 | ||
|
|
086007fbd4 | ||
|
|
5f317ba8d2 | ||
|
|
84dc718f62 | ||
|
|
5a3e3f92d9 | ||
|
|
f3373f1e8f | ||
|
|
915e976fc2 | ||
|
|
ce15836093 | ||
|
|
8c9c10a875 | ||
|
|
1b95ce456c | ||
|
|
3bfe9ca4ee | ||
|
|
4058e18864 | ||
|
|
a03838b953 | ||
|
|
3ac3911e95 | ||
|
|
b6c10ecf21 |
No files matched your search
@@ -10,6 +10,7 @@ CLICKHOUSE_ETL_PASSWORD=etl-26-writer
|
|||||||
CLICKHOUSE_LIFECYCLE_PASSWORD=lifecycle-26-owner
|
CLICKHOUSE_LIFECYCLE_PASSWORD=lifecycle-26-owner
|
||||||
CLICKHOUSE_BI_PASSWORD=bi-26-reader
|
CLICKHOUSE_BI_PASSWORD=bi-26-reader
|
||||||
CLICKHOUSE_ANALYST_PASSWORD=analyst-26-reader
|
CLICKHOUSE_ANALYST_PASSWORD=analyst-26-reader
|
||||||
|
CLICKHOUSE_STUDENT_PASSWORD=student-26-lab
|
||||||
CLICKHOUSE_GRAFANA_PASSWORD=grafana-26-reader
|
CLICKHOUSE_GRAFANA_PASSWORD=grafana-26-reader
|
||||||
KAFKA_EXTERNAL_PORT=29092
|
KAFKA_EXTERNAL_PORT=29092
|
||||||
PROMETHEUS_PORT=29090
|
PROMETHEUS_PORT=29090
|
||||||
|
|||||||
@@ -46,15 +46,10 @@
|
|||||||
|
|
||||||
## Кому что поручать
|
## Кому что поручать
|
||||||
|
|
||||||
Читатель здесь не побочный потребитель, а тот, ради кого стенд существует:
|
Автор задачи сам пишет и редактирует код, комментарии и документы, если
|
||||||
код, комментарии и документы и есть продукт. Поэтому всё, что человек будет
|
владелец не задал другое распределение работы.
|
||||||
читать и разбирать, пишет модель с чувством меры — сейчас это Opus. Codex
|
|
||||||
берёт техническую работу без вкусовых решений: миграции, разбор журналов и
|
|
||||||
объёмного вывода, обход репозитория, повторяющиеся правки. Там его запас
|
|
||||||
лимитов работает на нас, а экономить их на читаемом коде — экономия ложная:
|
|
||||||
переписывать выйдет дороже, чем сразу написать хорошо.
|
|
||||||
|
|
||||||
Делить работу между ними стоит не целыми задачами, а подзадачами, и решать
|
Делить работу стоит не целыми задачами, а подзадачами, и решать
|
||||||
это при планировании. Вкусовая часть идёт первой: она задаёт форму, имена и
|
это при планировании. Вкусовая часть идёт первой: она задаёт форму, имена и
|
||||||
границы, под которые механическая потом подстраивается.
|
границы, под которые механическая потом подстраивается.
|
||||||
|
|
||||||
@@ -81,6 +76,9 @@ Airflow) и названия из кода. Если для понятия ес
|
|||||||
писал: простые слова, короткие фразы, сложную мысль поясняй при первом
|
писал: простые слова, короткие фразы, сложную мысль поясняй при первом
|
||||||
упоминании. Если понятность и буквальная точность спорят — выбирай понятность.
|
упоминании. Если понятность и буквальная точность спорят — выбирай понятность.
|
||||||
|
|
||||||
|
При создании или редактировании лабораторных следуй
|
||||||
|
[правилам учебного текста](docs/specs/2026-09-01-labs-program.md#как-писать-задания).
|
||||||
|
|
||||||
Внутренние рассуждения и промежуточные пометки по ходу работы веди на
|
Внутренние рассуждения и промежуточные пометки по ходу работы веди на
|
||||||
английском — он экономнее по токенам. На русском остаётся всё, что видит и
|
английском — он экономнее по токенам. На русском остаётся всё, что видит и
|
||||||
хранит проект: итоговые ответы, документы, комментарии в коде и SQL, сообщения
|
хранит проект: итоговые ответы, документы, комментарии в коде и SQL, сообщения
|
||||||
|
|||||||
+247
-201
@@ -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 или файл. В пакетном режиме он отправляет их без пауз,
|
||||||
|
в режиме живого дня — с ускорением по времени событий.
|
||||||
@@ -5,132 +5,156 @@
|
|||||||
> курсовая работа. Если вам нужна помощь, обратитесь к автору материалов —
|
> курсовая работа. Если вам нужна помощь, обратитесь к автору материалов —
|
||||||
> ментору [@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. Остальные пользователи и роли
|
Для заданий с собственными таблицами наблюдений используйте `student` и пароль
|
||||||
описаны в [справочнике
|
из `CLICKHOUSE_STUDENT_PASSWORD` в `.env`. Он читает те же источники, а создавать,
|
||||||
стенда](docs/architecture/stand.md#пользователи-и-роли-clickhouse).
|
заполнять и удалять таблицы может только в базе `default`.
|
||||||
|
Для создания учебных витрин используйте подключение, описанное
|
||||||
|
в [инструкции к заданиям](docs/labs/README.md#выполнение-sql-в-dbeaver).
|
||||||
|
Нативные порты `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) — правила работы в репозитории для ИИ-агентов.
|
||||||
@@ -20,6 +20,7 @@ x-clickhouse-common: &clickhouse-common
|
|||||||
CLICKHOUSE_LIFECYCLE_PASSWORD: ${CLICKHOUSE_LIFECYCLE_PASSWORD:?Скопируйте .env.example в .env}
|
CLICKHOUSE_LIFECYCLE_PASSWORD: ${CLICKHOUSE_LIFECYCLE_PASSWORD:?Скопируйте .env.example в .env}
|
||||||
CLICKHOUSE_BI_PASSWORD: ${CLICKHOUSE_BI_PASSWORD:?Скопируйте .env.example в .env}
|
CLICKHOUSE_BI_PASSWORD: ${CLICKHOUSE_BI_PASSWORD:?Скопируйте .env.example в .env}
|
||||||
CLICKHOUSE_ANALYST_PASSWORD: ${CLICKHOUSE_ANALYST_PASSWORD:?Скопируйте .env.example в .env}
|
CLICKHOUSE_ANALYST_PASSWORD: ${CLICKHOUSE_ANALYST_PASSWORD:?Скопируйте .env.example в .env}
|
||||||
|
CLICKHOUSE_STUDENT_PASSWORD: ${CLICKHOUSE_STUDENT_PASSWORD:?Добавьте CLICKHOUSE_STUDENT_PASSWORD из .env.example в .env}
|
||||||
CLICKHOUSE_GRAFANA_PASSWORD: ${CLICKHOUSE_GRAFANA_PASSWORD:?Скопируйте .env.example в .env}
|
CLICKHOUSE_GRAFANA_PASSWORD: ${CLICKHOUSE_GRAFANA_PASSWORD:?Скопируйте .env.example в .env}
|
||||||
depends_on:
|
depends_on:
|
||||||
clickhouse-keeper:
|
clickhouse-keeper:
|
||||||
|
|||||||
+24
-32
@@ -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
@@ -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]:
|
||||||
|
|||||||
+23
-8
@@ -74,7 +74,11 @@ def _publish_summary(
|
|||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""Записать дневную сводку и вернуть малый срез готовой диагностики."""
|
"""Записать дневную сводку и вернуть малый срез готовой диагностики."""
|
||||||
if not summaries:
|
if not summaries:
|
||||||
raise AirflowException(f"{check_name}: область проверки пуста")
|
raise AirflowException(
|
||||||
|
f"{check_name}: запрос не вернул ни одного дня для проверки; "
|
||||||
|
"проверьте наличие данных в источниках сверки. Состав проверок "
|
||||||
|
"описан в docs/architecture/dm.md"
|
||||||
|
)
|
||||||
|
|
||||||
diagnostics: list[str] = []
|
diagnostics: list[str] = []
|
||||||
context = get_current_context()
|
context = get_current_context()
|
||||||
@@ -148,7 +152,10 @@ def build_classes_vs_inventory() -> list[str]:
|
|||||||
for mismatch_class, count in day["orders"].items()
|
for mismatch_class, count in day["orders"].items()
|
||||||
}
|
}
|
||||||
if not expected:
|
if not expected:
|
||||||
raise AirflowException("classes_vs_inventory: в паспорте нет закрытых дней")
|
raise AirflowException(
|
||||||
|
"classes_vs_inventory: в data/world-inventory.json нет счетчиков "
|
||||||
|
"заказов для закрытых дней; сообщите наставнику"
|
||||||
|
)
|
||||||
|
|
||||||
query = (SQL_ROOT / "dq" / "classes_vs_inventory.sql").read_text(encoding="utf-8")
|
query = (SQL_ROOT / "dq" / "classes_vs_inventory.sql").read_text(encoding="utf-8")
|
||||||
# GROUP BY закономерно не возвращает группу из нуля строк.
|
# GROUP BY закономерно не возвращает группу из нуля строк.
|
||||||
@@ -179,9 +186,9 @@ def build_classes_vs_inventory() -> list[str]:
|
|||||||
failed_rows += 1
|
failed_rows += 1
|
||||||
actual_text = actual_count if actual_count is not None else "нет ключа"
|
actual_text = actual_count if actual_count is not None else "нет ключа"
|
||||||
details.append(
|
details.append(
|
||||||
f"{day} key={mismatch_class}: "
|
f"{day} mismatch_class={mismatch_class}: "
|
||||||
f"эталон={reference_count}, "
|
f"заказов в паспорте={reference_count}, "
|
||||||
f"объект={actual_text}"
|
f"в dm.purchase_vs_orders_v={actual_text}"
|
||||||
)
|
)
|
||||||
summaries.append((data_date, reference_rows, actual_rows, failed_rows, details))
|
summaries.append((data_date, reference_rows, actual_rows, failed_rows, details))
|
||||||
return _publish_summary("classes_vs_inventory", summaries)
|
return _publish_summary("classes_vs_inventory", summaries)
|
||||||
@@ -194,7 +201,10 @@ def publication_scope() -> list[dict[str, dict[str, str]]]:
|
|||||||
"SELECT DISTINCT data_date FROM dm.dq_summary_stage_dist ORDER BY data_date"
|
"SELECT DISTINCT data_date FROM dm.dq_summary_stage_dist ORDER BY data_date"
|
||||||
)
|
)
|
||||||
if not rows:
|
if not rows:
|
||||||
raise AirflowException("донор dm.dq_summary пуст: публиковать нечего")
|
raise AirflowException(
|
||||||
|
"dm.dq_summary_stage_dist пуста: нет результатов для записи "
|
||||||
|
"в сводку DQ; проверьте журналы задач build_* этого запуска"
|
||||||
|
)
|
||||||
return [{"params": {"day": data_date.isoformat()}} for (data_date,) in rows]
|
return [{"params": {"day": data_date.isoformat()}} for (data_date,) in rows]
|
||||||
|
|
||||||
|
|
||||||
@@ -239,10 +249,15 @@ def assert_check(check_name: str, diagnostics: list[str]) -> None:
|
|||||||
logging.error("%s: %s", check_name, detail)
|
logging.error("%s: %s", check_name, detail)
|
||||||
summary = "; ".join(
|
summary = "; ".join(
|
||||||
f"{data_date}: эталонных ключей {reference_rows}, "
|
f"{data_date}: эталонных ключей {reference_rows}, "
|
||||||
f"фактических {actual_rows}, нарушено {failed_rows}"
|
f"ключей в проверяемом объекте {actual_rows}, "
|
||||||
|
f"отсутствуют или различаются {failed_rows}"
|
||||||
for data_date, reference_rows, actual_rows, failed_rows in failed_days
|
for data_date, reference_rows, actual_rows, failed_rows in failed_days
|
||||||
)
|
)
|
||||||
raise AirflowException(f"{check_name}: {summary}")
|
raise AirflowException(
|
||||||
|
f"{check_name}: {summary}. Примеры расхождений — в журнале этой задачи; "
|
||||||
|
"итоги по дням — в dm.dq_summary_v. Источники каждой сверки описаны "
|
||||||
|
"в docs/architecture/dm.md"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dag(
|
@dag(
|
||||||
|
|||||||
@@ -45,7 +45,10 @@ def check_mart(
|
|||||||
"""Сверить одну домашнюю витрину с независимым ответом из источника."""
|
"""Сверить одну домашнюю витрину с независимым ответом из источника."""
|
||||||
hook = _clickhouse_hook()
|
hook = _clickhouse_hook()
|
||||||
if not _mart_exists(hook, mart):
|
if not _mart_exists(hook, mart):
|
||||||
raise AirflowException(f"витрина {mart} не найдена; выполните ТЗ {lab_path}")
|
raise AirflowException(
|
||||||
|
f"витрина {mart} не найдена на узле подключения clickhouse_default; "
|
||||||
|
f"проверьте имя и место создания витрины по заданию {lab_path}"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
query = (SQL_ROOT / sql_name).read_text(encoding="utf-8")
|
query = (SQL_ROOT / sql_name).read_text(encoding="utf-8")
|
||||||
@@ -72,7 +75,8 @@ def check_mart(
|
|||||||
except Exception as error:
|
except Exception as error:
|
||||||
raise AirflowException(
|
raise AirflowException(
|
||||||
f"не удалось проверить витрину {mart}: {error}; "
|
f"не удалось проверить витрину {mart}: {error}; "
|
||||||
f"сверьте договор колонок и проверку в ТЗ {lab_path}"
|
f"сверьте имена и типы колонок с заданием {lab_path}. "
|
||||||
|
"Подробности ошибки — в журнале этой задачи"
|
||||||
) from error
|
) from error
|
||||||
|
|
||||||
failed_rows = sum(summary[3] for summary in summaries)
|
failed_rows = sum(summary[3] for summary in summaries)
|
||||||
@@ -93,7 +97,8 @@ def check_mart(
|
|||||||
compared_sides = diagnostics[0]
|
compared_sides = diagnostics[0]
|
||||||
raise AirflowException(
|
raise AirflowException(
|
||||||
f"витрина {mart} расходится с источником: {days}. "
|
f"витрина {mart} расходится с источником: {days}. "
|
||||||
f"Пример сравнения: {compared_sides}. Сверьтесь с ТЗ {lab_path}"
|
f"Пример сравнения: {compared_sides}. Другие примеры — в журнале этой "
|
||||||
|
f"задачи. Проверьте расчет для указанных ключей по заданию {lab_path}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+14
-21
@@ -1,18 +1,13 @@
|
|||||||
"""Приём заказов: пакетный забор слепка из Kafka в хранилище.
|
"""Пакетный прием заказов: Kafka → STG → ODS.
|
||||||
|
|
||||||
Путь Kafka → STG → ODS у заказов один и живёт одним дагом: сначала забор
|
Шаговый даг модельного времени отправляет слепок, запускает orders_ingest
|
||||||
порции в `stg`, затем разбор того же среза в типизированный ODS и в таблицу
|
и ждет его завершения. Собственного расписания у orders_ingest нет.
|
||||||
брака. Расписания у дага нет: его дёргает тот, кто положил слепок в топик, —
|
Сначала pull_batch сохраняет порцию сообщений в STG, затем parse_batch
|
||||||
шаговый даг модельного времени после проигрыша дня, — и ждёт конца.
|
разбирает тот же срез по _load_id в заказы и ошибки.
|
||||||
|
|
||||||
Контраст с приёмом событий и есть урок. События тянет матвью, навсегда
|
События принимаются непрерывно через материализованные представления.
|
||||||
подписанная на чтеца: приём идёт сам, пока идёт поток. Слепок заказов
|
Заказы читаются одним запросом; полнота слепка этим не гарантируется.
|
||||||
приезжает раз в модельный день целой выгрузкой, у которой есть начало и конец,
|
Правила и ограничения — docs/architecture/orders/ingestion.md.
|
||||||
и забирает её запрос по команде. Push против pull — два режима на одном стенде,
|
|
||||||
каждый там, где ему место по природе источника.
|
|
||||||
|
|
||||||
Решения и доводы целиком — ADR 0008 (забор) и ADR 0010 (версии в ODS); форма
|
|
||||||
перехода — docs/architecture/orders/ingestion.md.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -24,8 +19,7 @@ from airflow.sdk import dag
|
|||||||
|
|
||||||
START_DATE = datetime.datetime(2026, 1, 1, tzinfo=datetime.UTC)
|
START_DATE = datetime.datetime(2026, 1, 1, tzinfo=datetime.UTC)
|
||||||
|
|
||||||
# Запросы лежат файлами в sql/ репозитория, разложенные по слоям хранилища;
|
# Compose монтирует sql/ по этому пути во все службы Airflow.
|
||||||
# сюда их монтирует compose — всем службам Airflow сразу.
|
|
||||||
SQL_ROOT = "/opt/airflow/sql"
|
SQL_ROOT = "/opt/airflow/sql"
|
||||||
|
|
||||||
|
|
||||||
@@ -34,15 +28,14 @@ SQL_ROOT = "/opt/airflow/sql"
|
|||||||
schedule=None,
|
schedule=None,
|
||||||
start_date=START_DATE,
|
start_date=START_DATE,
|
||||||
is_paused_upon_creation=False,
|
is_paused_upon_creation=False,
|
||||||
# Чтец у топика один, и группа потребителей у него одна. Два прогона разом
|
# Один запуск за раз: параллельные чтения разделили бы порцию
|
||||||
# дрались бы за неё, а слепок разъехался бы по двум _load_id; второй
|
# между разными _load_id.
|
||||||
# прогон подождёт своей очереди.
|
|
||||||
max_active_runs=1,
|
max_active_runs=1,
|
||||||
template_searchpath=SQL_ROOT,
|
template_searchpath=SQL_ROOT,
|
||||||
tags=["заказы"],
|
tags=["заказы"],
|
||||||
)
|
)
|
||||||
def orders_ingest():
|
def orders_ingest():
|
||||||
"""Забрать приехавший слепок заказов и разложить его по слоям."""
|
"""Сохранить порцию заказов и разобрать ее в ODS."""
|
||||||
pull_batch = SQLExecuteQueryOperator(
|
pull_batch = SQLExecuteQueryOperator(
|
||||||
task_id="pull_batch",
|
task_id="pull_batch",
|
||||||
conn_id="clickhouse_default",
|
conn_id="clickhouse_default",
|
||||||
@@ -59,8 +52,8 @@ def orders_ingest():
|
|||||||
do_xcom_push=False,
|
do_xcom_push=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ClickHouse не даёт транзакции между двумя вставками parse_batch. Один
|
# Две вставки parse_batch не образуют транзакцию. Повтор задачи
|
||||||
# task_id сохраняет их порядок и одну точку повтора для неизменного load_id.
|
# выполняет обе над тем же срезом STG по _load_id.
|
||||||
pull_batch >> parse_batch
|
pull_batch >> parse_batch
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+29
-27
@@ -146,25 +146,25 @@ def _ingest_orders() -> TriggerDagRunOperator:
|
|||||||
type="integer",
|
type="integer",
|
||||||
minimum=1,
|
minimum=1,
|
||||||
maximum=7,
|
maximum=7,
|
||||||
title="Сколько дней прожить",
|
title="Число модельных дней для генерации",
|
||||||
# description_md рендерится подсказкой прямо в форме запуска
|
# description_md рендерится подсказкой прямо в форме запуска
|
||||||
# (params.rst, Airflow 3.3), чтобы правило было видно сразу.
|
# (params.rst, Airflow 3.3), чтобы правило было видно сразу.
|
||||||
description_md=(
|
description_md=(
|
||||||
"Форма задает только число дней. Номер первого несыгранного "
|
"Создать данные за следующие 1–7 модельных дней; по умолчанию "
|
||||||
"дня шаговый даг берёт из переменной `world_position` (Admin → "
|
"за один день. События отправляются без пауз. Номер первого дня "
|
||||||
"Variables). Поля Logical date и Run ID нужны самому Airflow "
|
"берется из `world_position` (Admin → Variables). Поля Logical "
|
||||||
"и на выбор дня не влияют."
|
"date и Run ID не меняют выбор дня. После завершения запустите "
|
||||||
|
"`etl_pipeline`, чтобы обновить DDS и витрины DM."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
def world_next_day():
|
def world_next_day():
|
||||||
"""Прожить следующие дни пачкой, без пауз.
|
"""Создать данные за следующие модельные дни без пауз между событиями.
|
||||||
|
|
||||||
Запускается руками. День по умолчанию один, но разгон вперёд идёт одним
|
Параметр `days` задает число дней: от 1 до 7, по умолчанию 1. Первый день
|
||||||
нажимом, а не десятью: сколько дней играть — параметр запуска. Первый
|
берется из `world_position`. После загрузки событий и слепков заказов
|
||||||
день шаговый даг берёт из `world_position`. Логическая дата прогона на выбор
|
запустите `etl_pipeline` вручную, чтобы обновить DDS и витрины DM.
|
||||||
дня не влияет.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@task
|
@task
|
||||||
@@ -174,7 +174,10 @@ def world_next_day():
|
|||||||
position = first_day + days
|
position = first_day + days
|
||||||
Variable.set(WORLD_POSITION, str(position))
|
Variable.set(WORLD_POSITION, str(position))
|
||||||
logging.info(
|
logging.info(
|
||||||
"дни %s–%s сыграны; новая позиция — %s", first_day, position - 1, position
|
"данные за модельные дни %s–%s отправлены; world_position=%s",
|
||||||
|
first_day,
|
||||||
|
position - 1,
|
||||||
|
position,
|
||||||
)
|
)
|
||||||
|
|
||||||
first_day = first_unplayed_day()
|
first_day = first_unplayed_day()
|
||||||
@@ -210,20 +213,23 @@ def world_next_day():
|
|||||||
tags=TAGS,
|
tags=TAGS,
|
||||||
)
|
)
|
||||||
def world_live_day():
|
def world_live_day():
|
||||||
"""Прожить следующий день в темпе модельного времени.
|
"""Постепенно отправить события за следующий модельный день.
|
||||||
|
|
||||||
Ускорение ×60: модельные сутки укладываются примерно в двадцать четыре
|
При ускорении ×60 отправка занимает около 24 реальных минут. Номер дня
|
||||||
реальные минуты, и суточная волна разворачивается на глазах. Запускается
|
берется из `world_position`; Logical date его не меняет. Затем даг
|
||||||
руками; чтобы мир жил так день за днём сам, есть выключатель `world_live`.
|
отправляет слепок заказов и ждет его загрузки и завершения `etl_pipeline`.
|
||||||
Следующий день шаговый даг берёт из `world_position`. Логическая дата прогона
|
Для автоматического запуска следующих дней снимите с паузы `world_live`.
|
||||||
на выбор дня не влияет.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@task
|
@task
|
||||||
def remember_played(first_day: int) -> None:
|
def remember_played(first_day: int) -> None:
|
||||||
"""Позиция ставится по сыгранному дню и только по успеху."""
|
"""Позиция ставится по сыгранному дню и только по успеху."""
|
||||||
Variable.set(WORLD_POSITION, str(first_day + 1))
|
Variable.set(WORLD_POSITION, str(first_day + 1))
|
||||||
logging.info("сыгран день %s; новая позиция — %s", first_day, first_day + 1)
|
logging.info(
|
||||||
|
"данные за модельный день %s отправлены; world_position=%s",
|
||||||
|
first_day,
|
||||||
|
first_day + 1,
|
||||||
|
)
|
||||||
|
|
||||||
first_day = first_unplayed_day()
|
first_day = first_unplayed_day()
|
||||||
played = _generator("play_day", ["live", "--day", PLAYED_DAY])
|
played = _generator("play_day", ["live", "--day", PLAYED_DAY])
|
||||||
@@ -254,16 +260,12 @@ def world_live_day():
|
|||||||
tags=TAGS,
|
tags=TAGS,
|
||||||
)
|
)
|
||||||
def world_live():
|
def world_live():
|
||||||
"""Выключатель: пока включён, мир живёт день за днём.
|
"""Автоматически запускать модельные дни, пока даг снят с паузы.
|
||||||
|
|
||||||
Своей работы у выключателя нет — он дёргает `world_live_day` и ждёт конца.
|
Запускает `world_live_day` и ждет его завершения. Пауза останавливает
|
||||||
Ожидание тут несущая конструкция, а не вежливость: без него тик шёл бы
|
запуск следующих дней; уже запущенный день завершится. Если он завершился
|
||||||
независимо от хода дня, лишние прогоны скопились бы очередью, и мир потом
|
с ошибкой, этот даг тоже завершится с ошибкой: откройте дочерний запуск
|
||||||
промчался бы по ней без всякого темпа.
|
и журнал неуспешной задачи.
|
||||||
|
|
||||||
Ждём триггером, а не сенсором: оператор опрашивает тот прогон, который сам
|
|
||||||
и создал, и ссылка на дочерний прогон видна прямо отсюда. Упал день —
|
|
||||||
краснеет и выключатель.
|
|
||||||
"""
|
"""
|
||||||
TriggerDagRunOperator(
|
TriggerDagRunOperator(
|
||||||
task_id="trigger_live_day",
|
task_id="trigger_live_day",
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
Дата: 9 августа 2026 года. Статус: принято; роль `monitoring_reader`
|
Дата: 9 августа 2026 года. Статус: принято; роль `monitoring_reader`
|
||||||
и пользователь `grafana` добавлены 21 августа 2026 года (#114), права
|
и пользователь `grafana` добавлены 21 августа 2026 года (#114), права
|
||||||
`bi_reader` расширены до ODS, DDS и DM 25 августа 2026 года (#150).
|
`bi_reader` расширены до ODS, DDS и DM 25 августа 2026 года (#150).
|
||||||
|
Пользователь `student` и роль `student_writer` добавлены 22 сентября 2026 года.
|
||||||
|
|
||||||
## Решение
|
## Решение
|
||||||
|
|
||||||
У кластера семь пользователей и шесть ролей.
|
У кластера восемь пользователей и семь ролей.
|
||||||
|
|
||||||
- `default` — с паролем, только служебный: проверки здоровья нод и работа
|
- `default` — с паролем, только служебный: проверки здоровья нод и работа
|
||||||
изнутри контейнеров. Приложения им не ходят.
|
изнутри контейнеров. Приложения им не ходят.
|
||||||
@@ -20,6 +21,10 @@
|
|||||||
шире.
|
шире.
|
||||||
- `analyst` с ролью `analyst_reader` — чтение всех слоёв и справочников. Им
|
- `analyst` с ролью `analyst_reader` — чтение всех слоёв и справочников. Им
|
||||||
человек подключается снаружи, из своего клиента.
|
человек подключается снаружи, из своего клиента.
|
||||||
|
- `student` с ролями `analyst_reader` и `student_writer` — то же чтение и
|
||||||
|
работа со своими таблицами в `default`: создание, вставка и удаление.
|
||||||
|
Сохранение учебных наблюдений не требует прав технической учётной записи
|
||||||
|
`etl` на изменение слоёв. Таблицы локальные; работа ведётся на одной ноде.
|
||||||
- `grafana` с ролью `monitoring_reader` — чтение слоёв, таблиц базы `dic` и
|
- `grafana` с ролью `monitoring_reader` — чтение слоёв, таблиц базы `dic` и
|
||||||
системных таблиц, `CLUSTER` и `REMOTE`. Им ходит Grafana, и больше никто.
|
системных таблиц, `CLUSTER` и `REMOTE`. Им ходит Grafana, и больше никто.
|
||||||
- `dict` с ролью `dict_reader` — чтение одной базы `dic`, пароля нет. Им
|
- `dict` с ролью `dict_reader` — чтение одной базы `dic`, пароля нет. Им
|
||||||
@@ -148,6 +153,11 @@ DDL для матвью приёма выполняется от имени `lif
|
|||||||
|
|
||||||
## Что проверено
|
## Что проверено
|
||||||
|
|
||||||
|
Права `student` сверены 22 сентября 2026 года через MCP Context7 и на стенде
|
||||||
|
26.3.17.56: создание локальных `MergeTree`, `INSERT SELECT` из DDS и DM,
|
||||||
|
чтение и удаление своих таблиц работают. На обеих нодах вставка в STG, ODS,
|
||||||
|
DDS, DM и `dic` завершается `ACCESS_DENIED`.
|
||||||
|
|
||||||
Сверено 9 августа 2026 года по документации ClickHouse через MCP Context7 и
|
Сверено 9 августа 2026 года по документации ClickHouse через MCP Context7 и
|
||||||
на живом стенде 26.3.17.56.
|
на живом стенде 26.3.17.56.
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
`first_matched_at` нет. Запланированные витрины его не читают, а два возможных
|
`first_matched_at` нет. Запланированные витрины его не читают, а два возможных
|
||||||
смысла — «первая покупка вообще» и «момент, с которого связь была известна
|
смысла — «первая покупка вообще» и «момент, с которого связь была известна
|
||||||
хранилищу» — требуют разных исторических моделей. Этот вопрос оставлен
|
хранилищу» — требуют разных исторических моделей. Этот вопрос оставлен
|
||||||
[самостоятельной работе](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/145),
|
[самостоятельной работе](../../labs/optional/identity-over-time.md),
|
||||||
а текущий стенд не выдаёт один смысл за другой.
|
а текущий стенд не выдаёт один смысл за другой.
|
||||||
|
|
||||||
Статус заказа связь не меняет: любой заказ, сопоставленный с событием покупки,
|
Статус заказа связь не меняет: любой заказ, сопоставленный с событием покупки,
|
||||||
|
|||||||
+86
-107
@@ -1,134 +1,113 @@
|
|||||||
# Модель заказа в DDS
|
# Модель заказа в DDS
|
||||||
|
|
||||||
Учебный результат: менти проектирует бизнес-сущность поверх готового ODS —
|
DDS готовит заказ для анализа: выбирает текущее состояние из ODS,
|
||||||
выбирает зерно, состав строки и способ наполнения — и видит идемпотентность
|
разбирает позиции и переводит время в часовой пояс счетчика. Этот слой
|
||||||
трансформации, устроенную иначе, чем в приёме: заменой партиции, а не
|
показывает, как собрать модель данных для витрин и повторить загрузку
|
||||||
версиями.
|
без удвоения заказов.
|
||||||
|
|
||||||
## Зерно
|
## Зерно
|
||||||
|
|
||||||
Одна строка — заказ в текущем состоянии, ключ — `order_id`. Модель отвечает
|
Одна строка — один заказ в последнем известном состоянии, ключ — `order_id`.
|
||||||
на вопрос «что сейчас известно о заказе»; историю версий не хранит:
|
История изменений здесь не хранится. ODS тоже не служит ее архивом:
|
||||||
читателя у истории нет ни в одной витрине, а ODS её и не обещает —
|
промежуточные версии удаляются при фоновых слияниях.
|
||||||
промежуточные версии съедают фоновые слияния. «Дыхание» выручки внутри окна
|
|
||||||
K живёт на текущем состоянии само: пересборка дня меняет статусы заказов, и
|
|
||||||
число дня меняется вместе с ними.
|
|
||||||
|
|
||||||
## Состав строки
|
## Состав строки
|
||||||
|
|
||||||
Деловые поля источника переезжают из `ods.order_v` без переименований —
|
Поля заказа приходят из `ods.order_v` с именами источника: `order_id`,
|
||||||
бэкенд уже говорит в snake_case: `order_id`, `user_id`, `status`, деньги в
|
`user_id`, `status`, `items_total`, `discount`, `delivery`, `total`.
|
||||||
`Decimal` (`items_total`, `discount`, `delivery`, `total`).
|
Денежные колонки хранят рубли с копейками в `Decimal(18, 2)`.
|
||||||
|
|
||||||
Времена `created_at` и `updated_at` пересчитываются при загрузке в пояс
|
`created_at` и `updated_at` переводятся из UTC в `Europe/Samara`, пояс
|
||||||
счётчика: DDS обслуживает человека с дашбордом ([дока
|
счетчика сайта. Из `created_at` в этом поясе вычисляется `order_date` —
|
||||||
хранилища](../storage.md), «Часовые пояса»). Там же один раз считается
|
день оформления заказа. Витрины группируют по готовой дате;
|
||||||
**день заказа** — колонка `order_date`, по образцу `EventDate` у события:
|
правило времени общее для [хранилища](../storage.md#часовые-пояса).
|
||||||
суточные срезы группируются по готовой дате и пояса не упоминают.
|
|
||||||
|
|
||||||
Позиции разобраны из сырой строки `items` в три типизированных массива
|
Строка `items` из ODS разбирается в три массива одной длины:
|
||||||
одной длины — артикул, количество, цена (`Decimal`). Это родная для
|
|
||||||
ClickHouse денормализация: позиция не живёт без заказа и собственного ключа
|
| Колонка | Что хранит |
|
||||||
не имеет, а разворачивает массивы читатель — `ARRAY JOIN` в витрине
|
|---|---|
|
||||||
выручки, с обогащением категорией через `dictGet`
|
| `item_sku` | Артикулы товаров |
|
||||||
([мастер-спека](../../specs/2026-07-30-stand-v2-realism.md), раздел 7). Разбор
|
| `item_quantity` | Количество каждого товара, `UInt32` |
|
||||||
JSON происходит один раз, на границе ODS → DDS, куда его и положила спека
|
| `item_price` | Цены за единицу в рублях, `Decimal(18, 2)` |
|
||||||
(раздел 2); записывать его параллельными колонками-массивами или `Nested` —
|
|
||||||
выбор DDL, не модели.
|
Элементы с одинаковым номером описывают одну позицию заказа. Витрина
|
||||||
|
выручки разворачивает их через `ARRAY JOIN` и получает категорию товара
|
||||||
|
из справочника через `dictGet`. Разбор JSON выполняется один раз
|
||||||
|
при загрузке DDS.
|
||||||
|
|
||||||
|
ODS проверяет только то, что `items` — массив. Разбор в DDS не заменяет
|
||||||
|
проверку его содержимого: например, некорректное количество может стать
|
||||||
|
нулем. Проверку суммы позиций участник добавляет в
|
||||||
|
[первом задании ступени 5](../../labs/5-1-order-checks.md).
|
||||||
|
|
||||||
## Движок и наполнение
|
## Движок и наполнение
|
||||||
|
|
||||||
`dds.order_rep` — `ReplicatedMergeTree` без колонки версии: партиция —
|
`dds.order_rep` — таблица на `ReplicatedMergeTree`, без колонки версии.
|
||||||
`order_date` (день заказа неизменен), ключ сортировки — `order_id`.
|
Партиция хранит один `order_date`, ключ сортировки — `order_id`.
|
||||||
`dds.order_dist` шардирует по `cityHash64(order_id)`, тем же выражением, что
|
Запись через `dds.order_dist` распределяет заказы по `cityHash64(order_id)`,
|
||||||
ODS: это единственный устойчивый ключ сущности.
|
как в ODS. Для чтения служит `dds.order_v` с явным списком колонок
|
||||||
|
над `dds.order_dist`; `FINAL` в этом слое не нужен.
|
||||||
|
|
||||||
Идемпотентность держит замена партиции, а не версии. Сбор дня — батч после
|
Даг `dds_transform` пересобирает выбранные дни из `ods.order_v`:
|
||||||
прихода слепка: трансформация собирает партицию заново из `ods.order_v` и
|
|
||||||
атомарно подменяет. Поэтому каждая строка физической таблицы — ровно один
|
|
||||||
заказ, а чтению не нужен `FINAL`. Пока окно K живо, день «дышит» — статусы
|
|
||||||
заказов меняются, и партиция дня пересобирается очередным запуском; на выходе
|
|
||||||
из окна день замерзает.
|
|
||||||
|
|
||||||
Публичная поверхность — `dds.order_v`: обычное представление с явным списком
|
1. [`order_scope.sql`](../../../sql/dds/order_scope.sql) определяет
|
||||||
деловых и собственных служебных колонок над `dds.order_dist`, без `FINAL`.
|
непрерывный отрезок `first_day`…`last_day`. Он включает дни заказов
|
||||||
Роли объектов и доступ следуют общей [конвенции](../storage.md), «Имена
|
из последнего слепка и начинается раньше, если в DDS отсутствует
|
||||||
объектов».
|
день, который есть в ODS.
|
||||||
|
2. [`order_rebuild.sql`](../../../sql/dds/order_rebuild.sql) очищает
|
||||||
|
промежуточную таблицу `dds.order_stage_rep` и наполняет ее через
|
||||||
|
`dds.order_stage_dist`. Это донор: в нем готовится результат,
|
||||||
|
пока основная таблица остается доступной для чтения.
|
||||||
|
3. [`order_replace.sql`](../../../sql/dds/order_replace.sql) заменяет
|
||||||
|
каждую дневную партицию основной таблицы готовой партицией донора.
|
||||||
|
|
||||||
Механика запуска — где собирается партиция и как подменяется на кластере —
|
Повтор при неизменном ODS дает те же деловые данные без дублей —
|
||||||
сюда не входит: это устройство [дневного конвейера](../etl.md).
|
это идемпотентность загрузки. Пока заказы дня входят в семидневное окно
|
||||||
|
источника, их состояние может меняться, и день пересобирается.
|
||||||
|
За пределами окна новые состояния не приходят, но пропущенный день
|
||||||
|
можно восстановить из ODS. Границы пересборки берутся из данных;
|
||||||
|
число семь в SQL хранилища не повторяется.
|
||||||
|
|
||||||
|
Замена атомарна внутри одного шарда: читатель видит прежнюю или новую
|
||||||
|
партицию целиком. Замены на двух шардах независимы, поэтому между ними
|
||||||
|
запрос через Distributed может увидеть смешанный результат.
|
||||||
|
Донор остается до следующей сборки и доступен для диагностики.
|
||||||
|
Порядок задач и повторы — в [описании дневного конвейера](../etl.md).
|
||||||
|
|
||||||
## Служебные колонки
|
## Служебные колонки
|
||||||
|
|
||||||
Строка получает `_load_id` и `_load_ts` запуска трансформации, собравшего
|
`_load_id` и `_load_ts` показывают, какой запуск DDS собрал строку
|
||||||
её партицию, — по общей конвенции хранилища. При замене партиции они
|
и когда он выполнил вставку. Они одинаковы у строк одной сборки.
|
||||||
одинаковы у всех строк дня и отвечают на вопрос «каким запуском и когда
|
При следующей пересборке эти метки обновятся, даже если заказ не изменился.
|
||||||
собран этот день».
|
|
||||||
|
|
||||||
Ярлыки ODS (`snapshot_date`, `_load_id` приёма, `_load_ts` прибытия) в модель
|
`snapshot_date` и метки приема ODS в DDS не переносятся: они описывают
|
||||||
не переезжают. Они описывают наблюдение и доставку строки в ODS. Модель DDS
|
получение сообщения источника. Их можно найти в ODS по `order_id`.
|
||||||
хранит текущее деловое состояние заказа. В ODS `snapshot_date` задаёт версию и
|
В каждом слое служебные колонки относятся к его собственной загрузке.
|
||||||
называет последний слепок, в котором источник показал заказ
|
|
||||||
([ADR 0014](../../adr/0014-order-version-snapshot-date.md)). Дату
|
|
||||||
наблюдения и координаты доставки читатель спрашивает у ODS по
|
|
||||||
`order_id`. Правило слоя:
|
|
||||||
**служебные колонки — координаты собственной загрузки слоя; чужие координаты
|
|
||||||
спрашиваются у их слоя по ключу сущности**.
|
|
||||||
|
|
||||||
Происшествие качества данных расследуется цепочкой, где каждый шаг спрашивает
|
При поиске ошибки начните с `_load_id` в DDS: он указывает на запуск
|
||||||
свой слой на его языке: по `_load_id` строки DDS видно, какой запуск собрал
|
сборки. По `order_id` найдите версии заказа в ODS, а по их `_load_id` —
|
||||||
день; по `order_id` ODS показывает физические версии и брак — там свой
|
соответствующие сырые сообщения в STG и ошибки того же приема.
|
||||||
`_load_id` приёма связывает годные строки, ошибки и данные STG; дальше читают
|
В таблице ошибок `order_id` может быть не разобран, поэтому нужное
|
||||||
STG, пока данные живы. Окно расследования задано сроками хранения: сырые данные
|
сообщение ищут по `raw` и координатам Kafka.
|
||||||
— трое суток, брак — месяц ([дока хранилища](../storage.md)).
|
Сырые сообщения хранятся около трех реальных суток, ошибки — месяц;
|
||||||
|
уточнения к срокам — в [справочнике хранилища](../storage.md#срок-жизни-сырых-данных).
|
||||||
|
|
||||||
## Читатели
|
## Читатели
|
||||||
|
|
||||||
- Витрина выручки `dm.revenue_daily_v`: заказы в `paid`, `ARRAY JOIN`
|
- `dm.revenue_daily_v` считает выручку по заказам со статусом `paid`,
|
||||||
позиций, категория через `dictGet` (спека, раздел 7).
|
разворачивает позиции и дополняет их категорией товара.
|
||||||
- Сверка `dm.purchase_vs_orders_v`: `order_id`, `order_date`, `items_total`,
|
- `dm.purchase_vs_orders_v` сопоставляет заказ с покупкой на сайте
|
||||||
`status`; соединение — GLOBAL JOIN с обеими сторонами-подзапросами
|
по `order_id`, используя `order_date`, `items_total` и `status`.
|
||||||
([исследование соединений](../../research/2026-08-22-distributed-join.md)).
|
- `dds.identity_map` связывает идентификаторы посетителей с `user_id`
|
||||||
- Сборка карты идентичностей: пара `order_id` ↔ `user_id`, источник —
|
через покупки и заказы.
|
||||||
`dds.order_v`.
|
|
||||||
|
|
||||||
## Не входит
|
|
||||||
|
|
||||||
- Механика запуска трансформации и замены партиций на кластере —
|
|
||||||
[дневной конвейер](../etl.md).
|
|
||||||
- Контур проверок качества (даг DQ, `dm.dq_summary`) — отдельный разговор
|
|
||||||
этапа 4.
|
|
||||||
- Инкрементальное наполнение вместо пересборки — техдолг #2.
|
|
||||||
|
|
||||||
## Отклонённые варианты
|
|
||||||
|
|
||||||
- **Одно динамическое представление вместо физической таблицы.** Рамка стенда —
|
|
||||||
показать модель данных, которую хранилище готовит потребителям; урок
|
|
||||||
«слой — контракт, а не копия» уже закреплён за `dds.event_v`, а разбор
|
|
||||||
позиций и пересчёт времён при каждом чтении собирали бы модель заново на
|
|
||||||
лету. Публичное `dds.order_v` поверх готовой физической модели этому доводу
|
|
||||||
не противоречит: оно ничего заново не вычисляет.
|
|
||||||
- **`ReplacingMergeTree` ещё раз.** Повторил бы механику ODS вторым разом:
|
|
||||||
физически хранил бы устаревшие состояния, вернул бы `FINAL`
|
|
||||||
и обёртку-представление — а урок версий менти уже отыграл.
|
|
||||||
- **Отдельная сущность `dds.order_item`.** Вторая таблица, второй вопрос
|
|
||||||
шардирования и `JOIN` там, где хватает `ARRAY JOIN`; нормализация из
|
|
||||||
реляционного мира, а не модель под колоночное хранилище. `JOIN` менти
|
|
||||||
увидит в сверке, работу с массивами — только здесь.
|
|
||||||
- **Зерно «версия заказа» (история).** Читателя нет; ODS историю не
|
|
||||||
обещает, а мир при нужде переигрывается генератором.
|
|
||||||
- **Перенос ярлыков ODS** — см. «Служебные колонки».
|
|
||||||
- **Без модели вовсе: витрины читают `ods.order_v` напрямую.** Разбор JSON
|
|
||||||
и пересчёт времён расползлись бы по витринам, а модель данных — то, ради
|
|
||||||
чего сущность и строится, — не была бы показана.
|
|
||||||
|
|
||||||
## Связанные решения
|
## Связанные решения
|
||||||
|
|
||||||
- [ADR 0010](../../adr/0010-order-versions-in-ods.md) — версии заказов в
|
- [ADR 0010](../../adr/0010-order-versions-in-ods.md) — версии заказов в ODS
|
||||||
ODS, устойчивая поверхность `ods.order_v`.
|
и чтение через `ods.order_v`.
|
||||||
- [Приём заказов](../orders/ingestion.md) — граница строгого приёма и
|
- [Прием заказов](../orders/ingestion.md) — проверки формата и выбор
|
||||||
четыре координаты строки ODS.
|
актуальной версии.
|
||||||
- [Проталкивание условий сквозь
|
- [Исследование соединений](../../research/2026-08-22-distributed-join.md) —
|
||||||
представления](../../research/2026-08-22-view-predicate-pushdown.md) —
|
почему сверка распределенных источников использует `GLOBAL JOIN`.
|
||||||
резолюция #128 той же карты: цена `FINAL` и цепочек представлений, фон, на
|
- [Проталкивание условий сквозь представления](../../research/2026-08-22-view-predicate-pushdown.md) —
|
||||||
котором здесь выбрана физическая таблица.
|
как фильтр запроса влияет на объем чтения ODS.
|
||||||
@@ -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, устройство и город.
|
||||||
|
|
||||||
## Читатели
|
## Читатели
|
||||||
|
|
||||||
|
|||||||
+172
-142
@@ -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-проверки открывайте после собственного решения: они содержат
|
||||||
|
расчет существенной части задания.
|
||||||
|
|
||||||
## Отклонённые варианты
|
## Отклонённые варианты
|
||||||
|
|
||||||
@@ -254,7 +281,10 @@ business_key`.
|
|||||||
- **Инкрементальный пересчёт затронутых дней по `first_matched_at`.**
|
- **Инкрементальный пересчёт затронутых дней по `first_matched_at`.**
|
||||||
Производственный ход, но на объёмах стенда его экономия неизмерима, а
|
Производственный ход, но на объёмах стенда его экономия неизмерима, а
|
||||||
след новых связок требует вернуть колонку, выкинутую в #133. Стал лабой
|
след новых связок требует вернуть колонку, выкинутую в #133. Стал лабой
|
||||||
повышенной сложности — issue #149.
|
повышенной сложности —
|
||||||
|
[«Как обновлять витрину без полной пересборки»](../labs/optional/incremental-daily-traffic.md).
|
||||||
|
В ней новые связи находятся сравнением наблюдений: время покупки
|
||||||
|
не заменяет момент обнаружения связи.
|
||||||
- **Superset поверх DDS без слоя DM.** Терялся бы урок «витрина — контракт
|
- **Superset поверх DDS без слоя DM.** Терялся бы урок «витрина — контракт
|
||||||
под потребителя», против раздела 7 спеки.
|
под потребителя», против раздела 7 спеки.
|
||||||
|
|
||||||
|
|||||||
+107
-82
@@ -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`. Старые версии
|
||||||
|
могут физически оставаться до фонового слияния; повторяемость относится
|
||||||
|
к результату чтения, а не к числу строк на диске.
|
||||||
|
|
||||||
## Чего конвейер о себе не знает
|
## Чего конвейер о себе не знает
|
||||||
|
|
||||||
|
|||||||
@@ -1,241 +1,172 @@
|
|||||||
# Приём заказов из Kafka в ODS
|
# Прием заказов из Kafka в ODS
|
||||||
|
|
||||||
Учебный результат: менти различает версию бизнес-сущности, наблюдение источника
|
Здесь разобран путь одного заказа: сообщение Kafka сохраняется в STG,
|
||||||
и запуск загрузки, а затем читает физические версии через явную поверхность
|
проверяется и попадает либо в ODS, либо в таблицу ошибок. Следующие слои
|
||||||
текущего состояния.
|
читают из ODS последнюю принятую версию каждого заказа.
|
||||||
|
|
||||||
## Проблема
|
|
||||||
|
|
||||||
Заказы приезжают полным слепком окна изменяемости, но Kafka передаёт его
|
|
||||||
отдельными сообщениями и не сообщает потребителю, где слепок закончился.
|
|
||||||
Прямое чтение Kafka Engine возвращает одну порцию без признака полноты.
|
|
||||||
Поэтому `snapshot_date` не может служить границей публикации: `REPLACE
|
|
||||||
PARTITION` могла бы заменить партицию дня неполным набором строк.
|
|
||||||
|
|
||||||
Одновременно типизированный ODS не должен принимать правдоподобные значения по
|
|
||||||
умолчанию из грязного JSON или останавливать весь пакет из-за одной строки.
|
|
||||||
|
|
||||||
## Цели
|
|
||||||
|
|
||||||
- сохранить пакетный забор как контраст потоковому приёму событий;
|
|
||||||
- один раз принять байты в STG и независимо разложить строки на годные и брак;
|
|
||||||
- хранить в ODS типизированные версии заказов, не публикуя слепок заменой
|
|
||||||
партиции;
|
|
||||||
- дать следующим слоям один корректный способ прочитать текущее состояние;
|
|
||||||
- оставить код проверки коротким и ограничить его контрактом провода.
|
|
||||||
|
|
||||||
## Не входит
|
|
||||||
|
|
||||||
- модель заказа в DDS: её зерно, связи, материализация и способ наполнения;
|
|
||||||
- проверка полей внутри `items`, переходов статуса и равенств денежных сумм;
|
|
||||||
- удаление заказа по отсутствию в следующем слепке;
|
|
||||||
- маркер конца слепка, опись ожидаемых строк и транзакция между целями ODS.
|
|
||||||
|
|
||||||
## Поток данных
|
## Поток данных
|
||||||
|
|
||||||
После завершения генератора Airflow один раз читает байтовый Kafka-чтец и
|
Источник отправляет в топик `orders` дневной слепок: по одному сообщению
|
||||||
записывает полученную порцию в `stg.orders_raw`. Даг зовётся `orders_ingest`, а
|
на заказ. Слепок дня D содержит состояния заказов, оформленных в дни
|
||||||
запускает его шаговый даг модельного времени, который положил слепок в топик и
|
D−6…D, с учетом [задержек первого появления](fate.md).
|
||||||
ждёт конца прогона. Все строки получают `_load_id`, равный `run_id` Airflow.
|
В начале модельного времени окно короче. Выгрузку за D отправляет
|
||||||
`_load_ts` вычисляется при этой записи и дальше переносится без пересчёта.
|
генератор при запуске дня D+1; подробности — в [описании слепка](snapshot.md).
|
||||||
Запрос забора лежит в
|
|
||||||
[`sql/stg/orders_raw_load.sql`](../../../sql/stg/orders_raw_load.sql):
|
|
||||||
`SQLExecuteQueryOperator` задаёт подключение и передаёт `run_id` как параметр
|
|
||||||
ClickHouse `load_id`, а преобразование остаётся в SQL своего слоя.
|
|
||||||
|
|
||||||
Один следующий `task_id` отвечает за весь переход STG → ODS. Внутри него два
|
После отправки шаговый даг модельного времени запускает
|
||||||
последовательных `INSERT SELECT` читают неизменный срез по `_load_id`: первый
|
[`orders_ingest`](../../../dags/orders_ingest.py) и ждет его завершения.
|
||||||
пишет годные строки в `ods.order`, второй — брак в `ods.order_errors`.
|
У `orders_ingest` нет расписания; его два шага выполняются по порядку:
|
||||||
Транзакции между запросами нет. При частичном сбое Airflow повторяет весь
|
|
||||||
`task_id`; одинаковые исходные строки и служебные метки не вычисляются заново.
|
|
||||||
Обе вставки лежат в одном самодостаточном файле
|
|
||||||
[`order_parse.sql`](../../../sql/ods/order_parse.sql). Airflow сначала
|
|
||||||
шаблонизирует файл, затем делит получившийся SQL на выражения и исполняет их по
|
|
||||||
порядку в одной задаче `parse_batch`.
|
|
||||||
|
|
||||||
Условия запросов взаимодополняющие: один общий предикат определяет годную
|
1. `pull_batch` читает порцию из `stg.orders_raw_kafka` и записывает ее
|
||||||
строку, а ветвь брака использует его буквальное отрицание. Все функции предиката
|
через `stg.orders_raw_dist`. Содержимое сообщения остается строкой `raw`,
|
||||||
возвращают результат без исключения, а сам предикат всегда заканчивается в
|
рядом сохраняются координаты Kafka, `_load_id` и `_load_ts`.
|
||||||
`true` или `false`, не в `NULL`. Постоянный классификатор между STG и ODS для
|
2. `parse_batch` читает сохраненные строки с этим `_load_id`. Первый
|
||||||
этого не нужен. Предикат объявлен локальным макросом Jinja внутри
|
`INSERT SELECT` пишет годные записи в `ods.order_dist`, второй —
|
||||||
`order_parse.sql` и разворачивается в оба выражения. Макрос убирает
|
остальные в `ods.order_errors_dist`.
|
||||||
дублирование, но не вводит отдельный фрагмент, который нельзя исполнить
|
|
||||||
самостоятельно.
|
`_load_id` равен `run_id` Airflow и связывает строки одного запуска.
|
||||||
|
`_load_ts` — реальное время записи в STG; при разборе оно переносится
|
||||||
|
без пересчета. Запросы лежат в
|
||||||
|
[`orders_raw_load.sql`](../../../sql/stg/orders_raw_load.sql) и
|
||||||
|
[`order_parse.sql`](../../../sql/ods/order_parse.sql).
|
||||||
|
`SQLExecuteQueryOperator` передает `run_id` параметром ClickHouse `load_id`.
|
||||||
|
|
||||||
|
Две вставки разбора используют одно условие годности, записанное макросом
|
||||||
|
Jinja внутри `order_parse.sql`. Перед выполнением макрос подставляется
|
||||||
|
в оба запроса; ветвь брака берет отрицание этого условия. Оно возвращает
|
||||||
|
только истину или ложь и не вызывает исключений на некорректном `raw`.
|
||||||
|
Так каждая строка сохраненной порции получает ровно один результат.
|
||||||
|
|
||||||
|
Транзакции между вставками нет: первая может завершиться, а вторая упасть.
|
||||||
|
Повтор `parse_batch` выполняет обе вставки над тем же срезом STG.
|
||||||
|
Текущее состояние заказа останется прежним, но таблица ошибок может
|
||||||
|
получить повторные строки с теми же `_load_id` и координатами Kafka.
|
||||||
|
Обе вставки ждут доставки строк на шарды и используют ключ целевой таблицы;
|
||||||
|
настройки объяснены в [справочнике хранилища](../storage.md#раскладка-по-шардам).
|
||||||
|
|
||||||
## Граница строгого приёма
|
## Граница строгого приёма
|
||||||
|
|
||||||
Единица решения — одна строка `stg.orders_raw`. Корень должен быть
|
Проверяется одна строка `stg.orders_raw_dist`. Корень JSON должен быть
|
||||||
JSON-объектом с точным набором ключей: `order_id`, `user_id`, `status`,
|
объектом с точным набором из одиннадцати ключей:
|
||||||
`created_at`, `updated_at`, `items_total`, `discount`, `delivery`, `total`,
|
`order_id`, `user_id`, `status`, `created_at`, `updated_at`,
|
||||||
`items`, `snapshot_date`.
|
`items_total`, `discount`, `delivery`, `total`, `items`, `snapshot_date`.
|
||||||
|
|
||||||
Скалярные поля проверяются по типу JSON. Деньги дополнительно обязаны быть
|
| Поля | Условия приема |
|
||||||
строками неотрицательной суммы с ровно двумя знаками после точки — минус в эту
|
|---|---|
|
||||||
форму не входит; времена — строками RFC 3339 в UTC с обязательными
|
| `order_id`, `status` | Строки JSON |
|
||||||
миллисекундами; дата слепка — строкой `YYYY-MM-DD`. `items` проверяется только
|
| `user_id` | Целое число JSON, которое разбирается в `UInt64` |
|
||||||
как JSON-массив. Каноническая форма и основания выбора зафиксированы в
|
| `created_at`, `updated_at` | Строки UTC с миллисекундами, например `2026-06-01T12:34:56.000Z`; проверяются форма и разбор времени |
|
||||||
[исследовании формата](../../research/2026-08-16-order-snapshot-wire-format.md).
|
| `items_total`, `discount`, `delivery`, `total` | Неотрицательные суммы в рублях, записанные строками с двумя знаками после точки; проверяется разбор в `Decimal(18, 2)` |
|
||||||
|
| `snapshot_date` | Строка даты `YYYY-MM-DD`, которая проходит разбор |
|
||||||
|
| `items` | Массив JSON; сохраняется целиком в строковой колонке |
|
||||||
|
|
||||||
Проверять все верхнеуровневые поля здесь уместно: их одиннадцать, и десять
|
ODS проверяет формат верхнего уровня. Здесь нет проверки допустимых
|
||||||
скалярных значений непосредственно образуют типизированную строку заказа. У
|
статусов, их переходов, полей внутри `items` или равенства денежных сумм.
|
||||||
события из 47 полей проверяются только пять опорных; переносить то сокращение на
|
Позиции разбираются в [DDS](../dds/order.md); проверку их суммы участник
|
||||||
малый контракт заказа нет причины. Граница строгости заканчивается на форме
|
добавляет в [задании ступени 5](../../labs/5-1-order-checks.md).
|
||||||
провода: содержимое позиций и бизнес-инварианты намеренно остаются ниже.
|
Основания выбора формата — в [исследовании](../../research/2026-08-16-order-snapshot-wire-format.md).
|
||||||
|
|
||||||
Контракт источника задаёт частоту: **одной дате соответствует один слепок**. Это
|
Класс ошибки записывается в `error_class` по первому нарушенному условию:
|
||||||
свойство всего слепка, поэтому предикат одной строки его не проверяет. Вторая
|
|
||||||
выгрузка того же дня даст ничью по `snapshot_date`, и выбор строки потеряет
|
|
||||||
однозначность. Модельный источник выполняет условие; в настоящей интеграции
|
|
||||||
его фиксируют в договоре с источником
|
|
||||||
([ADR 0014](../../adr/0014-order-version-snapshot-date.md)).
|
|
||||||
|
|
||||||
Класс брака выбирается первым совпадением:
|
1. `not_an_object` — корень JSON не объект;
|
||||||
|
2. `keyset_mismatch` — набор ключей отличается от ожидаемого;
|
||||||
|
3. `field_invalid` — тип, форма или разбор значения не прошли проверку.
|
||||||
|
|
||||||
1. `not_an_object`;
|
Имя поля в класс не входит. Для разбора конкретной ошибки в
|
||||||
2. `keyset_mismatch`;
|
`ods.order_errors_dist` есть исходный `raw`, координаты Kafka и метки загрузки.
|
||||||
3. `field_invalid`.
|
|
||||||
|
|
||||||
Имя отдельного поля в класс не включается. В таблице ошибок остаются сырой
|
|
||||||
текст, метаданные доставки и `_load_id`, поэтому единичный случай можно разобрать
|
|
||||||
без постоянной детализации предиката.
|
|
||||||
|
|
||||||
## Роль ODS
|
## Роль ODS
|
||||||
|
|
||||||
`ods.order_rep` хранит физически принятые версии в
|
`ods.order_rep` использует `ReplicatedReplacingMergeTree(snapshot_date)`.
|
||||||
`ReplacingMergeTree(snapshot_date)`. Ключ сортировки — `order_id`, партиция —
|
Строки с одним ключом сортировки `order_id` считаются версиями одного заказа;
|
||||||
день неизменного `created_at`. `ods.order_dist` шардирует по
|
остается строка с максимальной `snapshot_date`. Партиция —
|
||||||
`cityHash64(order_id)`: только так все версии заказа попадают на один шард и
|
`toDate(created_at)` в UTC, ключ шардирования — `cityHash64(order_id)`.
|
||||||
`FINAL` даёт корректный результат через распределённую таблицу.
|
Неизменный `created_at` и общий ключ направляют все версии заказа
|
||||||
|
в одну партицию и на один шард.
|
||||||
|
|
||||||
Четыре координаты отвечают на разные вопросы:
|
| Колонка | На какой вопрос отвечает |
|
||||||
|
|---|---|
|
||||||
|
| `snapshot_date` | На конец какого модельного дня источник показал это состояние? |
|
||||||
|
| `updated_at` | Когда запись заказа последний раз меняли в магазине? |
|
||||||
|
| `_load_id` | Какой запуск Airflow принял сообщение? |
|
||||||
|
| `_load_ts` | Когда сообщение записали в STG по реальным часам стенда? |
|
||||||
|
|
||||||
- `snapshot_date` — в слепке какого модельного дня источник показал строку;
|
Например, заказ не менялся с 1 июня, но входит в слепки за 1 и 2 июня.
|
||||||
- `updated_at` — когда строку последний раз меняли в источнике;
|
`updated_at` у строк одинаков, а `snapshot_date` различается. ODS оставит
|
||||||
- `_load_id` — какой запуск Airflow принял строку;
|
строку за 2 июня вместе с метками ее доставки. Новая дата наблюдения
|
||||||
- `_load_ts` — когда строка приехала в хранилище.
|
нужна DDS для определения окна пересборки, даже если состояние не изменилось.
|
||||||
|
|
||||||
Версию выбирает `snapshot_date`: выживает строка с максимальной датой слепка
|
Источник выпускает **один слепок на дату**. Повторная отправка дает то же
|
||||||
([ADR 0014](../../adr/0014-order-version-snapshot-date.md)). Поэтому
|
содержимое, а более поздний слепок не возвращает устаревшее состояние.
|
||||||
`snapshot_date` показывает последнее наблюдение заказа. По строкам последней
|
Эти условия не проверяются на одной строке при приеме. Если источник
|
||||||
даты слепка DDS определяет окно пересборки. `_load_id` и `_load_ts` принадлежат
|
пришлет два разных состояния с одной `snapshot_date`, эта колонка
|
||||||
той физической строке, которую сохранило слияние, и описывают её доставку.
|
не позволит определить, какое из них актуально.
|
||||||
|
Обоснование версии — [ADR 0014](../../adr/0014-order-version-snapshot-date.md).
|
||||||
|
|
||||||
Схема версий стоит на дисциплине источника: слепок с поздней датой не
|
`ods.order_v` читает `ods.order_dist FINAL` и возвращает одну текущую строку
|
||||||
показывает состояния старше. Модельный мир её держит — состояние читается на
|
на `order_id`, даже если фоновые слияния еще не убрали старые версии.
|
||||||
границе суток, и слепок дня выходит чистой функцией зерна и дня; источник,
|
Без `FINAL` физическая таблица может показать несколько строк заказа.
|
||||||
собирающий слепок из отставшей реплики, её ломает. Деловая метка `updated_at`
|
Историю ODS не гарантирует: старые версии постепенно удаляются при слияниях.
|
||||||
при этом монотонна по слепкам, поэтому выбор по дате слепка даёт тот же ответ,
|
Отсутствие заказа в новом слепке не удаляет его из ODS.
|
||||||
что выбор по метке (замер — «Что проверено»). Тихая правка строки — ручным
|
|
||||||
`UPDATE` мимо приложения, миграцией — метку не двигает, но следующий слепок
|
|
||||||
перекроет её сам, пока заказ в окне изменяемости. За окном источник заказ
|
|
||||||
больше не показывает, и там помогает только сверка самих полей, в терминах
|
|
||||||
dbt snapshots — стратегия `check` вместо `timestamp`.
|
|
||||||
|
|
||||||
Обычное чтение `_dist` показывает физически сохранившиеся версии и нужно для
|
DDS читает `ods.order_v`, переводит времена в пояс счетчика и разбирает
|
||||||
диагностики. Их число зависит от фоновых слияний: ODS не служит архивом истории.
|
позиции. Для своей загрузки DDS записывает собственные служебные метки —
|
||||||
`ods.order_v` сохраняет те же источник-ориентированные поля без обогащения
|
[модель заказа](../dds/order.md#служебные-колонки).
|
||||||
данными модели, но возвращает одну актуальную версию на `order_id`. Сначала оно
|
|
||||||
может быть простым представлением над `_dist FINAL`; способ выбора можно
|
|
||||||
заменить, не меняя потребителей.
|
|
||||||
|
|
||||||
Это представление остаётся ответственностью ODS: оно скрывает механику чтения
|
|
||||||
версий, но не строит бизнес-модель. DDS читает `ods.order_v` и отдельно решает,
|
|
||||||
какие сущности, связи и производные признаки ему нужны. Судьба служебных
|
|
||||||
колонок выше ODS решена вместе с моделью DDS: чужие ярлыки туда не переезжают
|
|
||||||
([модель заказа](../dds/order.md)).
|
|
||||||
|
|
||||||
## Одно чтение Kafka
|
## Одно чтение Kafka
|
||||||
|
|
||||||
Стандартный слепок содержит около полутора тысяч строк, тогда как предел одной
|
Один запуск делает один прямой `SELECT`, без чтения в цикле до пустоты.
|
||||||
порции на стенде — десятки тысяч сообщений. Поэтому один запуск Airflow делает
|
Обычный слепок содержит около полутора тысяч строк, а предел порции
|
||||||
один прямой `SELECT`, без цикла до пустоты и без фиксации конечных офсетов.
|
на стенде — десятки тысяч сообщений. Это допущение о размере данных:
|
||||||
|
Kafka не сообщает запросу, что прочитан весь слепок.
|
||||||
|
|
||||||
Это допущение о размере стенда, а не доказательство полноты слепка. Если Kafka
|
Короткая порция оставляет непрочитанные сообщения для следующих запусков.
|
||||||
вернёт короткую порцию, непрочитанный хвост останется в топике и приедет в один
|
Поэтому ODS добавляет версии заказов по мере поступления. Замена партиции
|
||||||
из следующих запусков. После отказа от замены партиции это задержка, а не потеря
|
по `snapshot_date` могла бы опубликовать неполный набор заказов
|
||||||
или публикация неполного дня.
|
([ADR 0010](../../adr/0010-order-versions-in-ods.md)).
|
||||||
|
|
||||||
Отказ — случай другой, и «хвост дождётся» на него не распространяется. Офсеты
|
Сбой после чтения опаснее короткой порции. Чтец подтверждает позиции
|
||||||
порции коммитятся в момент чтения, поэтому упавшая вставка уносит прочитанное с
|
сообщений в Kafka — офсеты — при прямом чтении. Если последующая вставка
|
||||||
собой: повторное чтение вернёт ноль, а часть строк может уже лежать на шарде.
|
в STG упадет, эти сообщения уже не будут прочитаны той же группой повторно;
|
||||||
Позиция мира при этом не двигается, и тот же день уедет заново — в STG он
|
часть из них может успеть записаться на шарды. Позиция модельного времени
|
||||||
окажется частичным дублем, а старый хвост, ушедший той же порцией, не вернётся
|
при неуспехе не продвигается. Повтор шагового дага снова отправит слепок
|
||||||
([ADR 0008](../../adr/0008-order-ingestion.md), «Следствия»).
|
того же дня, но не восстановит старые сообщения других слепков,
|
||||||
|
если они попали в неудачную порцию.
|
||||||
## Отклонённые варианты
|
Граница восстановления описана в [ADR 0008](../../adr/0008-order-ingestion.md#следствия).
|
||||||
|
|
||||||
- Партиционная идемпотентность — `REPLACE PARTITION snapshot_date` или
|
|
||||||
`ReplacingMergeTree` по `(snapshot_date, order_id)`: у потребителя нет
|
|
||||||
признака полноты партиции, а дата наблюдения становится частью ключа
|
|
||||||
сущности.
|
|
||||||
- Обычный `MergeTree` в ODS с дедупликацией только в DDS: навсегда сохраняет
|
|
||||||
технические повторы там, где семантика версии уже известна.
|
|
||||||
- Маркер, опись, чтение до пустоты или конечные офсеты: добавляют протокол ради
|
|
||||||
объёма, который с большим запасом помещается в одну порцию.
|
|
||||||
- Два `task_id` или материализованный классификатор: дробят один короткий
|
|
||||||
переход слоя, не добавляя транзакционности.
|
|
||||||
- Представление текущего состояния в DDS и готовая схема `dds.order` в этой
|
|
||||||
задаче: перекладывают механику ODS на следующий слой и преждевременно задают
|
|
||||||
модель данных.
|
|
||||||
|
|
||||||
## Риски и проверка
|
|
||||||
|
|
||||||
- На малой управляемой порции дать по одной строке каждого класса брака и две
|
|
||||||
годные версии одного `order_id`. Две цели должны сохранить все непустые
|
|
||||||
сообщения, а `ods.order_v` — вернуть новую версию независимо от фонового
|
|
||||||
слияния.
|
|
||||||
- Повторить переход с тем же `_load_id`: строка в `ods.order_v` и её `_load_ts`
|
|
||||||
не должны измениться; версии одного заказа должны остаться на одном шарде и
|
|
||||||
в одной партиции. Таблица ошибок может снова записать тот же брак: совпавшие
|
|
||||||
`_load_id` и Kafka-координаты показывают повтор задачи.
|
|
||||||
|
|
||||||
## Что проверено
|
## Что проверено
|
||||||
|
|
||||||
Переход STG → ODS снят на живом стенде 18 августа 2026 года при исполнении #94.
|
Это результаты прежних опытов на стенде; при редактуре они заново
|
||||||
Опыты этого раздела прогнаны и подтвердили обещанное: разбиение строк STG на
|
не выполнялись.
|
||||||
годные строки и брак полное и непересекающееся; две версии одного заказа легли в
|
|
||||||
одну партицию и на один шард, а `ods.order_v` вернуло позднюю независимо от
|
|
||||||
фонового слияния; повтор задачи с тем же `_load_id` строку в `ods.order_v` и её
|
|
||||||
`_load_ts` не изменил, а таблица ошибок записала тот же брак второй раз; строки
|
|
||||||
опытов убраны. Числа, механика опытов и поведение ClickHouse, на которое всё это
|
|
||||||
опирается, — [документ хранилища](../storage.md), «Что проверено».
|
|
||||||
|
|
||||||
После исправления
|
- 18 августа 2026 года (#93, #94) прямое чтение приняло слепок,
|
||||||
[#102](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/102)
|
а разбор разделил строки на годные и ошибки без потерь и пересечений.
|
||||||
честный прогон дня снят на живом стенде 20 августа 2026 года: все 1694 строки
|
Две версии заказа попали на один шард в одну партицию; `ods.order_v`
|
||||||
слепка дня 7 приняты, `ods.order_errors` осталась пустой. У заказа
|
вернул позднюю. Повтор разбора с тем же `_load_id` сохранил текущую строку
|
||||||
`20260608-0007`, который прежде приходил с отрицательным итогом, теперь
|
и ее `_load_ts`, но повторил ошибки. Подробности —
|
||||||
`5490.00 − 823.50 + 299.00 = 4965.50`.
|
[справочник хранилища](../storage.md#что-проверено) и
|
||||||
|
[ADR 0008](../../adr/0008-order-ingestion.md#что-проверено).
|
||||||
|
- 20 августа после исправления
|
||||||
|
[#102](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/102)
|
||||||
|
приняты все 1694 строки слепка дня 7 без ошибок. Итог заказа
|
||||||
|
`20260608-0007`: `5490.00 − 823.50 + 299.00 = 4965.50`.
|
||||||
|
- 21 августа (#124) пересборка через `SQLExecuteQueryOperator` завершила
|
||||||
|
`pull_batch` и `parse_batch`; в `system.query_log` осталось по одному
|
||||||
|
завершенному запросу `etl` на забор, годные строки и ошибки.
|
||||||
|
- 22 августа (#137) во всех 3034 парах с одинаковым `updated_at`
|
||||||
|
деловые поля совпали побайтово; по слепкам время изменения не убывало.
|
||||||
|
- 26 августа (#165) после двух слепков физическая таблица содержала
|
||||||
|
3356 версий 1917 заказов. В `ods.order_v` у всех заказов была дата
|
||||||
|
последнего слепка, а деловые поля совпали с `argMax` по `updated_at`
|
||||||
|
из сырых данных.
|
||||||
|
|
||||||
При исполнении #124 21 августа 2026 года адресная пересборка мира прошла через
|
На ClickHouse `26.3.17.56` отдельно проверены одна порция при прямом
|
||||||
`SQLExecuteQueryOperator`: `pull_batch` и `parse_batch` завершились успешно. В
|
`SELECT` Kafka и выполнение `FINAL` на шардах через Distributed.
|
||||||
`system.query_log` на первой ноде осталось ровно по одному завершённому запросу
|
При проектировании Context7 был недоступен; чтение сверяли с исходниками
|
||||||
пользователя `etl` для забора в STG, записи годных строк и записи брака.
|
той же версии:
|
||||||
|
|
||||||
Дисциплина источника замерена 22 августа 2026 года при исполнении #137.
|
|
||||||
Во всех 3034 парах строк с равным `updated_at` деловые поля совпали
|
|
||||||
побайтово, а по слепкам метка ни разу не убывала.
|
|
||||||
|
|
||||||
Текущее состояние проверено на пересобранном мире 26 августа 2026 года при
|
|
||||||
исполнении #165. После двух слепков физическая таблица содержала 3356 версий
|
|
||||||
1917 заказов. У всех строк `ods.order_v` дата слепка совпала с последним слепком
|
|
||||||
заказа; деловые поля совпали с эталоном `argMax` по `updated_at` из сырых
|
|
||||||
данных. В обеих сверках расхождений нет.
|
|
||||||
|
|
||||||
Забор из Kafka в STG снят на живом стенде 18 августа 2026 года при исполнении
|
|
||||||
#93: одно прямое чтение приносит весь слепок дня, метаданные доставки доступны,
|
|
||||||
офсеты коммитятся, а сбой забора не двигает позицию мира. Числа — [ADR
|
|
||||||
0008](../../adr/0008-order-ingestion.md), раздел «Что проверено».
|
|
||||||
|
|
||||||
MCP Context7 в сессии проектирования был недоступен. На локальном ClickHouse
|
|
||||||
`26.3.17.56` проверено, что прямой `SELECT` Kafka Engine завершается после
|
|
||||||
одной порции, а `FINAL` через `Distributed` исполняется на таблицах шардов.
|
|
||||||
Поэтому версии одного `order_id` направляются на один шард. Фоновое схлопывание
|
|
||||||
`ReplacingMergeTree` и необходимость точного чтения сверены с
|
|
||||||
[официальной документацией](https://clickhouse.com/docs/reference/engines/table-engines/mergetree-family/replacingmergetree),
|
|
||||||
поведение чтения — с исходниками той же версии
|
|
||||||
[`StorageKafka.cpp`](https://github.com/ClickHouse/ClickHouse/blob/v26.3.17.56-lts/src/Storages/Kafka/StorageKafka.cpp) и
|
[`StorageKafka.cpp`](https://github.com/ClickHouse/ClickHouse/blob/v26.3.17.56-lts/src/Storages/Kafka/StorageKafka.cpp) и
|
||||||
[`KafkaSource.cpp`](https://github.com/ClickHouse/ClickHouse/blob/v26.3.17.56-lts/src/Storages/Kafka/KafkaSource.cpp).
|
[`KafkaSource.cpp`](https://github.com/ClickHouse/ClickHouse/blob/v26.3.17.56-lts/src/Storages/Kafka/KafkaSource.cpp).
|
||||||
|
Повторная сверка правил версий с документацией — в
|
||||||
|
[справочнике хранилища](../storage.md#актуальные-события).
|
||||||
|
|
||||||
## Связанные решения
|
## Связанные решения
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
## Пользователи и роли ClickHouse
|
## Пользователи и роли ClickHouse
|
||||||
|
|
||||||
В ClickHouse семь пользователей и шесть ролей:
|
В ClickHouse восемь пользователей и семь ролей:
|
||||||
|
|
||||||
- `etl` — учётная запись рабочих дагов Airflow; роль `etl_writer` читает и
|
- `etl` — учётная запись рабочих дагов Airflow; роль `etl_writer` читает и
|
||||||
пишет слои хранилища;
|
пишет слои хранилища;
|
||||||
@@ -33,6 +33,9 @@
|
|||||||
справочники `dic`, а запросы Superset обращаются к публичным `_v`;
|
справочники `dic`, а запросы Superset обращаются к публичным `_v`;
|
||||||
- `analyst` — учётная запись аналитика с правом чтения всех слоёв и
|
- `analyst` — учётная запись аналитика с правом чтения всех слоёв и
|
||||||
справочников;
|
справочников;
|
||||||
|
- `student` — учебная учётная запись: роль `analyst_reader` дает чтение
|
||||||
|
источников, а `student_writer` — создание, заполнение и удаление своих
|
||||||
|
таблиц в `default`. Записи в слои хранилища у нее нет;
|
||||||
- `grafana` — учётная запись Grafana; роль `monitoring_reader` читает все слои,
|
- `grafana` — учётная запись Grafana; роль `monitoring_reader` читает все слои,
|
||||||
системные таблицы и данные со всех реплик кластера;
|
системные таблицы и данные со всех реплик кластера;
|
||||||
- `dict` — беспарольная учётная запись словаря товаров с правом чтения базы
|
- `dict` — беспарольная учётная запись словаря товаров с правом чтения базы
|
||||||
@@ -49,6 +52,9 @@ Superset получает пароль `bi`, а Grafana — пароль `grafan
|
|||||||
|
|
||||||
## Применение настроек
|
## Применение настроек
|
||||||
|
|
||||||
|
Для существующего стенда сначала добавьте `CLICKHOUSE_STUDENT_PASSWORD`
|
||||||
|
из `.env.example` в свой `.env`.
|
||||||
|
|
||||||
Чтобы применить новые значения ClickHouse из `.env` или изменения файлов в
|
Чтобы применить новые значения ClickHouse из `.env` или изменения файлов в
|
||||||
`infra/clickhouse/config.d/` и `infra/clickhouse/users.d/`, пересоздайте ноды:
|
`infra/clickhouse/config.d/` и `infra/clickhouse/users.d/`, пересоздайте ноды:
|
||||||
|
|
||||||
|
|||||||
+277
-319
@@ -30,223 +30,202 @@ 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)).
|
||||||
|
|
||||||
## Служебные колонки
|
## Служебные колонки
|
||||||
|
|
||||||
Собственные колонки не повторяют имён виртуальных. Виртуальные даёт движок:
|
STG сохраняет исходное сообщение в строке `raw` и добавляет сведения
|
||||||
`_topic`, `_partition`, `_offset`, `_timestamp` у Kafka, `_shard_num` у
|
о доставке. JSON на этом шаге не разбирается: некорректное содержимое
|
||||||
`Distributed` и прочие. Если положить на диск колонку с таким же именем, в
|
тоже попадает в таблицу.
|
||||||
матвью перестанет читаться, что дано движком, а что положено нами, — а это
|
|
||||||
ровно то различие, ради которого метаданные доставки и хранятся. Поэтому они
|
|
||||||
ложатся под именами `kafka_topic`, `kafka_partition`, `kafka_offset`,
|
|
||||||
`kafka_timestamp`, рядом — `consumer_host`, имя читавшей ноды: виртуальные
|
|
||||||
колонки его не несут, а после записи в `Distributed` он уже невосстановим.
|
|
||||||
|
|
||||||
Типы у них такие: `kafka_topic` и `consumer_host` — `LowCardinality(String)`,
|
| Колонка STG | Что хранит |
|
||||||
значений мало и они повторяются; `kafka_partition` и `kafka_offset` — `UInt64`.
|
|---|---|
|
||||||
С `kafka_timestamp` сложнее, и форма его решена на стенде. Меток времени движок
|
| `kafka_topic` | Имя топика из виртуальной колонки Kafka `_topic` |
|
||||||
даёт две: `_timestamp` — `Nullable(DateTime)`, то есть секунды, и
|
| `kafka_partition` | Номер партиции из `_partition` |
|
||||||
`_timestamp_ms` — `Nullable(DateTime64(3))`, миллисекунды. Колонка объявлена
|
| `kafka_offset` | Позицию сообщения внутри партиции из `_offset` |
|
||||||
`Nullable(DateTime64(3, 'UTC'))` и заполняется из `_timestamp_ms`: у брокера
|
| `kafka_timestamp` | Метку времени Kafka из `_timestamp_ms`, если она есть |
|
||||||
метка миллисекундная, соседняя `_load_ts` тоже миллисекундная, а сырой слой
|
| `consumer_host` | Имя ноды, прочитавшей Kafka |
|
||||||
хранит приехавшее, и округлять ему нечего. Обнуляемость нужна отдельно от
|
| `_load_ts` | Реальное время записи сообщения в STG |
|
||||||
разрядности: брокер метку заполняет не всегда, а необнуляемый тип значил бы либо
|
| `_load_id` | Только у заказов: `run_id` запуска Airflow, который принял порцию |
|
||||||
падение приёма на первом сообщении, либо тихие нули за 1970 год.
|
|
||||||
|
|
||||||
Заполняются все они выражением в `SELECT` матвью приёма, а не `DEFAULT` в
|
Топик, партиция и смещение вместе позволяют найти сообщение Kafka.
|
||||||
таблице. Для `consumer_host` это обязательно: `DEFAULT hostName()` вычисляется
|
`kafka_topic` и `consumer_host` имеют тип `LowCardinality(String)`,
|
||||||
на шарде-получателе, то есть назвал бы не ту ноду, которая читала топик, —
|
`kafka_partition` и `kafka_offset` — `UInt64`.
|
||||||
колонка молча отвечала бы на другой вопрос.
|
`kafka_timestamp` имеет тип `Nullable(DateTime64(3, 'UTC'))`: сохраняет
|
||||||
|
миллисекунды и допускает отсутствие метки.
|
||||||
|
|
||||||
Само сообщение лежит в колонке `raw` тем, чем пришло: чтец читает байты и ничего
|
Виртуальные колонки предоставляет движок, а колонки STG хранят значения
|
||||||
не проверяет, поэтому там оказываются и целые сообщения, и мусор. События ниже
|
на диске. Их имена различаются, чтобы не скрыть виртуальную колонку
|
||||||
разбирают матвью ODS ([ADR 0005](../adr/0005-event-ingestion.md)), заказы —
|
собственной. Значения вычисляются в запросе приема. В частности,
|
||||||
пакетный шаг ([ADR 0008](../adr/0008-order-ingestion.md), [ADR
|
`hostName()` должен выполняться на ноде, читающей Kafka:
|
||||||
0010](../adr/0010-order-versions-in-ods.md)).
|
`DEFAULT hostName()` только в локальной таблице назвал бы шард-получатель.
|
||||||
|
|
||||||
Движок таблицы STG — обычный `ReplicatedMergeTree`, `ORDER BY (kafka_partition,
|
Таблицы STG используют `ReplicatedMergeTree` с
|
||||||
kafka_offset)`: разбор полётов идёт от «какое сообщение», другого ключа у
|
`ORDER BY (kafka_partition, kafka_offset)`. Повторная доставка остается
|
||||||
таблицы STG и нет. Замену версий сюда ставить нельзя — она отменила бы свойство
|
отдельной строкой; STG нужен в том числе для разбора таких повторов.
|
||||||
слоя, ради которого он заведён: повтор доставки в STG обязан быть виден.
|
|
||||||
|
|
||||||
Метка времени загрузки зовётся `_load_ts`, тип `DateTime64(3, 'UTC')`. Ставится
|
`_load_ts` имеет тип `DateTime64(3, 'UTC')`. При переходе в ODS метка
|
||||||
она один раз при записи в STG: для событий — матвью приёма, для дневного слепка
|
копируется из STG без пересчета. У событий она также выбирает последнюю
|
||||||
заказов — пакетным шагом. Дальше метка переносится в ODS как есть и отвечает на
|
доставку; у заказов версию задает `snapshot_date`. `_load_id` заказа
|
||||||
вопрос «когда строка приехала в хранилище», а не «когда её разобрали». В
|
переносится в годную строку ODS и в таблицу ошибок.
|
||||||
`ods.event` она же служит колонкой версии `ReplacingMergeTree` и схлопывает
|
У непрерывного потока событий нет запуска Airflow, поэтому `_load_id`
|
||||||
повтор доставки. У `ods.order` версию задаёт `snapshot_date` — дата слепка
|
в его STG и ODS отсутствует.
|
||||||
источника; `_load_ts` только показывает, когда конкретная строка приехала.
|
|
||||||
|
|
||||||
`created_at` и `updated_at` заказа к служебным колонкам хранилища не относятся.
|
Время события `UTCEventTime` и дата `EventDate` приходят из счетчика сайта.
|
||||||
Они приезжают в сообщении как аудит строки в БД источника и в ODS разбираются в
|
Поля заказа `created_at` и `updated_at` приходят из базы магазина.
|
||||||
`DateTime64(3, 'UTC')`; ClickHouse их не создаёт и добавляет рядом собственную
|
Они относятся к модельному времени; `_load_ts` — к реальным часам стенда.
|
||||||
`_load_ts`. Совпадение слов «техническое время» не делает эти часы одной осью.
|
Например, событие июня можно загрузить в сентябре: его `EventDate`
|
||||||
|
останется июньской, а `_load_ts` будет сентябрьской.
|
||||||
|
|
||||||
Пакетной переобработки у событий в ODS нет: слой наполняют матвью, а не задание
|
События разбираются материализованными представлениями. Автоматической
|
||||||
Airflow. Переделать разобранное руками можно вставкой из STG с фильтром по
|
пакетной переобработки у них нет; сохраненный срез STG можно разобрать
|
||||||
`_load_ts` в пределах трёхсуточного окна; ничья по версии разрешается в пользу
|
отдельным запросом до истечения срока хранения. При повторной вставке
|
||||||
вставленного позже. Заказы разбирает пакетный шаг из неизменного среза STG по
|
с тем же ключом и `_load_ts` `ReplacingMergeTree` оставляет более поздно
|
||||||
`_load_id`; дневные партиции ODS он не заменяет.
|
вставленную строку. Заказы разбирает задача Airflow по `_load_id`.
|
||||||
|
В DDS метки загрузки уже относятся к собственной сборке слоя —
|
||||||
Имя согласовано с каноном служебных полей соседнего учебного стенда на
|
[пример заказа](dds/order.md#служебные-колонки).
|
||||||
Greenplum, чтобы словарь был общим у двух хранилищ; ведущее подчёркивание у
|
|
||||||
технических колонок — распространённая запись,
|
|
||||||
её же используют Fivetran, Airbyte и Stitch. С правилом выше это не спорит:
|
|
||||||
запрещено совпадать с именами виртуальных колонок, а не носить подчёркивание.
|
|
||||||
|
|
||||||
Общего идентификатора пачки загрузки (`_load_id`) нет. У потока событий нет ни
|
|
||||||
пачки, ни `run_id`, и колонка была бы пустой формальностью. У заказов читатель
|
|
||||||
назван: `_load_id` равен `run_id` Airflow и переносится из STG в годную строку
|
|
||||||
ODS и в таблицу ошибок. Нужен ли он выше ODS, решается вместе с моделью DDS.
|
|
||||||
|
|
||||||
## Актуальные события
|
## Актуальные события
|
||||||
|
|
||||||
`ods.event_rep` хранит физически принятые версии события в
|
Повторная отправка события может дать несколько строк в `ods.event_rep`.
|
||||||
`ReplacingMergeTree(_load_ts)`, а `ods.event_dist` служит для записи и
|
Движок `ReplicatedReplacingMergeTree(_load_ts)` считает версиями строки
|
||||||
диагностики доставки. Обычное чтение этой пары может показать несколько строк
|
с одинаковым полным ключом сортировки:
|
||||||
одного `WatchID`, пока фоновые слияния их не схлопнули.
|
`(CounterID, EventDate, intHash32(ClientID), WatchID)`.
|
||||||
|
При слиянии остается строка с максимальным `_load_ts`, то есть последняя
|
||||||
|
доставка. Один `WatchID` обозначает событие, но физический ключ замены шире.
|
||||||
|
|
||||||
`ods.event_v` — обычное представление над `ods.event_dist FINAL`. Оно сохраняет
|
`ods.event_v` читает `ods.event_dist FINAL`: выбирает актуальную строку
|
||||||
имена, типы и зерно события, но всегда возвращает актуальную версию. Актуальное
|
при выполнении запроса, не дожидаясь фоновых слияний. Поэтому события
|
||||||
состояние читают через него, а не повторяют `FINAL` в каждом запросе.
|
считают через `_v`; число строк в `_dist` зависит от уже прошедших слияний.
|
||||||
`dds.event_v` отвечает за другое: даёт событию язык бизнес-модели, в том
|
`dds.event_v` использует этот результат, дает колонкам имена в `snake_case`
|
||||||
числе snake_case-имена и расшифровку кодов.
|
и расшифровывает коды.
|
||||||
|
|
||||||
Типы ODS обычно повторяют контракт выгрузки. Денежный `purchaseRevenue` —
|
Выбор по полному `ORDER BY` и максимальной версии, включая чтение с `FINAL`,
|
||||||
исключение: источник объявляет `Array(Float64)`, а материализованное
|
сверен через Context7 с [документацией ClickHouse](https://clickhouse.com/docs/reference/engines/table-engines/mergetree-family/replacingmergetree).
|
||||||
представление ODS разбирает числа сырого JSON сразу в `Array(Decimal(18, 2))`.
|
В стенде закреплена версия 26.3.17.56; `_v` использует `FINAL`, чтобы
|
||||||
DDS наследует этот тип без нового преобразования. Прямой разбор подтвержден
|
результат не зависел от фоновых слияний.
|
||||||
документацией ClickHouse через Context7 и запросом к ClickHouse 26.3; сравнение
|
|
||||||
с сырыми данными подтвердило: копейки всех покупок целы.
|
Типы ODS повторяют [описание выгрузки](../formats/clickstream-event.md)
|
||||||
|
с одним денежным исключением: `purchaseRevenue` объявлен источником
|
||||||
|
как `Array(Float64)`, а из JSON разбирается сразу в
|
||||||
|
`Array(Decimal(18, 2))`. Это сумма в рублях с копейками; DDS наследует тип.
|
||||||
|
`productPrice` содержит округленные целые рубли и не подходит для
|
||||||
|
восстановления точной суммы покупки. Прямой разбор `purchaseRevenue`
|
||||||
|
проверен на ClickHouse 26.3; результаты сохранены в разделе «Что проверено».
|
||||||
|
|
||||||
## Версии заказов
|
## Версии заказов
|
||||||
|
|
||||||
|
Слепки повторяют состояние заказов из семидневного окна источника.
|
||||||
`ods.order_rep` хранит принятые версии в
|
`ods.order_rep` хранит принятые версии в
|
||||||
`ReplacingMergeTree(snapshot_date)`: ключ сортировки — `order_id`, партиция —
|
`ReplicatedReplacingMergeTree(snapshot_date)`: ключ сортировки — `order_id`,
|
||||||
`toDate(created_at)`. `ods.order_dist` шардирует по
|
партиция — `toDate(created_at)` в UTC. Запись через `ods.order_dist`
|
||||||
`cityHash64(order_id)`. Все версии заказа лежат в одной партиции, чтобы их могли
|
распределяет их по `cityHash64(order_id)`. У всех версий заказа одинаковы
|
||||||
схлопывать фоновые слияния, и на одном шарде, чтобы распределённый `FINAL`
|
день создания и шард, поэтому они могут заменять друг друга.
|
||||||
выбрал одного победителя.
|
|
||||||
|
|
||||||
Физическая пара нужна для загрузки и диагностики. Обычное чтение показывает
|
`ods.order_v` читает `_dist FINAL` и оставляет строку с максимальной датой
|
||||||
версии, которые ещё не убрали фоновые слияния, и не является архивом истории.
|
слепка. Дата `snapshot_date` показывает последнее принятое наблюдение,
|
||||||
`ods.order_v` служит поверхностью точного текущего состояния для следующих
|
а `updated_at` — время изменения заказа в магазине. У неизменившегося
|
||||||
слоёв. Представление сохраняет язык источника и не решает, какой станет модель
|
заказа в следующем слепке дата наблюдения обновится, время изменения — нет.
|
||||||
DDS. `snapshot_date` в нём одновременно дата наблюдения и колонка версии. Она
|
|
||||||
не доказывает полноту принятой порции, поэтому ODS не публикует партиции по
|
Источник выпускает один слепок на дату, а поздний слепок показывает
|
||||||
дате слепка. Модель версионного ODS записана в
|
не менее новое состояние. Отсутствие заказа в следующем слепке
|
||||||
[ADR 0010](../adr/0010-order-versions-in-ods.md), выбор колонки версии описан в
|
не означает удаления. Физическая таблица не служит архивом:
|
||||||
[ADR 0014](../adr/0014-order-version-snapshot-date.md), пакетный приём описан в
|
старые версии исчезают при фоновых слияниях.
|
||||||
[спецификации приёма заказов](orders/ingestion.md). Контракт следующего слоя
|
|
||||||
задан в [модели заказа в DDS](dds/order.md).
|
Прием добавляет версии отдельными строками. `snapshot_date` не доказывает,
|
||||||
|
что Kafka отдала весь слепок, поэтому партиция по этой дате не заменяется.
|
||||||
|
Подробности — [прием заказов](orders/ingestion.md),
|
||||||
|
[ADR 0010](../adr/0010-order-versions-in-ods.md) и
|
||||||
|
[ADR 0014](../adr/0014-order-version-snapshot-date.md).
|
||||||
|
Следующий слой описан в [модели заказа DDS](dds/order.md).
|
||||||
|
|
||||||
## Часовые пояса
|
## Часовые пояса
|
||||||
|
|
||||||
@@ -308,169 +287,148 @@ UTC+4), и пересчёт идёт один раз при наполнении
|
|||||||
|
|
||||||
## Приём: поток и его свойства
|
## Приём: поток и его свойства
|
||||||
|
|
||||||
Раздел — про события. У заказов приём устроен иначе: слепок забирает по команде
|
События принимаются непрерывно. Заказы читает отдельным запросом
|
||||||
даг `orders_ingest`, а не подписанная навсегда матвью — [ADR
|
даг `orders_ingest` — [описание пакетного приема](orders/ingestion.md).
|
||||||
0008](../adr/0008-order-ingestion.md) и [спецификация приёма
|
У событий путь такой:
|
||||||
заказов](orders/ingestion.md).
|
|
||||||
|
|
||||||
Цепочка одна: чтец топика → матвью → STG → матвью разбора → событие и таблица
|
```text
|
||||||
ошибок ODS.
|
stg.hits_raw_kafka → stg.hits_raw_mv → stg.hits_raw_dist
|
||||||
|
├→ ods.event_mv → ods.event_dist
|
||||||
|
└→ ods.event_errors_mv → ods.event_errors_dist
|
||||||
|
```
|
||||||
|
|
||||||
Чтец стоит на обеих нодах и читает одной группой потребителей — имя группы
|
Таблицы Kafka на обеих нодах используют одну группу `clickstream_hits`.
|
||||||
`clickstream_hits`, и оно одинаково на обеих нодах по построению: DDL идёт
|
Ноды делят чтение партиций топика. Разные группы прочитали бы каждая
|
||||||
`ON CLUSTER` и макросов в имени не содержит. Разные группы дали бы каждой ноде
|
свою полную копию потока. Имя группы одинаково в DDL на всем
|
||||||
полную копию топика, и это отдельная сцена для лабы, а не рабочий режим. Имя
|
`clickstream_cluster`; конфигурация кластера —
|
||||||
кластера в `ON CLUSTER` и в движке `Distributed` — `clickstream_cluster`, оно
|
`infra/clickhouse/config.d/cluster.xml`.
|
||||||
задано в `infra/clickhouse/config.d/cluster.xml`.
|
|
||||||
|
|
||||||
**Источник матвью разбора — `stg.hits_raw_dist`, а не локальная таблица.**
|
Материализованные представления разбора срабатывают на вставку
|
||||||
У матвью две привязки: источник, на вставку в который она срабатывает, и цель,
|
в `stg.hits_raw_dist`, до распределения строк по шардам.
|
||||||
куда пишет. Распределённая таблица — лицо слоя, локальная — его хранилище;
|
Оба видят вставляемую порцию на той ноде, которая читает Kafka.
|
||||||
потребитель слоя цепляется к лицу. Практически это значит, что разбор идёт на
|
`stg.hits_raw_mv` создается после них: его подключение запускает
|
||||||
той же ноде, что читала Kafka, в момент вставки первой матвью — до раскладки по
|
чтение топика. Если начать чтение раньше, сообщения успеют попасть
|
||||||
шардам.
|
в STG без разбора в ODS.
|
||||||
|
|
||||||
**Вставка фоновая, и окно потери мы принимаем.** Вставка в распределённую
|
Условие приема проверяет три вещи:
|
||||||
таблицу кладёт блок в локальный спул и сразу возвращает управление, а Kafka
|
|
||||||
коммитит офсеты по факту работы матвью — то есть по факту записи в спул. Топик
|
|
||||||
уже считает сообщение прочитанным, хотя на шарде его ещё нет: умри нода в этом
|
|
||||||
промежутке — сообщения не перечитаются.
|
|
||||||
|
|
||||||
Закрывает окно настройка `distributed_foreground_insert = 1`, и на ETL-вставках
|
- корень `raw` — объект JSON;
|
||||||
Airflow она стоит — там это обычный `SETTINGS` у запроса. На пути приёма её нет,
|
- набор верхнеуровневых ключей совпадает с 47 полями формата;
|
||||||
и по трём причинам. Вставку выполняет фоновый поток Kafka-движка, своего запроса
|
- пять опорных полей, `WatchID`, `VisitID`, `ClientID`, `EventDate`
|
||||||
у него не бывает, так что настройка уровня запроса доехала бы только профилем
|
и `UTCEventTime`, разбираются в ожидаемые типы.
|
||||||
пользователя в конфигурации ноды. Синхронный режим связывает шарды: пока второй
|
|
||||||
недоступен, вставка падает, офсеты не коммитятся, и приём встаёт целиком — тогда
|
|
||||||
как при фоновом первая нода продолжает принимать и копит спул для соседа.
|
|
||||||
Платится при этом не одно ожидание на блок, а три распределённые вставки — STG,
|
|
||||||
событие, ошибки, — и все внутри потока-потребителя, что само по себе повод для
|
|
||||||
ребаланса по таймауту сессии. Против всего этого — окно в сотню миллисекунд на
|
|
||||||
ноутбуке, где мир пересобирается одной командой. Размен не в пользу настройки, а
|
|
||||||
компромисс полезнее показать, чем спрятать за галочкой.
|
|
||||||
|
|
||||||
Ещё одно место, где сырой слой хранит не всё приехавшее: запись с пустым
|
Остальные поля извлекаются по описанию выгрузки без отдельных проверок.
|
||||||
значением и запись-надгробие проходят молча, не оставляя строки. Свойство
|
Это граница приема: годная строка не означает, что проверен смысл
|
||||||
измерено и принято осознанно — подробности в разделе «Что проверено».
|
всех 47 значений. Подробности — в [ADR 0005](../adr/0005-event-ingestion.md).
|
||||||
|
|
||||||
**Гарантии нет ни в одну сторону — есть два узких окна.** Окно потери описано
|
`ods.event_mv` принимает строки, для которых условие истинно,
|
||||||
выше: нода умерла между коммитом офсетов и сбросом спула. Окно дубля
|
`ods.event_errors_mv` — для которых оно ложно. Условие не должно
|
||||||
противоположное: нода умерла после записи на шард, но до коммита офсетов, и при
|
возвращать `NULL`: такую строку не выбрали бы ни оно, ни его отрицание.
|
||||||
перечитывании сообщение приедет второй раз. Сказать про такой приём «хотя бы
|
Разбор использует функции без исключений на некорректном сообщении,
|
||||||
один раз» нельзя — это обещало бы, что потерь не бывает, а они возможны.
|
поскольку ошибка материализованного представления прерывает вставку
|
||||||
|
и останавливает чтение Kafka до устранения причины.
|
||||||
|
|
||||||
Дубль ниже по течению ведёт себя по-разному. В ODS его схлопнет
|
### Доставка и повторы
|
||||||
`ReplacingMergeTree`, а таблицы STG вовсе не устраняют дубли: перезаливка
|
|
||||||
модельного дня честно удваивает `count()` в STG, и живёт эта пара до истечения
|
|
||||||
срока хранения. Это свойство слоя, а не поломка, — но обещание идемпотентности
|
|
||||||
конвейера к сырому слою не относится.
|
|
||||||
|
|
||||||
Оговорка к последнему: у семейства `Replicated*` есть своя дедупликация — блок с
|
Вставка в Distributed на этом пути фоновая: блок сначала записывается
|
||||||
тем же хешем, вставленный повторно, отбрасывается (`insert_deduplicate`).
|
в локальную очередь на диске, затем отправляется на шарды.
|
||||||
Удвоение строк STG проходит мимо неё только потому, что при повторном чтении
|
Kafka подтверждает обработку после выполнения материализованных
|
||||||
`_load_ts` новый и хеш блока другой. Свойство слоя держится на этом, а не на
|
представлений, когда данные еще могут находиться в очереди.
|
||||||
отсутствии механизма.
|
Если после подтверждения потерять ноду вместе с очередью, сообщения
|
||||||
|
не перечитаются. Возможен и повтор: данные уже записались на шард,
|
||||||
|
но подтверждение Kafka не успело сохраниться.
|
||||||
|
Поэтому прием не гарантирует доставку каждого сообщения хотя бы один раз.
|
||||||
|
|
||||||
**Матвью разбора две, и их условия обязаны делить поток без зазора и без
|
В ETL-запросах Airflow `distributed_foreground_insert = 1` дожидается
|
||||||
нахлёста.** Одна забирает годные строки в `ods.event_dist`, вторая — брак в
|
записи на шарды. У фонового приема событий этой настройки нет:
|
||||||
`ods.event_errors_dist`. Строка, подошедшая обеим, задвоится; не подошедшая ни
|
недоступность одного шарда тогда остановила бы весь прием. В принятом
|
||||||
одной — исчезнет молча. Держится это формой: второе условие пишется буквальным
|
режиме доступная нода продолжает принимать и накапливает очередь
|
||||||
отрицанием первого, а сам предикат NULL не возвращает ни в одной своей части, —
|
для недоступного шарда. Ограничение сохранено для учебного стенда;
|
||||||
иначе трёхзначная логика даст строку, которую не возьмёт ни `условие`, ни
|
подробности и наблюдения — в разделе «Что проверено».
|
||||||
`NOT условие`. Обнуляемый разбор в предикате поэтому есть, но заканчивается
|
|
||||||
`IS NOT NULL`, а сравнения дают 0 или 1. Функции предиката не должны и бросать
|
|
||||||
исключений: упавшая матвью
|
|
||||||
роняет вставку и останавливает потребление до починки
|
|
||||||
([ADR 0005](../adr/0005-event-ingestion.md)).
|
|
||||||
|
|
||||||
Постоянной сверки **слоёв между собой** при этом нет и не должно быть. У сырого
|
В STG повтор сохраняется, в ODS его устраняет выбор актуальной версии.
|
||||||
слоя срок жизни трое суток, а ODS хранит всё, поэтому равенство «строки STG =
|
Хотя `Replicated*` умеет отбрасывать повторно вставленные одинаковые блоки,
|
||||||
события + ошибки» разъедется само: строки STG истекут раньше, а перезаливка
|
при новом чтении Kafka меняется `_load_ts`, а вместе с ним и хеш блока.
|
||||||
модельного дня задвоит их, тогда как в ODS тот же повтор схлопнется. Правило,
|
Поэтому повторная отправка модельного дня увеличивает число строк STG.
|
||||||
красное в норме, учит не смотреть на оповещения ([ADR
|
|
||||||
0002](../adr/0002-monitoring-scope.md)). Равенство проверено разовым опытом при
|
|
||||||
исполнении #43, на управляемой пачке: отправили N сообщений — получили N строк
|
|
||||||
STG и N в сумме событий и ошибок. Постоянной целью такой опыт не становится, и
|
|
||||||
почему — в [карте проверок](testing.md), раздел «Интеграционная проверка
|
|
||||||
постоянной целью не становится». Счёт актуальных событий идёт через
|
|
||||||
`ods.event_v`, которое скрывает `FINAL`: голый `count()` по `ReplacingMergeTree`
|
|
||||||
зависит от того, сколько мержей успело пройти, и спека это прямо запрещает
|
|
||||||
(раздел 6).
|
|
||||||
|
|
||||||
**Постоянная сверка одна, и она другого рода** — не слой против слоя, а ODS
|
Пустое значение сообщения и значение `null` не дают строки даже в STG:
|
||||||
против [паспорта мира](../../data/world-inventory.json), лежащего в git (`make
|
чтец продвигает позицию Kafka, но не передает строку на разбор.
|
||||||
check-clickhouse`, пришла с #42). Разъехаться сама она не может, и в этом вся
|
Генератор таких сообщений не создает. Поведение измерено на стенде
|
||||||
разница: паспорт описывает восемь дней стартового мира, счёт обрамлён их датами,
|
и описано в разделе «Что проверено».
|
||||||
повтор заливки схлопывается в `ods.event_v`, а срок хранения ей не помеха — ODS
|
|
||||||
хранит всё. Обе стороны равенства зафиксированы: одна кодом генератора, другая
|
### Что можно сверять
|
||||||
файлом в git. У межслойной сверки такой опоры нет ни с одной стороны.
|
|
||||||
|
Для короткого неизменного среза доставки можно проверить баланс:
|
||||||
|
число строк STG равно сумме годных строк и ошибок до удаления повторов.
|
||||||
|
Постоянно сравнивать полные таблицы так нельзя: STG удаляет старые данные,
|
||||||
|
ODS оставляет события и объединяет повторные доставки.
|
||||||
|
Счетчики по слоям тогда расходятся при штатной работе.
|
||||||
|
|
||||||
|
`make check-clickhouse` сравнивает актуальные события в `ods.event_v`
|
||||||
|
с [паспортом мира](../../data/world-inventory.json) только за восемь
|
||||||
|
начальных дней. Эта проверка учитывает повторную отправку через `FINAL`
|
||||||
|
и не зависит от срока жизни STG. Границы остальных проверок —
|
||||||
|
в [карте проверок](testing.md).
|
||||||
|
|
||||||
## Срок жизни сырых данных
|
## Срок жизни сырых данных
|
||||||
|
|
||||||
Сырые данные в STG живут трое суток реального времени и уходят сами. Трое — это
|
TTL — правило автоматического удаления старых данных. В обеих таблицах
|
||||||
окно отладки: столько сырые данные хранятся на ноутбуке, чтобы менти успел
|
STG срок задан как три суток от `_load_ts`, по реальному времени загрузки.
|
||||||
разобрать полёты, после чего перестаёт занимать место. Нарезка — по дню
|
Партиции тоже нарезаны по дню `_load_ts`. Это оставляет короткий период
|
||||||
загрузки, срок — по той же колонке `_load_ts`, снятие — целыми кусками
|
для поиска ошибок и повторного разбора, после которого данные освобождают
|
||||||
(`ttl_only_drop_parts`). В DDL значение проставлено явно, чтобы поведение не
|
место на диске.
|
||||||
зависело от умолчания версии.
|
|
||||||
|
|
||||||
Нарезать сырые данные по модельному дню события было бы соблазнительно — он
|
Настройка `ttl_only_drop_parts = 1` удаляет только целые части данных —
|
||||||
единица переобработки и он же ключ партиции в ODS, — но чистку это ломает. Кусок
|
файлы, которые ClickHouse создает при вставках и объединяет при слияниях.
|
||||||
снимается целиком, только когда в нём истекли все строки, а в партиции
|
Часть удаляется, когда срок истек у всех ее строк. Внутри партиции
|
||||||
модельного дня лежит приехавшее в разное реальное время: мерж склеит куски
|
дня загрузки возраст строк отличается не более чем на сутки.
|
||||||
разного возраста, самая свежая строка удержит весь кусок, и данные переживут
|
Поэтому к трем суткам может добавиться до суток ожидания самой свежей
|
||||||
срок неограниченно.
|
строки части, а затем ожидание фоновой обработки TTL. Точного времени
|
||||||
|
удаления правило не обещает.
|
||||||
|
|
||||||
В партиции дня загрузки склейка идёт точно так же, и строки в ней тоже разного
|
Модельная дата для TTL не подходит: события июня могут загружаться
|
||||||
возраста — но не более чем на сутки, потому что партицию закрывает календарный
|
осенью, и срок от `EventDate` истек бы сразу. Партиция по модельному дню
|
||||||
день. Отсюда и оценка: сырые данные живут трое суток плюс хвост до суток, а не
|
тоже смешала бы доставки разного возраста: очередная отправка дня могла бы
|
||||||
ровно трое. Оговорка про сроки: TTL исполняется на мержах, а не по будильнику,
|
продлевать хранение старых строк после слияния частей.
|
||||||
так что «уходит само» здесь обещано, а «уходит вовремя» — нет.
|
|
||||||
|
|
||||||
Модельного дня среди колонок STG нет вовсе. Он свойство содержимого, а
|
Для разбора сохраненных сообщений выбирайте небольшой срез по `_load_ts`.
|
||||||
содержимое разбирает ODS — там `EventDate` и живёт, ключом партиции. Сырые
|
Так запрос ограничивает партиции и сохраняет некорректные сообщения,
|
||||||
данные режутся по своим координатам: и разбор полётов, и переобработка фильтруют
|
из которых модельную дату извлечь нельзя. `EventDate` появляется
|
||||||
по `_load_ts`, попадая в ключ партиции, а не идя сплошным проходом. Фильтр по
|
отдельной колонкой только в ODS. В [задании ступени 6](../labs/6-1-event-parsing.md)
|
||||||
модельному дню вдобавок пропускал бы битые строки — у них дата не извлекается.
|
такой срез разбирается рядом с действующим приемом.
|
||||||
|
|
||||||
Две оси времени тут не совпадают намеренно. Ось модельного времени начинается в
|
|
||||||
D0 и к реальному календарю не привязана; пакетный режим проигрывает две недели
|
|
||||||
модельного мира за минуты реальных. Поэтому у переобработки и у гигиены диска
|
|
||||||
разные часы, и обслуживают их разные средства. Декларативный TTL по модельной
|
|
||||||
дате был бы просто сломан: он отсчитывает срок от реального «сейчас» и удалял бы
|
|
||||||
эталонные дни прямо на входе.
|
|
||||||
|
|
||||||
В бою сырой слой иногда собирают на движке `Null` — тогда он не хранится вовсе.
|
|
||||||
Такой вариант отвергнут: на стенде сырые данные нужны для отладки, поэтому окно,
|
|
||||||
а не ноль.
|
|
||||||
|
|
||||||
## Таблица ошибок
|
## Таблица ошибок
|
||||||
|
|
||||||
`ods.event_errors` держит строки, не прошедшие строгий приём, вместе с их сырым
|
`ods.event_errors_dist` содержит сообщения, отвергнутые разбором событий:
|
||||||
текстом, метаданными доставки и классом брака. Ключи её собственные, потому что
|
исходный `raw`, сведения о доставке и класс ошибки `error_class`.
|
||||||
у брака нет разобранных полей: шардируется `cityHash64` сырой строки —
|
Класс имеет тип `LowCardinality(String)` и выбирается по первому
|
||||||
`ClientID` у строки, которая не разобралась, взять неоткуда; нарезается по дню
|
нарушенному условию:
|
||||||
загрузки, как и сырые данные; живёт месяц. Дольше сырых данных — намеренно: если
|
|
||||||
брак истекает вместе с ними, разбираться к моменту разбирательства будет уже
|
|
||||||
нечем. Снятие — целыми кусками (`ttl_only_drop_parts`), как у STG и по той же
|
|
||||||
причине: строки в партиции дня загрузки разного возраста не более чем на сутки,
|
|
||||||
и куску незачем переживать срок из-за самой свежей строки.
|
|
||||||
|
|
||||||
Класс брака лежит в колонке `error_class` типа `LowCardinality(String)`. Без неё
|
1. `not_an_object` — корень JSON не объект;
|
||||||
в таблице копятся строки «что-то не так» без ответа на «что именно», а витрине
|
2. `keyset_mismatch` — набор ключей не совпал с форматом;
|
||||||
качества не на что опереться. Сами классы, их порядок и довод, почему порядок
|
3. `key_field_unparsed` — одно из пяти опорных полей не разобралось.
|
||||||
обязателен, — в [ADR 0005](../adr/0005-event-ingestion.md).
|
|
||||||
|
|
||||||
Движок — обычный `ReplicatedMergeTree`, без замены версий: схлопывать брак не по
|
Порядок важен: строка вместо объекта нарушает сразу несколько условий,
|
||||||
чему, у него нет ключа сущности. `ORDER BY` — `(error_class, kafka_partition,
|
но получает один класс. Полные условия — в
|
||||||
kafka_offset)`: смотрят такую таблицу от класса, а внутри класса — по координатам
|
[ADR 0005](../adr/0005-event-ingestion.md).
|
||||||
доставки.
|
|
||||||
|
|
||||||
`ods.order_errors` держит тот же диагностический минимум и `_load_id`
|
`ods.order_errors_dist` хранит такой же набор сведений и `_load_id`
|
||||||
запуска. У заказов три класса по приоритету: `not_an_object`,
|
запуска приема. Первые два класса совпадают с событиями,
|
||||||
`keyset_mismatch`, `field_invalid`. Сырой текст остаётся рядом, поэтому класс не
|
третий называется `field_invalid`: у заказа проверяются все поля
|
||||||
разрастается до имени отдельного поля. Точная граница приёма — в
|
верхнего уровня. Точные правила — в [описании приема заказов](orders/ingestion.md#граница-строгого-приёма).
|
||||||
[спецификации заказов](orders/ingestion.md).
|
|
||||||
|
Таблицы ошибок используют `ReplicatedMergeTree` без замены версий:
|
||||||
|
у некорректного сообщения может не быть разобранного ключа сущности.
|
||||||
|
Шард выбирается по `cityHash64(raw)`, ключ сортировки —
|
||||||
|
`(error_class, kafka_partition, kafka_offset)`, партиция — день `_load_ts`.
|
||||||
|
Начать поиск можно с класса ошибки, затем перейти к сообщениям
|
||||||
|
по координатам Kafka.
|
||||||
|
|
||||||
|
Срок хранения — один месяц от `_load_ts`, удаление целыми частями
|
||||||
|
с `ttl_only_drop_parts = 1`, как в STG. Ошибка вместе с исходным текстом
|
||||||
|
остается доступна дольше остальных сырых сообщений. Повторная обработка
|
||||||
|
может добавить тот же брак еще раз; у заказов повтор виден по совпадению
|
||||||
|
`_load_id` и координат Kafka.
|
||||||
|
|
||||||
## Справочники
|
## Справочники
|
||||||
|
|
||||||
|
|||||||
@@ -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 целиком: добавленная в интерфейсе метрика или подпись колонки
|
||||||
исчезнет при следующем старте контейнера. Правка датасета — это правка файла.
|
исчезнет при следующем старте контейнера. Правка датасета — это правка файла.
|
||||||
@@ -196,6 +238,13 @@ superset import-directory /app/import --overwrite
|
|||||||
каждой витрины, — поэтому фильтр периода дашборду разрешен, а кросс-фильтрация
|
каждой витрины, — поэтому фильтр периода дашборду разрешен, а кросс-фильтрация
|
||||||
нет.
|
нет.
|
||||||
|
|
||||||
|
**Вкладки и блоки «что смотреть».** Вкладки были условным вариантом: их
|
||||||
|
заводят, когда экран перестает читаться одним взглядом. Четыре строки панелей
|
||||||
|
читаются сразу, поэтому вкладки спрятали бы половину экрана за щелчком без
|
||||||
|
выигрыша. Блок с текстом «что смотреть» рядом с панелью означал
|
||||||
|
бы, что панель сама себя не объясняет: тогда чинить надо название и подписи, а
|
||||||
|
не дописывать к ним пояснение.
|
||||||
|
|
||||||
**Пороги и раскраска панелей.** Правило
|
**Пороги и раскраска панелей.** Правило
|
||||||
[спеки](../specs/2026-07-30-stand-v2-realism.md), раздел 9: на дашборд смотрят
|
[спеки](../specs/2026-07-30-stand-v2-realism.md), раздел 9: на дашборд смотрят
|
||||||
глазами, красное и зеленое живет в проверках.
|
глазами, красное и зеленое живет в проверках.
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ DDS складывает свою модель и называет атрибу
|
|||||||
| 41 | `productID` | `Array(String)` | `object` | `product_id` | id товаров события |
|
| 41 | `productID` | `Array(String)` | `object` | `product_id` | id товаров события |
|
||||||
| 42 | `productName` | `Array(String)` | `object` | `product_name` | названия тех же товаров |
|
| 42 | `productName` | `Array(String)` | `object` | `product_name` | названия тех же товаров |
|
||||||
| 43 | `productCategory` | `Array(String)` | `object` | `product_category` | категории тех же товаров |
|
| 43 | `productCategory` | `Array(String)` | `object` | `product_category` | категории тех же товаров |
|
||||||
| 44 | `productPrice` | `Array(Int64)` | `int64` | `product_price` | цена за штуку целым числом: деньги генератор считает целыми |
|
| 44 | `productPrice` | `Array(Int64)` | `int64` | `product_price` | цена за штуку, округленная до целых рублей |
|
||||||
| 45 | `productQuantity` | `Array(UInt64)` | `uint64` | `product_quantity` | количество штук каждого товара |
|
| 45 | `productQuantity` | `Array(UInt64)` | `uint64` | `product_quantity` | количество штук каждого товара |
|
||||||
| 46 | `productEventType` | `Array(String)` | `object` | `product_event_type` | действие с товаром: стенд шлёт add и purchase, полный словарь Метрики (detail, remove, impressions) не берём |
|
| 46 | `productEventType` | `Array(String)` | `object` | `product_event_type` | действие с товаром: стенд шлёт add и purchase, полный словарь Метрики (detail, remove, impressions) не берём |
|
||||||
| 47 | `ecommerce` | `String` | `object` | `ecommerce` | сырой JSON события, как отдаёт Метрика — материал лабы про разбор JSON внутри колонки |
|
| 47 | `ecommerce` | `String` | `object` | `ecommerce` | сырой JSON события, как отдаёт Метрика — материал лабы про разбор JSON внутри колонки |
|
||||||
+26
-27
@@ -1,50 +1,49 @@
|
|||||||
# Путеводитель по стенду
|
# Путеводитель по стенду
|
||||||
|
|
||||||
Если вы впервые видите стенд, начните отсюда. Путеводитель подскажет, на что
|
Путеводитель объясняет, как данные поступают в хранилище, обрабатываются
|
||||||
смотреть и в каком порядке, а за подробностями отправит в справочники. Так вы
|
и попадают в отчеты. В главах есть короткие примеры, которые можно выполнить
|
||||||
разберетесь в устройстве, прежде чем браться за уроки.
|
на запущенном стенде перед переходом к учебным заданиям.
|
||||||
|
|
||||||
## Как читать
|
## Порядок чтения
|
||||||
|
|
||||||
Тропа такая: [словарь понятий](../../CONTEXT.md) → главы путеводителя →
|
Сначала запустите стенд по [инструкции](../../README.md#быстрый-старт).
|
||||||
[справочники `docs/architecture/`](../architecture/) →
|
Затем читайте главы по порядку и выполняйте примеры из них.
|
||||||
[уроки](../labs/README.md).
|
Незнакомые термины можно найти в
|
||||||
|
[словаре понятий](../../CONTEXT.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)
|
### [Мониторинг в Grafana](monitoring.md)
|
||||||
|
|
||||||
Свежесть и поток данных, брак и отставание собраны на дашборде «Данные».
|
Что показывают панели Grafana и как меняются их показания, пока в стенд
|
||||||
Глава научит находить нужную панель и разбирать ее показания.
|
поступают события и заказы. Вы увидите, почему заказы обновляются реже событий.
|
||||||
|
|
||||||
### [Немного Superset](superset.md)
|
### [Отчеты в Superset](superset.md)
|
||||||
|
|
||||||
Глава покажет, как витрина становится графиком и где проходит граница между
|
Как Superset строит графики по витринам. Вы сравните числа на готовом графике
|
||||||
своими объектами и теми, которые поднимает стенд.
|
с результатом SQL и узнаете, какие отчеты сохраняются при перезапуске стенда.
|
||||||
|
|
||||||
### [Генератор: откуда берутся данные](generator.md)
|
### [Генератор: откуда берутся данные](generator.md)
|
||||||
|
|
||||||
Здесь генератор — источник данных, а не предмет изучения. Глава в общих
|
Откуда берутся события и заказы, почему начальные данные повторяются
|
||||||
чертах объяснит, откуда берутся события и заказы и в каком виде их получает
|
и как сверить число загруженных событий с ожидаемым.
|
||||||
хранилище.
|
|
||||||
@@ -1,94 +1,106 @@
|
|||||||
# Потоки Airflow и даги модельного времени
|
# Генерация и обработка данных в Airflow
|
||||||
|
|
||||||
В Airflow два рода дагов. Даги мира создают мир и проигрывают модельные дни,
|
Airflow управляет генерацией данных и их обработкой в хранилище. Это разные
|
||||||
а дневной конвейер раскладывает сыгранный день по слоям хранилища.
|
этапы: сначала в Kafka поступают события и заказы за день работы магазина,
|
||||||
|
затем по ним рассчитываются данные DDS и витрины DM.
|
||||||
|
Даг описывает задачи Airflow и порядок их выполнения.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
world_initialize --> etl_pipeline
|
world_initialize --> etl_pipeline
|
||||||
world_live_day --> etl_pipeline
|
world_live_day --> etl_pipeline
|
||||||
manual[запуск рукой] --> etl_pipeline
|
manual[ручной запуск] --> etl_pipeline
|
||||||
etl_pipeline --> dds_transform --> dm_transform --> dq_check
|
etl_pipeline --> dds_transform --> dm_transform --> dq_check
|
||||||
world_next_day --> sources[источники без ETL]
|
world_next_day --> sources[источники без ETL]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Два потока входят по-разному
|
## Загрузка событий и заказов
|
||||||
|
|
||||||
События приходят в топик `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-конвейера).
|
||||||
|
|
||||||
## Как найти в интерфейсе
|
## Поиск дагов в Airflow
|
||||||
|
|
||||||
Даги мира ищите по тегам «модельное время» и «жизненный цикл мира». Остальные
|
Даги мира ищите по тегам «модельное время» и «жизненный цикл мира». Остальные
|
||||||
даги ищите по тегам «заказы», `etl`, `dds`, `dm` и `dq`. `homework_check` с
|
даги ищите по тегам «заказы», `etl`, `dds`, `dm` и `dq`. `homework_check` с
|
||||||
тегами `dm` и `labs` проверяет домашние работы; о нем рассказано в учебных
|
тегами `dm` и `labs` проверяет домашние работы; о нем рассказано в учебных
|
||||||
заданиях.
|
заданиях.
|
||||||
|
|
||||||
## Посмотрите сами
|
## Один день от генерации до витрин
|
||||||
|
|
||||||
Откройте 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` завершился успешно, а в витринах нового дня еще нет.
|
||||||
|
Значение `world_position` должно увеличиться на один.
|
||||||
|
Запустите `etl_pipeline` вручную и дождитесь его завершения.
|
||||||
|
|
||||||
В DBeaver, подключенном по
|
В DBeaver, подключенном по
|
||||||
[инструкции](../../README.md#подключиться-из-dbeaver), выполните два запроса:
|
[инструкции](../../README.md#подключиться-из-dbeaver), выполните два запроса:
|
||||||
@@ -106,12 +118,13 @@ ORDER BY report_date DESC
|
|||||||
LIMIT 3;
|
LIMIT 3;
|
||||||
```
|
```
|
||||||
|
|
||||||
В трафике уже есть сыгранный день, а в выручке только предыдущий: вместе с этим
|
В витрине трафика уже есть новый день, а в выручке последняя дата —
|
||||||
днем пришел слепок заказов за предыдущий. Выручка сыгранного дня появится после
|
предыдущий день. Причина в выгрузке заказов: вместе с событиями дня D
|
||||||
следующего.
|
приходит слепок за день D−1. Выручка дня D появится после генерации
|
||||||
|
дня D+1 и запуска конвейера.
|
||||||
|
|
||||||
## Куда дальше
|
## Следующая глава и справочники
|
||||||
|
|
||||||
Детали — в справочниках [конвейера](../architecture/etl.md) и
|
Детали — в справочниках [конвейера](../architecture/etl.md) и
|
||||||
[мира](../architecture/world.md). Следующая глава — [«Мониторинг: куда
|
[мира](../architecture/world.md). Следующая глава —
|
||||||
смотреть»](monitoring.md).
|
[«Мониторинг в Grafana»](monitoring.md).
|
||||||
+152
-154
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
+40
-32
@@ -1,44 +1,51 @@
|
|||||||
# Генератор: откуда берутся данные
|
# Генератор: откуда берутся данные
|
||||||
|
|
||||||
Генератор изображает две системы интернет-магазина. Первая — счетчик на сайте,
|
Генератор имитирует работу двух систем интернет-магазина. Счетчик
|
||||||
он шлет события в топик `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 минут.
|
||||||
|
|
||||||
## Посмотрите сами
|
## Сверка с паспортом мира
|
||||||
|
|
||||||
Подключитесь из DBeaver по [инструкции](../../README.md#подключиться-из-dbeaver)
|
Подключитесь из DBeaver по [инструкции](../../README.md#подключиться-из-dbeaver)
|
||||||
и выполните запрос:
|
и выполните запрос:
|
||||||
@@ -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).
|
||||||
+54
-39
@@ -1,71 +1,86 @@
|
|||||||
# Мониторинг: куда смотреть
|
# Мониторинг в Grafana
|
||||||
|
|
||||||
В Grafana три дашборда, и у каждого своя тема. «Данные» показывает, все ли в
|
В Grafana настроены три дашборда — экрана с показателями и графиками.
|
||||||
порядке с потоком данных. «Кластер» — состояние машин и самого ClickHouse.
|
«Данные» показывает загрузку и обработку событий и заказов,
|
||||||
«Запросы» — какие запросы выполнялись и сколько ресурсов они заняли.
|
«Кластер» — состояние узлов и самого ClickHouse, а «Запросы» — выполненные
|
||||||
|
запросы и затраченные на них ресурсы.
|
||||||
|
|
||||||
Prometheus собирает метрики с двух узлов ClickHouse, keeper и экспортера Kafka.
|
На панелях не заданы пороги для цветовых предупреждений: оценивать показания
|
||||||
Grafana читает и Prometheus, и ClickHouse напрямую. На панелях нет порогов и
|
вы будете самостоятельно. Качество данных автоматически проверяет
|
||||||
цветовой подсветки: оценивать картинку придется самому. Автоматические
|
даг `dq_check`.
|
||||||
проверки, которые выносят вердикт, собраны в контуре DQ.
|
|
||||||
|
|
||||||
## Дашборд «Данные»
|
## Дашборд «Данные»
|
||||||
|
|
||||||
«Свежесть» показывает, сколько времени прошло с последнего события и с
|
«Свежесть» показывает, сколько реального времени прошло с поступления
|
||||||
последнего заказа в ODS. Свежесть заказов растет весь модельный день и
|
последних успешно разобранных событий и заказов. Время поступления хранится
|
||||||
сбрасывается, когда на границе суток приходит слепок. Это нормальное поведение
|
в колонке `_load_ts` слоя ODS. Показатель для заказов растет весь модельный день
|
||||||
источника, а не поломка.
|
и снижается, когда поступает очередная дневная выгрузка — слепок.
|
||||||
|
Это ожидаемо: заказы загружаются раз в день.
|
||||||
|
|
||||||
«Поток» показывает число сырых сообщений в STG за выбранный интервал. «Брак»
|
«Поток» показывает число сырых сообщений в STG за выбранный интервал. «Брак»
|
||||||
рядом — доля строк, которые не удалось разобрать.
|
показывает долю строк, которые не удалось разобрать.
|
||||||
|
|
||||||
«Сходимость» сравнивает число событий в Kafka и в ClickHouse. Тревожный
|
«Сходимость» сравнивает число событий в Kafka и в ClickHouse. Тревожный
|
||||||
признак — устойчивый разрыв между линиями. Расхождение в одном столбце ничего
|
признак — устойчивый разрыв между рядами. Разовое расхождение еще не говорит
|
||||||
не значит.
|
о потере данных: метрики Kafka и строки ClickHouse могут попадать в соседние
|
||||||
|
интервалы на графике.
|
||||||
|
|
||||||
«Отставание» показывает, сколько сообщений в Kafka еще не прочитано
|
«Отставание» показывает, сколько сообщений в Kafka еще не прочитано
|
||||||
потребителями. «Дневной слепок» перечисляет последние десять слепков заказов в
|
потребителями. «Дневной слепок» перечисляет последние десять слепков заказов в
|
||||||
STG: по нему видно, пришла ли выгрузка вовремя.
|
STG: по нему видно, поступила ли очередная выгрузка.
|
||||||
|
|
||||||
Источники и правила чтения всех панелей разобраны в [справочнике дашборда
|
Источники и правила чтения всех панелей разобраны в [справочнике дашборда
|
||||||
«Данные»](../architecture/monitoring/data.md).
|
«Данные»](../architecture/monitoring/data.md).
|
||||||
|
|
||||||
## Почему у панелей разные источники
|
## Источники показателей
|
||||||
|
|
||||||
Свежесть считается по ODS: если разбор остановился, это видно именно там. Поток
|
Grafana получает данные напрямую из ClickHouse и через Prometheus.
|
||||||
и брак считаются по STG, где сохраняется каждое доставленное сообщение. В ODS
|
Prometheus собирает метрики с двух узлов ClickHouse, ClickHouse Keeper
|
||||||
повторы события и старые версии заказа схлопываются, поэтому счетчик там был бы
|
и экспортера Kafka — службы, которая предоставляет метрики Kafka.
|
||||||
занижен. Подробности — в [разделе о слоях
|
|
||||||
|
Свежесть считается по 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).
|
||||||
|
|
||||||
## Посмотрите сами
|
## Показатели за один модельный день
|
||||||
|
|
||||||
Откройте Grafana по адресу из раздела [«Адреса и
|
Откройте Grafana по адресу из раздела [«Адреса и
|
||||||
пароли»](../../README.md#адреса-и-пароли), затем дашборд «Данные». Запустите в
|
пароли»](../../README.md#адреса-и-пароли), затем дашборд «Данные». Запустите в
|
||||||
Airflow [`world_live_day`](airflow-dags.md#даги-мира): модельный день займет
|
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). Следующая глава —
|
Почему выбраны именно эти показатели, объясняет
|
||||||
[«Немного Superset»](superset.md).
|
[ADR 0002](../adr/0002-monitoring-scope.md). Следующая глава —
|
||||||
|
[«Отчеты в Superset»](superset.md).
|
||||||
@@ -1,66 +1,70 @@
|
|||||||
# Хранилище и его слои
|
# Хранилище и его слои
|
||||||
|
|
||||||
В хранилище приходят два потока из Kafka: в топике `hits` — события
|
В Kafka поступают события сайта, или кликстрим, и заказы магазина.
|
||||||
кликстрима, в `orders` — заказы магазина. Оба проходят один путь: STG → ODS →
|
Для событий выделен топик `hits`, для заказов — `orders`.
|
||||||
DDS → DM. Каждый слой отвечает на свой вопрос.
|
Из Kafka данные проходят четыре слоя хранилища. В STG сообщения сохраняются
|
||||||
|
как есть, в ODS разбираются по полям, в DDS из них собираются события,
|
||||||
|
заказы и сессии, а в DM — витрины для отчетов.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
Kafka --> STG --> ODS --> DDS --> DM
|
Kafka --> STG --> ODS --> DDS --> DM
|
||||||
```
|
```
|
||||||
|
|
||||||
## Как читать имена
|
## Имена таблиц и представлений
|
||||||
|
|
||||||
Для обычного чтения выбирайте объекты с суффиксом `_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` нет: так сессии можно собрать
|
|
||||||
самому и сравнить с эталоном.
|
|
||||||
|
|
||||||
## DM: данные для дашбордов
|
Готовые сессии читайте через `dds.session_v`. События для самостоятельного
|
||||||
|
расчета доступны в `dds.event_v`: в этом представлении `VisitID` скрыт.
|
||||||
|
|
||||||
|
## DM: витрины для отчетов
|
||||||
|
|
||||||
DM — слой витрин. Витрина — готовый набор показателей для отчета или
|
DM — слой витрин. Витрина — готовый набор показателей для отчета или
|
||||||
дашборда. Здесь уже посчитаны, например, дневная выручка по категориям и
|
дашборда. Здесь уже посчитаны, например, дневная выручка по категориям и
|
||||||
@@ -70,44 +74,43 @@ 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#карта-таблиц).
|
||||||
|
|
||||||
## Посмотрите сами
|
## Примеры запросов
|
||||||
|
|
||||||
Подключитесь к 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 +125,10 @@ ORDER BY UTCEventTime
|
|||||||
LIMIT 20;
|
LIMIT 20;
|
||||||
```
|
```
|
||||||
|
|
||||||
Второй запрос покажет сессии того же дня. Они рассчитаны из событий
|
Каждая строка здесь — отдельное действие посетителя.
|
||||||
первого запроса:
|
|
||||||
|
Второй запрос покажет 20 сессий того же дня с наибольшим числом событий.
|
||||||
|
Сессии рассчитаны по всем событиям дня:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT
|
SELECT
|
||||||
@@ -140,7 +145,12 @@ ORDER BY events_total DESC
|
|||||||
LIMIT 20;
|
LIMIT 20;
|
||||||
```
|
```
|
||||||
|
|
||||||
Третий запрос покажет готовую выручку по категориям за тот же день:
|
Теперь одна строка представляет целый визит. `events_total` показывает,
|
||||||
|
сколько событий в него вошло; начало и конец визита записаны в
|
||||||
|
`started_at` и `finished_at`.
|
||||||
|
|
||||||
|
Третий запрос покажет выручку магазина по категориям за тот же день.
|
||||||
|
Эта витрина рассчитана по заказам:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT
|
SELECT
|
||||||
@@ -153,14 +163,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).
|
|
||||||
+42
-29
@@ -1,48 +1,56 @@
|
|||||||
# Немного 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`, а потом без предупреждения
|
|
||||||
заменится версией из файла.
|
|
||||||
|
|
||||||
Ваши собственные объекты хранятся в той же базе метаданных Superset, но импорт
|
Готовые датасеты, графики и дашборд сохранены в файлах репозитория
|
||||||
их не трогает: они переживают и `make up`, и `make rebuild-storage`. Стирает их
|
и импортируются при каждом `make up`. Если изменить их через интерфейс,
|
||||||
только `make clean` вместе с остальным постоянным состоянием стенда. Поэтому
|
следующий `make up` заменит изменения версией из файлов без предупреждения.
|
||||||
стендовые объекты оставляйте как есть, а свои отчеты стройте рядом. Устройство
|
|
||||||
|
Созданные вами датасеты, графики и дашборды хранятся в базе метаданных
|
||||||
|
Superset. Импорт их не меняет: они сохраняются после `make up`
|
||||||
|
и `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#стартовый-мир).
|
||||||
|
|
||||||
## Посмотрите сами
|
## Сверка графика с витриной
|
||||||
|
|
||||||
Откройте Superset по адресу из раздела [«Адреса и
|
Откройте Superset по адресу из раздела [«Адреса и
|
||||||
пароли»](../../README.md#адреса-и-пароли) и выберите дашборд «Бизнес-сюжеты».
|
пароли»](../../README.md#адреса-и-пароли) и выберите дашборд «Бизнес-сюжеты».
|
||||||
|
Найдите график «Посетители и известные пользователи».
|
||||||
Затем откройте SQL Lab и выполните запрос:
|
Затем откройте SQL Lab и выполните запрос:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
@@ -52,11 +60,16 @@ ORDER BY report_date DESC
|
|||||||
LIMIT 7;
|
LIMIT 7;
|
||||||
```
|
```
|
||||||
|
|
||||||
Как построить свой отчет, рассказывают [учебные задания](../labs/README.md).
|
Выберите одну дату из результата. На дашборде задайте «Период», в который
|
||||||
|
она входит, и обновите график. Наведите указатель на точку этой даты:
|
||||||
|
числа посетителей и известных пользователей должны совпасть с `visitors`
|
||||||
|
и `known_users` из SQL.
|
||||||
|
|
||||||
## Куда дальше
|
Как построить собственный отчет, рассказывают [учебные задания](../labs/README.md).
|
||||||
|
|
||||||
Устройство BI-зоны описано в [справочнике
|
## Следующая глава и справочники
|
||||||
|
|
||||||
|
Подключение и настройка Superset описаны в [справочнике
|
||||||
Superset](../architecture/superset.md), а состав витрин — в [справочнике
|
Superset](../architecture/superset.md), а состав витрин — в [справочнике
|
||||||
DM](../architecture/dm.md). Следующая глава — [«Генератор: откуда берутся
|
DM](../architecture/dm.md). Следующая глава — [«Генератор: откуда берутся
|
||||||
данные»](generator.md).
|
данные»](generator.md).
|
||||||
@@ -1,37 +1,38 @@
|
|||||||
# Задание 1: `dm.events_enriched_v`
|
# Задание 1: события для анализа, `dm.events_enriched_v`
|
||||||
|
|
||||||
**Чему учит:** строить слой как договор о данных, а не как их копию.
|
Вы подготовите представление с полями для анализа событий. Оно будет
|
||||||
Оставлять витрину представлением, если зерно источника не меняется.
|
добавлять вычисляемые поля при чтении, сохраняя одну строку на событие.
|
||||||
|
|
||||||
## Сначала прочитать
|
## Подготовка
|
||||||
|
|
||||||
- [«Хранилище и его слои»](../handbook/storage-layers.md).
|
- [«Хранилище и его слои»](../handbook/storage-layers.md).
|
||||||
- [Как применить свой DDL](README.md#как-применить-свой-ddl): файл для решения
|
- [Выполнение SQL в DBeaver](README.md#выполнение-sql-в-dbeaver): файл для решения
|
||||||
и подключение с правами на создание витрины.
|
и подключение с правами на создание витрины.
|
||||||
|
|
||||||
## Роль витрины
|
## Данные для анализа событий
|
||||||
|
|
||||||
В `dds.event_v` 44 колонки, включая технические поля источника и массивы
|
В `dds.event_v` 44 колонки, включая технические поля источника и массивы
|
||||||
товаров. Для анализа событий в Superset нужны деловые поля и три готовых
|
товаров. Для анализа событий в Superset нужны сведения о посетителе и его
|
||||||
значения: час события, путь страницы и домен перехода. Тогда вам не придется
|
действиях, а также три вычисляемых поля: час события, путь страницы и домен,
|
||||||
|
с которого пришел посетитель. Тогда вам не придется
|
||||||
повторять эти выражения в каждом запросе.
|
повторять эти выражения в каждом запросе.
|
||||||
|
|
||||||
Витрины следующих заданий читают DDS напрямую. Использовать
|
Витрины следующих заданий читают DDS напрямую. Использовать
|
||||||
`dm.events_enriched_v` как источник для них не нужно: задания независимы, их
|
`dm.events_enriched_v` как источник для них не нужно: задания независимы, их
|
||||||
можно выполнять в любом порядке.
|
можно выполнять в любом порядке.
|
||||||
|
|
||||||
## Что сделать
|
## Создание представления
|
||||||
|
|
||||||
Создайте обычное представление `CREATE VIEW dm.events_enriched_v`.
|
Создайте обычное представление `CREATE VIEW dm.events_enriched_v`.
|
||||||
Единственный источник — `dds.event_v`. Зерно — **событие**: каждой строке
|
Единственный источник: `dds.event_v`. Каждой строке источника должна
|
||||||
источника соответствует одна строка витрины.
|
соответствовать ровно одна строка витрины: одно событие.
|
||||||
|
|
||||||
Физическая таблица, таблица-донор и задача в `dm_transform` не нужны.
|
Представление выполняет запрос при чтении и не хранит отдельную копию данных.
|
||||||
Представление не хранит строки, поэтому ключ шардирования и пересборка ему
|
Для него не нужно создавать таблицы или добавлять задачу в `dm_transform`.
|
||||||
не требуются.
|
|
||||||
|
|
||||||
Укажите колонки явно. Иначе новая колонка источника попадет в витрину
|
Укажите колонки явно. Иначе новая колонка источника попадет в витрину
|
||||||
автоматически, и договор с потребителем изменится без вашего участия.
|
автоматически. Отчет получит ее, даже если вы не собирались менять его набор
|
||||||
|
полей.
|
||||||
|
|
||||||
| Группа | Колонки |
|
| Группа | Колонки |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -57,9 +58,7 @@ DDS читают из ODS без отдельной загрузки. Эти с
|
|||||||
## Типичная ошибка
|
## Типичная ошибка
|
||||||
|
|
||||||
Не разворачивайте массивы товаров через `ARRAY JOIN`. У события с тремя
|
Не разворачивайте массивы товаров через `ARRAY JOIN`. У события с тремя
|
||||||
позициями получится три строки. Зерно изменится. Витрину с новым зерном на
|
позициями получится три строки вместо одной.
|
||||||
стенде хранят готовым агрегатом — причины описаны в разделе [«Как принято на
|
|
||||||
ступени 1»](README.md#как-принято-на-ступени-1).
|
|
||||||
|
|
||||||
Главная опасность — потеря событий с пустым массивом товаров: `ARRAY JOIN`
|
Главная опасность — потеря событий с пустым массивом товаров: `ARRAY JOIN`
|
||||||
исключает их из результата. Товары есть только в событиях корзины и покупки.
|
исключает их из результата. Товары есть только в событиях корзины и покупки.
|
||||||
@@ -68,9 +67,9 @@ DDS читают из ODS без отдельной загрузки. Эти с
|
|||||||
по-прежнему будет выглядеть таблицей событий.
|
по-прежнему будет выглядеть таблицей событий.
|
||||||
|
|
||||||
Сохраните массивы как в источнике. Для анализа отдельных товарных позиций
|
Сохраните массивы как в источнике. Для анализа отдельных товарных позиций
|
||||||
нужна другая витрина с другим зерном и именем.
|
нужна отдельная витрина, где одна строка соответствует товарной позиции.
|
||||||
|
|
||||||
## Как проверить себя
|
## Проверка результата
|
||||||
|
|
||||||
После применения DDL откройте Airflow по [адресу из README](../../README.md#адреса-и-пароли),
|
После применения DDL откройте Airflow по [адресу из README](../../README.md#адреса-и-пароли),
|
||||||
найдите даг `homework_check` и запустите его вручную. В новом запуске откройте
|
найдите даг `homework_check` и запустите его вручную. В новом запуске откройте
|
||||||
@@ -79,24 +78,18 @@ DDS читают из ODS без отдельной загрузки. Эти с
|
|||||||
расхождениях с источником — дни и примеры несовпадений. Задачи еще не
|
расхождениях с источником — дни и примеры несовпадений. Задачи еще не
|
||||||
созданных витрин будут красными; это ожидаемо и не мешает проверке первой.
|
созданных витрин будут красными; это ожидаемо и не мешает проверке первой.
|
||||||
|
|
||||||
Деловой ключ — `event_id` внутри дня. Получите результат прямо из
|
Выберите день и прочитайте его события из `dds.event_v` и своей витрины.
|
||||||
`dds.event_v` за тот же день. Соедините его с витриной полным внешним
|
Сопоставьте строки по `event_id` через `FULL OUTER JOIN`.
|
||||||
соединением по `event_id`. Посчитайте строки, где ключ отсутствует с одной
|
Найдите события, которые есть только с одной стороны, и события, у которых
|
||||||
из сторон или различаются значения деловых колонок. Расхождений должно быть
|
различаются перенесенные из источника поля. Расхождений должно быть ноль.
|
||||||
ноль.
|
|
||||||
|
|
||||||
Отдельно сверьте `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` нет.
|
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
# Задание 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).
|
||||||
|
|
||||||
## Роль витрины
|
## Дневные показатели страниц
|
||||||
|
|
||||||
По дневным показателям страниц аналитик сравнивает просмотры, посетителей и
|
По дневным показателям страниц аналитик сравнивает просмотры, посетителей и
|
||||||
действия с товарами. Добавление в корзину происходит на карточке товара,
|
действия с товарами. Добавление в корзину происходит на карточке товара,
|
||||||
@@ -19,19 +20,23 @@
|
|||||||
действия происходят на разных страницах, а связи между ними в агрегате нет.
|
действия происходят на разных страницах, а связи между ними в агрегате нет.
|
||||||
Путь человека исследуют по событиям или сессиям.
|
Путь человека исследуют по событиям или сессиям.
|
||||||
|
|
||||||
## Что сделать
|
## Создание витрины
|
||||||
|
|
||||||
Источник — `dds.event_v`. Зерно витрины — **день события × страница**.
|
Читайте события из `dds.event_v`. В результате нужна одна строка на страницу
|
||||||
Читайте DDS напрямую: так выполнение задания не зависит от витрины
|
за день. Например, все просмотры одной карточки товара за один день попадут
|
||||||
задания 1.
|
в одну строку. Витрина задания 1 для этого расчета не нужна.
|
||||||
|
|
||||||
На стенде витрину с новым зерном хранят как готовый агрегат, чтобы не
|
Сохраните рассчитанные показатели в таблице, чтобы отчеты читали готовые
|
||||||
рассчитывать ее заново для каждого запроса. Причины описаны в разделе
|
итоги. Создайте пять объектов:
|
||||||
[«Как принято на ступени 1»](README.md#как-принято-на-ступени-1). Создайте
|
|
||||||
пять объектов:
|
- основные таблицы `dm.top_pages_daily_rep` и `dm.top_pages_daily_dist`;
|
||||||
`dm.top_pages_daily_rep` / `_dist`, донор `_stage_rep` / `_stage_dist` и
|
- промежуточные таблицы `dm.top_pages_daily_stage_rep` и `dm.top_pages_daily_stage_dist`;
|
||||||
публичное представление `dm.top_pages_daily_v`. Ключ партиции — день, он же
|
- представление для чтения `dm.top_pages_daily_v`.
|
||||||
единица пересборки.
|
|
||||||
|
Их назначение и порядок обновления разобраны во вводной:
|
||||||
|
[«Создание и обновление витрин»](README.md#создание-и-обновление-витрин).
|
||||||
|
Разбейте данные на партиции по дням, чтобы можно было заменить результат
|
||||||
|
одного дня целиком.
|
||||||
|
|
||||||
### Состав колонок
|
### Состав колонок
|
||||||
|
|
||||||
@@ -50,25 +55,26 @@
|
|||||||
адрес разделил бы показатели одной страницы на сотни строк. Учитывайте все
|
адрес разделил бы показатели одной страницы на сотни строк. Учитывайте все
|
||||||
три типа событий, а их счетчики вычисляйте через `countIf`.
|
три типа событий, а их счетчики вычисляйте через `countIf`.
|
||||||
|
|
||||||
### Ритм пересборки
|
### Дни для пересчета
|
||||||
|
|
||||||
События прожитого дня больше не меняются. Определяйте объем прогона по
|
События завершившегося модельного дня больше не меняются. Найдите первый
|
||||||
состоянию витрины: от первого несобранного прожитого дня до последнего
|
такой день, которого еще нет в витрине, и рассчитайте все дни от него до
|
||||||
прожитого. Отсчет от последнего собранного дня оставит пропуски после
|
последнего завершившегося. Если начать после последнего дня, который уже
|
||||||
частичного сбоя.
|
есть в витрине, можно пропустить более ранний день, не загруженный из-за сбоя.
|
||||||
|
|
||||||
Как у `revenue_daily`, заполните донор за весь диапазон одной вставкой,
|
Как у `revenue_daily`, запишите расчет за все выбранные дни в промежуточную
|
||||||
затем замените дневные партиции по одной.
|
таблицу одной вставкой. Затем замените дневные партиции основной таблицы по
|
||||||
|
одной. Добавьте запуск этих запросов в `dm_transform`.
|
||||||
|
|
||||||
### Ключ шардирования
|
### Ключ шардирования
|
||||||
|
|
||||||
Выберите ключ самостоятельно. У `revenue_daily` кроме дня одно измерение с
|
Выберите ключ самостоятельно. У `revenue_daily` строки сгруппированы по дню
|
||||||
шестью близкими по объему значениями. Здесь страниц много, и число событий
|
и категории товара; категорий всего шесть. Здесь страниц много, и число
|
||||||
на них сильно различается.
|
событий на них сильно различается.
|
||||||
|
|
||||||
У цели и донора используйте одинаковый ключ: иначе при замене партиции
|
У основной и промежуточной таблиц используйте одинаковый ключ шардирования.
|
||||||
строка потеряется на одном шарде и удвоится на другом. О том, [как устроен
|
Замена партиции не перераспределяет строки между шардами. Подробнее об этом
|
||||||
донор](../architecture/storage.md), читайте в разделе «Раскладка по шардам».
|
в разделе [«Раскладка по шардам»](../architecture/storage.md#раскладка-по-шардам).
|
||||||
|
|
||||||
## Типичная ошибка
|
## Типичная ошибка
|
||||||
|
|
||||||
@@ -82,13 +88,13 @@
|
|||||||
Выбирайте проверку под возможную ошибку. Этот подход описан в
|
Выбирайте проверку под возможную ошибку. Этот подход описан в
|
||||||
[справочнике проверок](../architecture/testing.md).
|
[справочнике проверок](../architecture/testing.md).
|
||||||
|
|
||||||
## Как проверить себя
|
## Проверка результата
|
||||||
|
|
||||||
Деловой ключ — `page_path` внутри дня. Получите прямой агрегат из
|
Рассчитайте те же показатели по `dds.event_v` за выбранный день.
|
||||||
`dds.event_v` за день и соедините его с `dm.top_pages_daily_v` полным
|
Сопоставьте результат со строками `dm.top_pages_daily_v` за этот день
|
||||||
внешним соединением по ключу. Посчитайте строки, где ключ отсутствует с
|
через `FULL OUTER JOIN` по `page_path`. Найдите страницы, которые есть
|
||||||
одной из сторон или различается любой из четырех счетчиков. Расхождений
|
только с одной стороны, и строки, где различается хотя бы один из четырех
|
||||||
должно быть ноль.
|
счетчиков. Расхождений должно быть ноль.
|
||||||
|
|
||||||
Перед полным сравнением проверьте дневные суммы:
|
Перед полным сравнением проверьте дневные суммы:
|
||||||
|
|
||||||
@@ -98,13 +104,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` и убедились, что повторный
|
||||||
не меняет результат.
|
прогон конвейера не меняет результат.
|
||||||
@@ -1,32 +1,36 @@
|
|||||||
# Задание 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).
|
|
||||||
|
|
||||||
## Что сделать
|
## Создание витрины
|
||||||
|
|
||||||
Источник — `dds.session_v`. Зерно — **день сессии × категория устройства**.
|
Читайте готовые сессии из `dds.session_v`. В результате нужна одна строка
|
||||||
Зерно новое, поэтому витрина хранится готовым агрегатом — см. [«Как принято
|
на категорию устройства за день: например, итоги всех визитов с телефонов
|
||||||
на ступени 1»](README.md#как-принято-на-ступени-1). Создайте пять объектов:
|
за один день.
|
||||||
`dm.session_overview_rep` / `_dist`, донор `_stage_rep` / `_stage_dist` и
|
|
||||||
публичное представление `dm.session_overview_v`. Ключ партиции — день.
|
Создайте пять объектов по [общим правилам ступени 1](README.md#создание-и-обновление-витрин):
|
||||||
|
|
||||||
|
- основные таблицы `dm.session_overview_rep` и `dm.session_overview_dist`;
|
||||||
|
- промежуточные таблицы `dm.session_overview_stage_rep` и `dm.session_overview_stage_dist`;
|
||||||
|
- представление для чтения `dm.session_overview_v`.
|
||||||
|
|
||||||
|
Разбейте данные на партиции по дням.
|
||||||
|
|
||||||
### Состав колонок
|
### Состав колонок
|
||||||
|
|
||||||
@@ -50,19 +54,20 @@ DDS, а повторять зерно сессий ради представле
|
|||||||
категорий встречаются `desktop`, `mobile` и `tablet`. Строку для `tv`
|
категорий встречаются `desktop`, `mobile` и `tablet`. Строку для `tv`
|
||||||
создавайте только при появлении таких визитов.
|
создавайте только при появлении таких визитов.
|
||||||
|
|
||||||
Вместо `avg_duration_seconds` храните слагаемые. Среднее из средних по трем
|
Для расчета средней длительности храните общую длительность и число сессий.
|
||||||
устройствам даст неверный ответ за день: число сессий в категориях
|
Готовую среднюю `avg_duration_seconds` сохранять не нужно. Число сессий
|
||||||
различается, поэтому средние нужно взвешивать. Потребитель делит сумму
|
в категориях различается, поэтому среднее из трех средних даст неверный
|
||||||
`duration_seconds_total` на сумму `sessions`. Долю отказов считают так же:
|
ответ за день. Чтобы получить его правильно, разделите сумму
|
||||||
сумму `bounce_sessions` делят на сумму `sessions`, а не усредняют готовые
|
`duration_seconds_total` на сумму `sessions`. Для доли отказов разделите
|
||||||
доли.
|
сумму `bounce_sessions` на сумму `sessions`. Усреднение готовых долей тоже
|
||||||
|
даст неверный результат.
|
||||||
|
|
||||||
### Ритм пересборки
|
### Дни для пересчета
|
||||||
|
|
||||||
Сессии прожитого дня больше не меняются. Как в задании 2, определяйте
|
Сессии завершившегося модельного дня больше не меняются. Как в задании 2,
|
||||||
диапазон от первого несобранного прожитого дня до последнего прожитого.
|
найдите первый завершившийся день, которого еще нет в витрине. Рассчитайте
|
||||||
Соберите его в донор и замените дневные партиции. Добавьте задачу в
|
все дни от него до последнего завершившегося, запишите их в промежуточную
|
||||||
`dm_transform`.
|
таблицу и замените дневные партиции основной. Добавьте задачу в `dm_transform`.
|
||||||
|
|
||||||
### Ключ шардирования
|
### Ключ шардирования
|
||||||
|
|
||||||
@@ -71,18 +76,18 @@ DDS, а повторять зерно сессий ради представле
|
|||||||
`device_category`, сначала посчитайте строки на каждом шарде. Затем
|
`device_category`, сначала посчитайте строки на каждом шарде. Затем
|
||||||
объясните, почему такое распределение вас устраивает.
|
объясните, почему такое распределение вас устраивает.
|
||||||
|
|
||||||
Ключ донора — тот же, что у цели (см. [«Как принято на ступени
|
У основной и промежуточной таблиц должен быть одинаковый ключ шардирования:
|
||||||
1»](README.md#как-принято-на-ступени-1) и раздел «Раскладка по шардам»
|
см. [общие правила ступени 1](README.md#создание-и-обновление-витрин).
|
||||||
[справочника хранилища](../architecture/storage.md)).
|
|
||||||
|
|
||||||
## Почему сумма `visitors` сходится с `dm.daily_traffic_v`
|
## Число посетителей за день
|
||||||
|
|
||||||
Сравните сумму `visitors` по устройствам за день с `dm.daily_traffic_v`. На
|
Сравните сумму `visitors` по устройствам за день с `dm.daily_traffic_v`. На
|
||||||
стенде значения совпадут: устройство приписано куке на всю жизнь, поэтому за
|
стенде значения совпадут: одному `client_id` всегда соответствует одно
|
||||||
день она учитывается только в одной строке. Это свойство мира стенда; см.
|
устройство, поэтому за день он учитывается только в одной категории.
|
||||||
|
Это свойство мира стенда; см.
|
||||||
раздел «Паспорт куки» в [словаре понятий](../../CONTEXT.md).
|
раздел «Паспорт куки» в [словаре понятий](../../CONTEXT.md).
|
||||||
|
|
||||||
Если появятся визиты одной куки с двух устройств, сумма начнет завышать
|
Если появятся визиты одного `client_id` с двух устройств, сумма начнет завышать
|
||||||
аудиторию. Проверки счетчиков по категориям этого не обнаружат. Число
|
аудиторию. Проверки счетчиков по категориям этого не обнаружат. Число
|
||||||
посетителей дня берите из `dm.daily_traffic_v`, где оно рассчитано для дня
|
посетителей дня берите из `dm.daily_traffic_v`, где оно рассчитано для дня
|
||||||
целиком.
|
целиком.
|
||||||
@@ -94,12 +99,13 @@ DDS, а повторять зерно сессий ради представле
|
|||||||
событий совпало с источником, а число сессий завышено, вероятная причина —
|
событий совпало с источником, а число сессий завышено, вероятная причина —
|
||||||
сборка витрины из событий вместо сессий.
|
сборка витрины из событий вместо сессий.
|
||||||
|
|
||||||
## Как проверить себя
|
## Проверка результата
|
||||||
|
|
||||||
Деловой ключ — `device_category` внутри дня. Получите прямой агрегат из
|
Рассчитайте те же показатели по `dds.session_v` за выбранный день.
|
||||||
`dds.session_v` за день. Соедините его с `dm.session_overview_v` полным
|
Сопоставьте результат со строками `dm.session_overview_v` за этот день
|
||||||
внешним соединением по ключу. Посчитайте строки, где ключ отсутствует с
|
через `FULL OUTER JOIN` по `device_category`. Найдите категории, которые
|
||||||
одной из сторон или различается любой счетчик. Расхождений должно быть ноль.
|
есть только с одной стороны, и строки, где различается хотя бы один
|
||||||
|
счетчик. Расхождений должно быть ноль.
|
||||||
|
|
||||||
Еще две проверки дневных итогов:
|
Еще две проверки дневных итогов:
|
||||||
|
|
||||||
@@ -107,12 +113,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`.
|
||||||
@@ -1,29 +1,42 @@
|
|||||||
# Задание 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).
|
||||||
|
|
||||||
## Роль витрины
|
## Ошибки загрузки и их источники
|
||||||
|
|
||||||
Сводку открывают при росте брака в потоке: смотрят, каких ошибок стало
|
Если сообщение не удалось разобрать, стенд записывает его в таблицу ошибок.
|
||||||
больше и сколько строк забраковано. Причину конкретной ошибки исследуют
|
Такие строки дальше называются браком. Вы соберете сводку, по которой можно
|
||||||
в `ods.event_errors_dist` и `ods.order_errors_dist`, где есть сырой текст и
|
увидеть, каких ошибок стало больше и сколько строк забраковано.
|
||||||
координаты доставки.
|
|
||||||
|
|
||||||
## Что сделать
|
Источники сводки:
|
||||||
|
|
||||||
Зерно — **день загрузки × источник × класс брака**. В источнике зерно —
|
- `ods.event_errors_dist`: ошибки разбора событий;
|
||||||
строка брака. Зерно новое, поэтому витрина хранится готовым агрегатом — см.
|
- `ods.order_errors_dist`: ошибки разбора выгрузки заказов.
|
||||||
[«Как принято на ступени 1»](README.md#как-принято-на-ступени-1). Создайте
|
|
||||||
пять объектов: `dm.dq_errors_daily_rep` / `_dist`, донор `_stage_rep` /
|
В этих таблицах есть исходный текст сообщения и сведения о его доставке:
|
||||||
`_stage_dist` и публичное представление `dm.dq_errors_daily_v`. Ключ
|
они пригодятся при поиске причины ошибки. Представлений `_v` у таблиц ошибок
|
||||||
партиции — день загрузки.
|
нет, создавать их для задания не нужно. Чтение локальной таблицы `_rep`
|
||||||
|
даст строки только одного шарда из двух, поэтому используйте `_dist`.
|
||||||
|
|
||||||
|
## Создание сводки
|
||||||
|
|
||||||
|
В результате нужна одна строка на день загрузки, источник и класс ошибки.
|
||||||
|
Например, ошибки `field_invalid` в заказах за один день загрузки попадут
|
||||||
|
в одну строку с их количеством.
|
||||||
|
|
||||||
|
Создайте пять объектов по [общим правилам ступени 1](README.md#создание-и-обновление-витрин):
|
||||||
|
|
||||||
|
- основные таблицы `dm.dq_errors_daily_rep` и `dm.dq_errors_daily_dist`;
|
||||||
|
- промежуточные таблицы `dm.dq_errors_daily_stage_rep` и `dm.dq_errors_daily_stage_dist`;
|
||||||
|
- представление для чтения `dm.dq_errors_daily_v`.
|
||||||
|
|
||||||
|
Разбейте данные на партиции по дням загрузки.
|
||||||
|
|
||||||
### Состав колонок
|
### Состав колонок
|
||||||
|
|
||||||
@@ -42,16 +55,17 @@
|
|||||||
Служебная `_load_ts` витрины описывает сборку сводки; ее дата может
|
Служебная `_load_ts` витрины описывает сборку сводки; ее дата может
|
||||||
отличаться от `load_date`.
|
отличаться от `load_date`.
|
||||||
|
|
||||||
У событий и заказов свои классы брака. `source` входит в зерно, чтобы
|
У событий и заказов свои классы брака. Группируйте строки также по `source`,
|
||||||
одинаковые имена классов с разным смыслом не объединялись в одну строку.
|
чтобы одноименные классы разных источников не объединялись.
|
||||||
|
|
||||||
### Ритм пересборки
|
### Пересчет и хранение итогов
|
||||||
|
|
||||||
Брак хранится месяц по `TTL`, сырые данные — трое суток. Сводка хранится
|
Правило `TTL` удаляет строки брака через месяц, сырые данные через трое суток.
|
||||||
бессрочно: итоги нужны и после удаления исходных строк.
|
Сводку храните бессрочно: итоги нужны и после удаления исходных строк.
|
||||||
|
|
||||||
Получите список дней загрузки, еще имеющихся в источниках. Соберите их в
|
Получите список дней загрузки, еще имеющихся в источниках. Рассчитайте их
|
||||||
донор и замените только эти партиции, сохранив остальные.
|
итоги в промежуточной таблице и замените только эти партиции основной таблицы.
|
||||||
|
Более старые итоги сохраните.
|
||||||
|
|
||||||
В течение текущих суток ошибки еще поступают: пересчитайте этот день на
|
В течение текущих суток ошибки еще поступают: пересчитайте этот день на
|
||||||
следующем прогоне.
|
следующем прогоне.
|
||||||
@@ -61,53 +75,39 @@
|
|||||||
Выберите ключ самостоятельно. Классов брака у событий и заказов вместе
|
Выберите ключ самостоятельно. Классов брака у событий и заказов вместе
|
||||||
меньше десятка. Объясните, насколько существенен перекос при таком объеме.
|
меньше десятка. Объясните, насколько существенен перекос при таком объеме.
|
||||||
|
|
||||||
Ключ донора — тот же, что у цели (см. [«Как принято на ступени
|
У основной и промежуточной таблиц должен быть одинаковый ключ шардирования:
|
||||||
1»](README.md#как-принято-на-ступени-1) и раздел «Раскладка по шардам»
|
см. [общие правила ступени 1](README.md#создание-и-обновление-витрин).
|
||||||
[справочника хранилища](../architecture/storage.md)).
|
|
||||||
|
|
||||||
## Источники: таблицы `_dist`
|
|
||||||
|
|
||||||
Только это задание читает данные не через `_v`: брак нужен для диагностики
|
|
||||||
приема и не входит в бизнес-модель. Публичных представлений у таблиц ошибок
|
|
||||||
нет, создавать их для задания не нужно. Источники:
|
|
||||||
|
|
||||||
- `ods.event_errors_dist` — ошибки разбора событий;
|
|
||||||
- `ods.order_errors_dist` — ошибки разбора слепка заказов.
|
|
||||||
|
|
||||||
Чтение локальной `_rep` даст строки одного шарда из двух: часть брака не
|
|
||||||
попадет в сводку, хотя запрос выполнится без ошибки.
|
|
||||||
|
|
||||||
## Типичная ошибка
|
## Типичная ошибка
|
||||||
|
|
||||||
Если заменить всю таблицу данными из нового донора, исчезнут итоги дней,
|
Если заменить всю сводку новым расчетом, исчезнут итоги дней, за которые
|
||||||
истекших по `TTL` источника. Восстановить их неоткуда.
|
исходные строки уже удалены по `TTL`. Восстановить эти итоги неоткуда.
|
||||||
|
|
||||||
## Как проверить себя
|
## Проверка результата
|
||||||
|
|
||||||
Деловой ключ — `source` и `error_class` внутри `load_date`. Проверяйте дни,
|
Сопоставляйте строки по `load_date`, `source` и `error_class`. Проверяйте дни,
|
||||||
оставшиеся хотя бы в одной таблице ошибок. Если дня за пределами `TTL` уже
|
оставшиеся хотя бы в одной таблице ошибок. Старые итоги, для которых исходные
|
||||||
нет в источнике, это не ошибка: сверять его уже не с чем.
|
строки уже удалены по `TTL`, сверять не с чем.
|
||||||
|
|
||||||
Посчитайте строки прямо в обеих таблицах ошибок и соедините результат с
|
Посчитайте строки прямо в обеих таблицах ошибок и соедините результат с
|
||||||
`dm.dq_errors_daily_v` полным внешним соединением по ключу. Затем посчитайте
|
`dm.dq_errors_daily_v` через `FULL OUTER JOIN` по этим трем полям. Посчитайте
|
||||||
строки, где ключ отсутствует с одной из сторон или различается `error_rows`.
|
строки, которые есть только с одной стороны или у которых различается `error_rows`.
|
||||||
Расхождений должно быть ноль.
|
Расхождений должно быть ноль.
|
||||||
|
|
||||||
Сумма `error_rows` по проверяемым дням равна общему числу строк в
|
Сумма `error_rows` по проверяемым дням равна общему числу строк в
|
||||||
`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`.
|
||||||
@@ -1,31 +1,38 @@
|
|||||||
# Задание 5: `dm.utm_effectiveness`
|
# Задание 5: показатели рекламы, `dm.utm_effectiveness`
|
||||||
|
|
||||||
**Чему учит:** понимать предупреждение в приставке `declared_`: это сумма со
|
Вы подготовите показатели рекламных каналов по событиям сайта. Суммы покупок
|
||||||
слов клиента, а не деньги отчетности. Самостоятельно находить причины
|
в этой витрине будут отличаться от выручки по заказам магазина. В конце
|
||||||
расхождения двух сумм выручки.
|
работы вы сравните их и объясните причины расхождения.
|
||||||
|
|
||||||
## Сначала прочитать
|
## Подготовка
|
||||||
|
|
||||||
- [«Хранилище и его слои»](../handbook/storage-layers.md);
|
- [«Хранилище и его слои»](../handbook/storage-layers.md);
|
||||||
- [«Потоки Airflow и даги модельного времени»](../handbook/airflow-dags.md).
|
- [«Генерация и обработка данных в Airflow»](../handbook/airflow-dags.md).
|
||||||
|
|
||||||
## Роль витрины
|
## Покупки по рекламным каналам
|
||||||
|
|
||||||
Аналитик сравнивает рекламные каналы по дням: трафик, добавления в корзину,
|
Аналитик сравнивает рекламные каналы по дням: трафик, добавления в корзину,
|
||||||
покупки и заявленные клиентом суммы.
|
покупки и их суммы из событий сайта.
|
||||||
|
|
||||||
На стенде поведение и атрибуцию считают по трекеру, деньги — по бэкенду.
|
События сайта показывают действия посетителя и рекламные метки его переходов.
|
||||||
Здесь сделано исключение: UTM-метки есть только в кликстриме, поэтому для
|
Заказы магазина служат источником выручки. Но в заказах нет UTM-меток, поэтому
|
||||||
оценки покупок по меткам используют клиентскую сумму.
|
в этой витрине вы возьмете суммы из событий покупки. Их передает браузер
|
||||||
|
посетителя; дальше они называются клиентскими суммами. Приставка `declared_`
|
||||||
|
в имени показателя напоминает об этом источнике.
|
||||||
|
|
||||||
## Что сделать
|
## Создание витрины
|
||||||
|
|
||||||
Читайте `dds.event_v` напрямую, без соседних витрин DM. Зерно — **день
|
Читайте события из `dds.event_v`, без других витрин DM. В результате нужна
|
||||||
события × `utm_source` × `utm_medium` × `utm_campaign`**. Зерно новое,
|
одна строка на день и сочетание трех меток: `utm_source`, `utm_medium`,
|
||||||
поэтому витрина хранится готовым агрегатом — см. [«Как принято на ступени
|
`utm_campaign`. События одного дня с одинаковыми метками попадают в одну группу.
|
||||||
1»](README.md#как-принято-на-ступени-1). Создайте пять объектов:
|
|
||||||
`dm.utm_effectiveness_rep` / `_dist`, донор `_stage_rep` / `_stage_dist` и
|
Создайте пять объектов по [общим правилам ступени 1](README.md#создание-и-обновление-витрин):
|
||||||
публичное представление `dm.utm_effectiveness_v`. Ключ партиции — день.
|
|
||||||
|
- основные таблицы `dm.utm_effectiveness_rep` и `dm.utm_effectiveness_dist`;
|
||||||
|
- промежуточные таблицы `dm.utm_effectiveness_stage_rep` и `dm.utm_effectiveness_stage_dist`;
|
||||||
|
- представление для чтения `dm.utm_effectiveness_v`.
|
||||||
|
|
||||||
|
Разбейте данные на партиции по дням.
|
||||||
|
|
||||||
### Состав колонок
|
### Состав колонок
|
||||||
|
|
||||||
@@ -44,7 +51,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` используют тот же
|
||||||
индекс.
|
индекс.
|
||||||
|
|
||||||
@@ -55,31 +62,32 @@
|
|||||||
Для `utm_campaign` выбран тип `String`: имен кампаний со временем становится
|
Для `utm_campaign` выбран тип `String`: имен кампаний со временем становится
|
||||||
много, и затраты на словарь `LowCardinality` перестают окупаться.
|
много, и затраты на словарь `LowCardinality` перестают окупаться.
|
||||||
|
|
||||||
### Ритм пересборки
|
### Дни для пересчета
|
||||||
|
|
||||||
События прожитого дня больше не меняются. Как в задании 2, соберите в донор
|
События завершившегося модельного дня больше не меняются. Как в задании 2,
|
||||||
дни от первого несобранного до последнего прожитого и замените дневные
|
найдите первый завершившийся день, которого еще нет в витрине. Рассчитайте
|
||||||
партиции. Добавьте задачу в `dm_transform`.
|
все дни от него до последнего завершившегося, запишите их в промежуточную
|
||||||
|
таблицу и замените дневные партиции основной. Добавьте задачу в `dm_transform`.
|
||||||
|
|
||||||
### Ключ шардирования
|
### Ключ шардирования
|
||||||
|
|
||||||
Выберите ключ самостоятельно. Если берете хеш одной из трех UTM-колонок,
|
Выберите ключ самостоятельно. Если берете хеш одной из трех UTM-колонок,
|
||||||
посчитайте долю строк с ее самым частым значением и обоснуйте выбор.
|
посчитайте долю строк с ее самым частым значением и обоснуйте выбор.
|
||||||
|
|
||||||
Ключ донора — тот же, что у цели (см. [«Как принято на ступени
|
У основной и промежуточной таблиц должен быть одинаковый ключ шардирования:
|
||||||
1»](README.md#как-принято-на-ступени-1) и раздел «Раскладка по шардам»
|
см. [общие правила ступени 1](README.md#создание-и-обновление-витрин).
|
||||||
[справочника хранилища](../architecture/storage.md)).
|
|
||||||
|
|
||||||
## Атрибуция по событию, а не по визиту
|
## Рекламные метки событий
|
||||||
|
|
||||||
Группируйте события по их собственным меткам. Модели «первое касание» и
|
Группируйте события по их собственным меткам. Модели «первое касание» и
|
||||||
«последнее касание» здесь не рассчитываются.
|
«последнее касание» здесь не рассчитываются.
|
||||||
|
|
||||||
В `dds.session_v` записаны UTM первого события визита. Распределение по
|
В `dds.session_v` записаны UTM первого события визита. Распределение по
|
||||||
каналам может отличаться от этой витрины, и это не означает ошибку в
|
каналам может отличаться от этой витрины, и это не означает ошибку в
|
||||||
расчете. В отчете различайте атрибуцию визитов и событий.
|
расчете. Укажите в отчете, что распределяете по рекламным каналам отдельные
|
||||||
|
события. Такое распределение называют атрибуцией.
|
||||||
|
|
||||||
## Прямой трафик остается в витрине
|
## События без рекламных меток
|
||||||
|
|
||||||
Сохраните события без меток: прямой трафик обычно крупнейший канал, и без
|
Сохраните события без меток: прямой трафик обычно крупнейший канал, и без
|
||||||
него доля рекламы будет завышена. Если отфильтровать такие строки, дневные
|
него доля рекламы будет завышена. Если отфильтровать такие строки, дневные
|
||||||
@@ -109,13 +117,13 @@
|
|||||||
причины можно посмотреть в `dm.purchase_vs_orders_v`, где выделены классы
|
причины можно посмотреть в `dm.purchase_vs_orders_v`, где выделены классы
|
||||||
расхождений.
|
расхождений.
|
||||||
|
|
||||||
## Как проверить себя
|
## Проверка результата
|
||||||
|
|
||||||
Деловой ключ — `utm_source`, `utm_medium`, `utm_campaign` внутри дня.
|
Рассчитайте те же показатели по `dds.event_v` за выбранный день.
|
||||||
Получите прямой агрегат из `dds.event_v` за день и соедините его с
|
Сопоставьте результат со строками `dm.utm_effectiveness_v` за этот день
|
||||||
`dm.utm_effectiveness_v` полным внешним соединением по ключу. Посчитайте
|
через `FULL OUTER JOIN` по `utm_source`, `utm_medium` и `utm_campaign`.
|
||||||
строки, где ключ отсутствует с одной из сторон или различается любая из пяти
|
Найдите сочетания меток, которые есть только с одной стороны, и строки,
|
||||||
величин. Расхождений должно быть ноль.
|
где различается хотя бы один из пяти показателей. Расхождений должно быть ноль.
|
||||||
|
|
||||||
Дополнительно сверьте дневные итоги:
|
Дополнительно сверьте дневные итоги:
|
||||||
|
|
||||||
@@ -127,12 +135,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`.
|
||||||
@@ -1,24 +1,27 @@
|
|||||||
# Свой график в Superset
|
# Свой график в Superset
|
||||||
|
|
||||||
**Чему учит:** увидеть, как собственный результат из хранилища становится
|
В этой работе вы построите график по своей витрине и проверите числа
|
||||||
отчетом в BI.
|
на нем SQL-запросом.
|
||||||
|
|
||||||
## Сначала прочитать
|
## Подготовка
|
||||||
|
|
||||||
- [«Немного 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`.
|
||||||
каких устройств чаще уходят после первой страницы. Постройте столбиковый
|
Теперь покажите, с каких устройств чаще уходят после единственного события.
|
||||||
график доли отказов по `device_category` за весь период витрины. Измерение
|
Такие визиты в стенде считаются отказами. Постройте столбиковый график доли
|
||||||
одно — `device_category`, метрика тоже одна.
|
отказов по `device_category` за весь период витрины. Каждый столбик должен
|
||||||
|
показывать долю для одной категории устройств.
|
||||||
|
|
||||||
## Что сделать
|
## Настройка графика
|
||||||
|
|
||||||
Заведите физический датасет в подключении к `ClickHouse`: схема `dm`, объект
|
Добавьте представление как источник данных для графиков. В Superset такой
|
||||||
`session_overview_v`. Назовите по-русски хотя бы две колонки:
|
источник называется физическим датасетом. Выберите подключение `ClickHouse`,
|
||||||
|
схему `dm` и объект `session_overview_v`. Задайте русские подписи хотя бы двум
|
||||||
|
колонкам:
|
||||||
|
|
||||||
- `device_category` — «Категория устройства»;
|
- `device_category` — «Категория устройства»;
|
||||||
- `report_date` — «Дата отчета».
|
- `report_date` — «Дата отчета».
|
||||||
@@ -32,10 +35,10 @@
|
|||||||
| `SQL expression` | `SUM(bounce_sessions) / SUM(sessions)` |
|
| `SQL expression` | `SUM(bounce_sessions) / SUM(sessions)` |
|
||||||
| `D3 format` | `.1%` |
|
| `D3 format` | `.1%` |
|
||||||
|
|
||||||
Доля считается из сумм слагаемых: среднее готовых долей соврет. Поэтому
|
Выражение делит общее число отказов на общее число сессий. Усреднение дневных
|
||||||
метрике нужно это выражение. Сохраните ее в датасете, а не в графике:
|
долей может дать неверный результат: число сессий в разные дни различается.
|
||||||
подписи и формат задаются в датасете, а график их использует. Так во всех
|
Сохраните метрику в датасете, чтобы все будущие графики использовали одно
|
||||||
будущих графиках у метрики будут одинаковые подписи. Это правило описано в
|
выражение, подпись и формат. Это правило описано в
|
||||||
[справочнике Superset](../architecture/superset.md#дерево-импорта).
|
[справочнике Superset](../architecture/superset.md#дерево-импорта).
|
||||||
|
|
||||||
Не используйте вкладку «Custom SQL» конструктора графика. Созданная там
|
Не используйте вкладку «Custom SQL» конструктора графика. Созданная там
|
||||||
@@ -45,10 +48,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)
|
||||||
@@ -56,12 +59,12 @@ FROM dm.session_overview_v;
|
|||||||
```
|
```
|
||||||
|
|
||||||
Стендовые объекты не меняйте. Что переживает `make up`, а что стирает
|
Стендовые объекты не меняйте. Что переживает `make up`, а что стирает
|
||||||
`make clean`, описано в главе [«Немного Superset»](../handbook/superset.md#стендовые-объекты-и-ваши).
|
`make clean`, описано в главе [«Отчеты в Superset»](../handbook/superset.md#готовые-и-собственные-отчеты).
|
||||||
|
|
||||||
## Как проверить себя
|
## Проверка результата
|
||||||
|
|
||||||
График проверяется глазами, поэтому задачи для него в `homework_check` нет.
|
Автоматической проверки этого задания нет. Сверьте числа на графике
|
||||||
В SQL Lab выполните запрос:
|
с результатом запроса в SQL Lab:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT device_category,
|
SELECT device_category,
|
||||||
@@ -76,15 +79,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;
|
||||||
- [ ] стендовые объекты не изменены, дашборд не создан.
|
- [ ] вы сохранили график отдельно, без создания дашборда и изменения
|
||||||
|
стендовых объектов.
|
||||||
@@ -1,35 +1,35 @@
|
|||||||
# Нарежь сессии сам
|
# Сессии из событий
|
||||||
|
|
||||||
**Чему учит:** собрать сессии из событий и сверить их число с внешним
|
Вы самостоятельно объедините события сайта в сессии и сравните их число
|
||||||
эталоном. Найти причину расхождения в отдельных строках.
|
с готовыми визитами источника, обозначенными `VisitID`. Затем найдете
|
||||||
|
события, из-за которых расчеты расходятся.
|
||||||
|
|
||||||
## Сначала прочитать
|
## Подготовка
|
||||||
|
|
||||||
- [Хранилище и его слои, раздел DDS](../handbook/storage-layers.md#dds-язык-бизнеса);
|
- [Хранилище и его слои, раздел DDS](../handbook/storage-layers.md#dds-события-заказы-и-сессии);
|
||||||
- [формат события: Идентификаторы и время](../formats/clickstream-event.md#идентификаторы-и-время).
|
- [формат события: Идентификаторы и время](../formats/clickstream-event.md#идентификаторы-и-время).
|
||||||
|
|
||||||
## Роль сессии
|
## События и визиты
|
||||||
|
|
||||||
Визит, или сессия, объединяет действия посетителя за один заход на сайт.
|
Визит, или сессия, объединяет действия посетителя за один заход на сайт.
|
||||||
По визитам аналитик считает длительность и глубину просмотра.
|
По визитам аналитик считает длительность и глубину просмотра.
|
||||||
В хранилище сессии собираются из отдельных событий.
|
В хранилище сессии собираются из отдельных событий.
|
||||||
Сверьте самостоятельный расчет с `VisitID`, чтобы увидеть, как из событий
|
|
||||||
получается сессия.
|
|
||||||
|
|
||||||
## Что сделать
|
## Расчет сессий
|
||||||
|
|
||||||
### Нарезка
|
### Границы визитов
|
||||||
|
|
||||||
В SQL Lab или DBeaver под `analyst` напишите запрос к `ods.event_v`.
|
В SQL Lab или DBeaver под `analyst` напишите запрос к `ods.event_v`.
|
||||||
Для своей нарезки и эталона возьмите одни и те же строки за все доступные
|
Для своего расчета и эталона возьмите одни и те же строки за все доступные
|
||||||
дни: для расследования понадобятся события соседних дней.
|
дни: для расследования понадобятся события соседних дней.
|
||||||
Выберите `ClientID`, `UTCEventTime`, `EventDate`, `EventType` и `VisitID`.
|
Выберите `ClientID`, `UTCEventTime`, `EventDate`, `EventType` и `VisitID`.
|
||||||
В `dds.event_v` эталонного `VisitID` нет.
|
В `dds.event_v` эталонного `VisitID` нет.
|
||||||
|
|
||||||
Для начала считайте визитом последовательные события одной куки (`ClientID`);
|
`ClientID` хранится в cookie браузера и обозначает посетителя в событиях
|
||||||
пауза дольше 30 минут открывает новый визит.
|
сайта. Для начала считайте визитом последовательные события одного
|
||||||
|
`ClientID`: пауза дольше 30 минут открывает новый визит.
|
||||||
Учитывайте события всех типов. Упорядочите их по `UTCEventTime`.
|
Учитывайте события всех типов. Упорядочите их по `UTCEventTime`.
|
||||||
Присвойте каждому событию номер своей сессии внутри куки.
|
Присвойте каждому событию номер своей сессии внутри `ClientID`.
|
||||||
Подсказка: оконные функции.
|
Подсказка: оконные функции.
|
||||||
|
|
||||||
Днем сессии считайте `EventDate` ее первого события: дата из `UTCEventTime`
|
Днем сессии считайте `EventDate` ее первого события: дата из `UTCEventTime`
|
||||||
@@ -38,7 +38,8 @@
|
|||||||
|
|
||||||
### Сверка с эталоном
|
### Сверка с эталоном
|
||||||
|
|
||||||
В эталоне посчитайте число визитов каждой куки за модельный день:
|
Посчитайте по эталонному `VisitID` число визитов каждого `ClientID`
|
||||||
|
за модельный день:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT ClientID, EventDate, uniqExact(VisitID) AS reference_sessions
|
SELECT ClientID, EventDate, uniqExact(VisitID) AS reference_sessions
|
||||||
@@ -47,43 +48,48 @@ GROUP BY ClientID, EventDate
|
|||||||
ORDER BY EventDate, ClientID;
|
ORDER BY EventDate, ClientID;
|
||||||
```
|
```
|
||||||
|
|
||||||
Посчитайте свои сессии по куке и дню сессии.
|
Посчитайте свои сессии по `ClientID` и дню сессии. Соедините результаты двух
|
||||||
Соедините два готовых агрегата через `FULL OUTER JOIN` по куке и дню.
|
расчетов через `FULL OUTER JOIN` по этим полям. Учтите строки, которые есть
|
||||||
В результате полного соединения останутся ключи обеих сторон: при расхождении
|
только с одной стороны: полное соединение сохранит их в результате.
|
||||||
на вашей стороне ключа может не быть совсем. Учтите такие строки при сравнении.
|
|
||||||
Выведите сводку по дням с числом разошедшихся кук и список этих кук
|
Выведите сводку по дням с числом `ClientID`, для которых расчеты разошлись,
|
||||||
с днем и обоими значениями счета. Общая сумма за день могла бы скрыть
|
и список этих идентификаторов с днем и обоими значениями. Общая сумма за день
|
||||||
взаимную компенсацию ошибок разных кук. По списку видно, у кого искать причину.
|
могла бы скрыть ошибки: для одного посетителя вы посчитали лишнюю сессию,
|
||||||
|
для другого пропустили. По списку видно, чьи события нужно исследовать.
|
||||||
|
|
||||||
Расхождений будет всего несколько. Если их нет, мир прожил мало дней.
|
Расхождений будет всего несколько. Если их нет, мир прожил мало дней.
|
||||||
Прогоните несколько дней через [даги модельного времени](../handbook/airflow-dags.md#даги-мира)
|
Прогоните несколько дней через [даги модельного времени](../handbook/airflow-dags.md#генерация-модельных-дней)
|
||||||
и повторите сверку.
|
и повторите сверку.
|
||||||
|
|
||||||
### Расследование
|
### Поиск причины расхождения
|
||||||
|
|
||||||
Выберите одну разошедшуюся куку. Выведите ее события за два соседних дня
|
Выберите один `ClientID` с расхождением. Выведите его события за два соседних дня
|
||||||
с `UTCEventTime`, `EventDate`, `VisitID` и номером своей сессии.
|
с `UTCEventTime`, `EventDate`, `VisitID` и номером своей сессии.
|
||||||
Сопоставьте сессии через события: общего ключа у двух нарезок нет,
|
Сопоставьте сессии через события: общего ключа у двух расчетов нет,
|
||||||
зато у каждого события есть и `VisitID`, и присвоенный вами номер.
|
зато у каждого события есть и `VisitID`, и присвоенный вами номер.
|
||||||
Найдите два соседних события, между которыми границы визитов разошлись.
|
Найдите два соседних события, между которыми границы визитов разошлись.
|
||||||
Запишите куку, день, время обоих событий и причину одним предложением.
|
Запишите `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.
|
На ступени 4 вы сравните свой результат с готовыми сессиями стенда
|
||||||
|
по `session_id`.
|
||||||
|
|
||||||
## Приемка
|
## Результат работы
|
||||||
|
|
||||||
- [ ] запрос нарезает все события `ods.event_v` за все доступные дни;
|
- [ ] ваш запрос собирает сессии из всех событий `ods.event_v` за все
|
||||||
- [ ] сверка через `FULL OUTER JOIN` дает сводку по дням и список кук;
|
доступные дни;
|
||||||
- [ ] при разборе расхождения указаны кука, день, два события и причина;
|
- [ ] у вас есть сводка расхождений по дням и список `ClientID`, полученные через
|
||||||
|
`FULL OUTER JOIN`;
|
||||||
|
- [ ] вы объяснили причину одного расхождения и записали `ClientID`, день и два
|
||||||
|
события, на которых оно видно;
|
||||||
- [ ] уточненный расчет дает ноль расхождений во всех днях.
|
- [ ] уточненный расчет дает ноль расхождений во всех днях.
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
# Перепиши нарезку стенда
|
# Расчет сессий в стенде
|
||||||
|
|
||||||
**Чему учит:** изменить расчетную сущность внутри готового конвейера и
|
На прошлой ступени вы рассчитали сессии отдельным запросом. Теперь
|
||||||
проверить результат внешним эталоном.
|
перепишете расчет внутри стенда, запустите его через Airflow и проверите
|
||||||
|
результат по исходным событиям.
|
||||||
|
|
||||||
## Сначала прочитать
|
## Подготовка
|
||||||
|
|
||||||
- [задание «Нарежь сессии сам»](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#раскладка-по-шардам).
|
||||||
|
|
||||||
## Граница задания
|
## Файлы для изменения
|
||||||
|
|
||||||
Даг, таблицы сессий и проверка их числа уже созданы.
|
Даг, таблицы сессий и проверка их числа уже созданы.
|
||||||
Перепишите только три файла:
|
Перепишите только три файла:
|
||||||
@@ -19,68 +20,69 @@
|
|||||||
- `sql/dds/session_rebuild.sql`;
|
- `sql/dds/session_rebuild.sql`;
|
||||||
- `sql/dds/session_replace.sql`.
|
- `sql/dds/session_replace.sql`.
|
||||||
|
|
||||||
Удалите из них готовый SQL и соберите свою реализацию. Параметры и порядок
|
Удалите из них готовый SQL и напишите свой. Даг `dds_transform` передаст
|
||||||
запросов заданы в `dds_transform`. DDL задает целевые таблицы и донор. Донор —
|
параметры и выполнит запросы по порядку. Таблицы уже созданы: основные хранят
|
||||||
служебная таблица, где день собирается перед заменой. В
|
готовые сессии, промежуточные нужны для нового расчета перед заменой данных.
|
||||||
`sessions_vs_reference.sql` число сессий сравнивается с `VisitID` из ODS.
|
Запрос `sessions_vs_reference.sql` сравнивает число сессий с `VisitID` из ODS.
|
||||||
|
|
||||||
## Договор с дагом
|
## Выполнение запросов в Airflow
|
||||||
|
|
||||||
Объем прогона — дни, которые соберет один запуск. Его вычисляет первый файл.
|
Объем прогона — дни, которые соберет один запуск. Его вычисляет первый файл.
|
||||||
|
|
||||||
| Файл | Вход | Результат |
|
| Файл | Вход | Результат |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `session_scope.sql` | `{position:UInt16}` — номер первого несыгранного дня | `first_day` и `last_day` |
|
| `session_scope.sql` | `{position:UInt16}` — номер первого еще не прожитого модельного дня | `first_day` и `last_day` |
|
||||||
| `session_rebuild.sql` | `{first_day:Date}`, `{last_day:Date}`, `{load_id:String}` | Сессии в `dds.session_stage_dist` |
|
| `session_rebuild.sql` | `{first_day:Date}`, `{last_day:Date}`, `{load_id:String}` | Сессии в `dds.session_stage_dist` |
|
||||||
| `session_replace.sql` | День в `{{ params.day }}` | Партиция дня в `dds.session_rep` |
|
| `session_replace.sql` | День в `{{ params.day }}` | Партиция дня в `dds.session_rep` |
|
||||||
|
|
||||||
Первый прожитый день, которого еще нет в DDS, становится `first_day`.
|
`first_day` должен быть первым завершившимся модельным днем, которого еще
|
||||||
`last_day` — последний прожитый день. Начало шкалы нужно взять из событий ODS:
|
нет в DDS, а `last_day` последним завершившимся днем. Начальную дату возьмите
|
||||||
`position` считает дни от него. Живой модельный день еще не закончен, поэтому
|
из событий ODS: `position` считает дни от нее. Текущий модельный день еще
|
||||||
сессии за него не собираются.
|
не закончен, поэтому сессии за него не рассчитываются.
|
||||||
|
|
||||||
Если в ODS нет ни одного события, запрос не возвращает строк. Если DDS уже
|
Если в ODS нет ни одного события, запрос не возвращает строк. Если все
|
||||||
догнал мир, запрос возвращает строку с `first_day = NULL`. В этом случае группа
|
завершившиеся дни уже есть в DDS, запрос возвращает строку с `first_day = NULL`.
|
||||||
сессий в Airflow получает состояние `skipped`.
|
Тогда Airflow пропускает группу задач расчета сессий: ее состояние будет `skipped`.
|
||||||
|
|
||||||
`session_rebuild.sql` сначала очищает `dds.session_stage_rep` на всем кластере.
|
`session_rebuild.sql` сначала очищает `dds.session_stage_rep` на всем кластере.
|
||||||
Затем он собирает в `dds.session_stage_dist` все дни отрезка. Добавьте к
|
Затем он записывает в `dds.session_stage_dist` расчет за все выбранные дни. Добавьте к
|
||||||
вставке `SETTINGS distributed_foreground_insert = 1`. Так строки успеют попасть
|
вставке `SETTINGS distributed_foreground_insert = 1`. Так строки успеют попасть
|
||||||
на шарды до замены партиций.
|
на шарды до замены партиций.
|
||||||
|
|
||||||
Цель `session_replace.sql` — локальная таблица `dds.session_rep`, источник —
|
Запрос `session_replace.sql` заменяет партицию в основной `dds.session_rep`
|
||||||
локальный донор `dds.session_stage_rep`. Операция выполняется на всем кластере.
|
готовой партицией из промежуточной `dds.session_stage_rep`.
|
||||||
|
Операция выполняется на всем кластере, на локальных таблицах каждого шарда.
|
||||||
|
|
||||||
Не ставьте точку с запятой в конце `session_scope.sql`. Драйвер добавляет формат
|
Не ставьте точку с запятой в конце `session_scope.sql`. Драйвер добавляет формат
|
||||||
ответа, и точка с запятой превратит строку в мультизапрос, который ClickHouse
|
ответа. С точкой с запятой получится несколько команд, которые ClickHouse
|
||||||
отклонит. День для замены подставляет шаблон Jinja в Airflow. Параметр
|
отклонит. День для замены подставляет шаблон Jinja в Airflow. Параметр
|
||||||
ClickHouse в `ALTER ... PARTITION` здесь не работает.
|
ClickHouse в `ALTER ... PARTITION` здесь не работает.
|
||||||
|
|
||||||
## Нарежьте сессии
|
## Правила расчета сессий
|
||||||
|
|
||||||
Источник сборки — `dds.event_v`. Одна строка результата представляет один
|
Источник сборки — `dds.event_v`. Одна строка результата представляет один
|
||||||
визит. На третьей ступени вы уже нарезали сессии:
|
визит. На третьей ступени вы уже получили правила разделения событий на сессии:
|
||||||
|
|
||||||
- сессия не пересекает модельные сутки;
|
- сессия не пересекает модельные сутки;
|
||||||
- пауза больше 30 минут открывает новую сессию;
|
- пауза больше 30 минут открывает новую сессию;
|
||||||
- в нарезке участвуют события всех типов.
|
- в расчете участвуют события всех типов.
|
||||||
|
|
||||||
Пересборка добавляет требования к повторяемости. При одинаковом времени
|
Повторный расчет должен давать тот же результат. При одинаковом времени
|
||||||
порядок событий задает `event_id`, поэтому полный порядок окон —
|
упорядочивайте события по `event_id`: порядок в оконных функциях должен быть
|
||||||
`(event_time, event_id)`. Один визит должен получать одинаковый `session_id`
|
`(event_time, event_id)`. Один визит должен получать одинаковый `session_id`
|
||||||
при каждом запуске. Тогда его можно сравнить с сохраненным результатом.
|
при каждом запуске.
|
||||||
Вычислите идентификатор как `cityHash64(client_id, min(event_time))`.
|
Вычислите идентификатор как `cityHash64(client_id, min(event_time))`.
|
||||||
|
|
||||||
Состав и типы строки заданы в `dds.session_rep`. Заполните начало, конец и
|
Состав и типы строки заданы в `dds.session_rep`. Заполните начало, конец и
|
||||||
длительность. Посчитайте все события, просмотры, добавления в корзину и
|
длительность. Посчитайте все события, просмотры, добавления в корзину и
|
||||||
покупки. Возьмите входную и выходную страницы, UTM первого события, устройство
|
покупки. Возьмите входную и выходную страницы, UTM первого события, устройство
|
||||||
и город. Служебные колонки тоже входят в договор таблицы.
|
и город. Заполните также служебные колонки из схемы таблицы.
|
||||||
|
|
||||||
Пишите через `dds.session_stage_dist`: место строки определит ключ
|
Пишите через `dds.session_stage_dist`: шард для строки определяется по
|
||||||
`client_id`. При записи прямо в локальную таблицу оно зависело бы от ноды,
|
`client_id`. При записи прямо в локальную таблицу он зависел бы от узла,
|
||||||
которая выполнила запрос.
|
который выполнил запрос.
|
||||||
|
|
||||||
## Запустите свою реализацию
|
## Запуск нового расчета
|
||||||
|
|
||||||
Готовые дни повторно не собираются. Чтобы новый SQL точно выполнился, выберите
|
Готовые дни повторно не собираются. Чтобы новый SQL точно выполнился, выберите
|
||||||
последний собранный день:
|
последний собранный день:
|
||||||
@@ -90,8 +92,8 @@ SELECT max(session_date)
|
|||||||
FROM dds.session_v;
|
FROM dds.session_v;
|
||||||
```
|
```
|
||||||
|
|
||||||
Берите последний день: более ранняя дыра расширит объем до всех последующих
|
Берите последний день: если удалить более ранний, расчет придется повторить
|
||||||
прожитых дней.
|
и за все последующие завершившиеся дни.
|
||||||
|
|
||||||
Сохраните десять строк с идентификатором, границами, длительностью и счетчиками:
|
Сохраните десять строк с идентификатором, границами, длительностью и счетчиками:
|
||||||
|
|
||||||
@@ -113,7 +115,7 @@ ORDER BY client_id, started_at, session_id
|
|||||||
LIMIT 10;
|
LIMIT 10;
|
||||||
```
|
```
|
||||||
|
|
||||||
Создайте в DBeaver подключение к первой ноде по [инструкции из
|
Создайте в DBeaver подключение к первому узлу по [инструкции из
|
||||||
README](../../README.md#подключиться-из-dbeaver). Укажите пользователя `etl` и
|
README](../../README.md#подключиться-из-dbeaver). Укажите пользователя `etl` и
|
||||||
пароль `CLICKHOUSE_ETL_PASSWORD` из `.env`. У этой учетной записи есть право
|
пароль `CLICKHOUSE_ETL_PASSWORD` из `.env`. У этой учетной записи есть право
|
||||||
менять таблицы DDS на кластере. Подставьте найденную дату и удалите только ее
|
менять таблицы DDS на кластере. Подставьте найденную дату и удалите только ее
|
||||||
@@ -128,19 +130,18 @@ DROP PARTITION 'YYYY-MM-DD';
|
|||||||
группу `session`: в ней должны выполниться задачи `scope` и `rebuild`, а также
|
группу `session`: в ней должны выполниться задачи `scope` и `rebuild`, а также
|
||||||
`replace` для выбранного дня. Если после третьей ступени мир ушел вперед,
|
`replace` для выбранного дня. Если после третьей ступени мир ушел вперед,
|
||||||
в группе появятся задачи `replace` и для последующих прожитых дней. Следом
|
в группе появятся задачи `replace` и для последующих прожитых дней. Следом
|
||||||
выполнятся витрины и `dq_check`.
|
обновятся витрины и выполнится `dq_check`.
|
||||||
|
|
||||||
Если прогон дошел до замены, но упал на проверке, ошибочная партиция уже
|
Если прогон дошел до замены, но упал на проверке, ошибочная партиция уже
|
||||||
находится в DDS. После правки снова удалите тот же день перед следующим
|
находится в DDS. После правки снова удалите тот же день перед следующим
|
||||||
запуском. Иначе день останется заполненным, а новая сборка для него не
|
запуском. Иначе день останется заполненным, а новая сборка для него не
|
||||||
выполнится.
|
выполнится.
|
||||||
|
|
||||||
## Как проверить себя
|
## Проверка результата
|
||||||
|
|
||||||
Сначала повторите запрос с десятью строками. `session_id`, границы,
|
Сначала повторите запрос с десятью строками. `session_id`, границы,
|
||||||
длительность и счетчики должны совпасть с сохраненным результатом. Так вы
|
длительность и счетчики должны совпасть с сохраненным результатом.
|
||||||
завершите отложенное с третьей ступени сопоставление со стендовой нарезкой по
|
Это проверит, что ваш расчет восстановил те же сессии с теми же идентификаторами.
|
||||||
`session_id`.
|
|
||||||
|
|
||||||
Задача `dq_check.sessions_vs_reference` должна стать зеленой. Та же проверка
|
Задача `dq_check.sessions_vs_reference` должна стать зеленой. Та же проверка
|
||||||
видна в хранилище:
|
видна в хранилище:
|
||||||
@@ -152,41 +153,41 @@ WHERE check_name = 'sessions_vs_reference'
|
|||||||
ORDER BY data_date;
|
ORDER BY data_date;
|
||||||
```
|
```
|
||||||
|
|
||||||
Эта сверка сравнивает число сессий каждой куки за день с `VisitID`. Нулевой
|
Эта сверка сравнивает число сессий каждого `client_id` за день с `VisitID`.
|
||||||
`failed_rows` не подтверждает границы, страницы и UTM: две ошибки могут
|
При `failed_rows = 0` число сессий совпало, но их границы, страницы и UTM
|
||||||
оставить тот же счет. Возьмите поштучную сверку из третьей ступени. Для одной
|
еще могут различаться. Как на третьей ступени, проверьте отдельные сессии.
|
||||||
куки выбранного дня сопоставьте границы `VisitID` по событиям с границами
|
Для одного `client_id` выбранного дня найдите начало и конец каждого
|
||||||
строк `dds.session_v`. По исходным событиям проверьте страницы, UTM, устройство
|
`VisitID` по событиям и сравните с границами строк `dds.session_v`.
|
||||||
|
По исходным событиям проверьте страницы, UTM, устройство
|
||||||
и город. Страницы и UTM не входят в сохраненные десять строк: готовый SQL
|
и город. Страницы и UTM не входят в сохраненные десять строк: готовый SQL
|
||||||
стенда выбирал их через `argMin` и `argMax` только по `event_time`. При равном
|
стенда выбирал их через `argMin` и `argMax` только по `event_time`. При равном
|
||||||
времени ожидаемое значение определяйте по исходным событиям и полному порядку
|
времени ожидаемое значение определяйте по исходным событиям и полному порядку
|
||||||
`(event_time, event_id)`.
|
`(event_time, event_id)`.
|
||||||
|
|
||||||
Осталась физическая раскладка. Напишите отдельный запрос к
|
Проверьте также, на каком шарде лежит каждая строка. Напишите отдельный
|
||||||
`dds.session_dist`. Для каждой строки он должен:
|
запрос к `dds.session_dist`. Он должен:
|
||||||
|
|
||||||
1. получить фактическую ноду через `hostName()`;
|
1. получить фактический узел строки через `hostName()`;
|
||||||
2. пересчитать ожидаемую ноду по `cityHash64(client_id)`;
|
2. определить ожидаемый узел по `cityHash64(client_id)`;
|
||||||
3. посчитать строки, у которых ноды не совпали.
|
3. посчитать строки, у которых фактический и ожидаемый узлы не совпали.
|
||||||
|
|
||||||
У кластера два равных шарда: остаток 0 соответствует `clickhouse-01`, остаток
|
У кластера два равных шарда: остаток 0 соответствует `clickhouse-01`, остаток
|
||||||
1 — `clickhouse-02`. Сгруппируйте ответ по фактической ноде. На каждой ноде
|
1 — `clickhouse-02`. Сгруппируйте ответ по фактическому узлу. На каждом узле
|
||||||
число строк на чужом шарде должно быть нулевым. Проверка числа сессий читает
|
число строк на чужом шарде должно быть нулевым. Проверка числа сессий читает
|
||||||
Distributed-таблицу и такую ошибку не заметит.
|
Distributed-таблицу и такую ошибку не заметит.
|
||||||
|
|
||||||
После зеленого результата запустите `etl_pipeline` еще раз, не удаляя
|
После зеленого результата запустите `etl_pipeline` еще раз, не удаляя
|
||||||
партицию. Расчет объема должен пропустить группу сессий: новых прожитых дней и
|
партицию. Группа сессий должна быть пропущена: все завершившиеся дни уже
|
||||||
дыр в слое нет.
|
рассчитаны, новых дней и пропусков нет.
|
||||||
|
|
||||||
## Приемка
|
## Результат работы
|
||||||
|
|
||||||
- [ ] переписаны только три файла `sql/dds/session_*.sql`, инфраструктура
|
- [ ] вы переписали три файла `sql/dds/session_*.sql`, сохранив даг,
|
||||||
упражнения не изменена;
|
таблицы и проверку стенда;
|
||||||
- [ ] запуск после удаления партиции восстановил выбранный день и завершил
|
- [ ] ваш расчет восстановил удаленный день, `etl_pipeline` завершился успешно;
|
||||||
`etl_pipeline` успешно;
|
- [ ] вы сверили десять сохраненных строк: `session_id`, границы,
|
||||||
- [ ] сохраненные `session_id`, границы, длительность и счетчики десяти строк
|
длительность и счетчики совпали после пересборки;
|
||||||
совпали после пересборки;
|
- [ ] вы получили ноль расхождений в `sessions_vs_reference` за все
|
||||||
- [ ] `sessions_vs_reference` дает ноль расхождений во всех прожитых днях;
|
прожитые дни и вручную сверили границы и поля сессии одного `client_id`;
|
||||||
- [ ] для одной куки вручную сверены границы и атрибуты сессии;
|
- [ ] ваш запрос с `hostName()` не нашел строк на чужом шарде;
|
||||||
- [ ] пересчет ключа по `hostName()` не нашел строк на чужом шарде;
|
- [ ] повторный прогон без удаления данных пропустил группу сессий.
|
||||||
- [ ] повторный прогон без новой дыры пропустил группу сессий.
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
# Сначала сторож, потом ломай: заказы
|
# Проверки качества заказов
|
||||||
|
|
||||||
**Чему учит:** до рискованного изменения задавать проверке явный договор и
|
Перед изменением обработки заказов вы добавите две проверки качества данных.
|
||||||
испытывать его управляемой ошибкой.
|
Затем намеренно испортите один заказ и убедитесь, что проверки обнаружили
|
||||||
|
ошибки.
|
||||||
|
|
||||||
## Сначала прочитать
|
## Подготовка
|
||||||
|
|
||||||
- [модель заказа DDS](../architecture/dds/order.md);
|
- [модель заказа DDS](../architecture/dds/order.md);
|
||||||
- [дневной конвейер](../architecture/etl.md#объём-прогона);
|
- [дневной конвейер](../architecture/etl.md#объём-прогона);
|
||||||
@@ -13,30 +14,29 @@
|
|||||||
и на ее включение в [`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` для него мало.
|
|
||||||
|
|
||||||
## Два договора качества
|
## Требования к запросам
|
||||||
|
|
||||||
Обе проверки следуют договору `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/`.
|
||||||
|
|
||||||
@@ -44,45 +44,48 @@ Kafka и службы остаются несущей частью упражн
|
|||||||
|
|
||||||
Имя проверки - `order_items_total`. Для каждого заказа из `dds.order_v`
|
Имя проверки - `order_items_total`. Для каждого заказа из `dds.order_v`
|
||||||
посчитайте сумму `item_price * item_quantity` и сравните ее с `items_total`.
|
посчитайте сумму `item_price * item_quantity` и сравните ее с `items_total`.
|
||||||
`reference_rows` считает ключи со стороны `items_total`, `actual_rows` - ключи
|
В `reference_rows` запишите число заказов со стороны `items_total`,
|
||||||
со стороны вычисленной суммы, а `failed_rows` - несовпавшие значения. В
|
в `actual_rows` число заказов со стороны вычисленной суммы, в `failed_rows`
|
||||||
исправном наборе первые два счетчика равны по построению; сам инвариант выражен
|
число заказов, у которых суммы не совпали. На исправных данных первые два
|
||||||
через `failed_rows`. Для диагностики достаточно `order_id`, ожидаемой суммы и
|
счетчика равны, а `failed_rows` равен нулю. Для примеров расхождений
|
||||||
суммы позиций.
|
достаточно `order_id`, значения `items_total` и вычисленной суммы позиций.
|
||||||
|
|
||||||
Проверка относится к типизированному разбору массива `items` на границе
|
Проверка поможет заметить ошибки разбора `items` при переносе заказа
|
||||||
ODS -> DDS. Скидка, доставка и равенство `total` в этот договор не входят.
|
из ODS в DDS. Скидку, доставку и равенство `total` здесь проверять не нужно.
|
||||||
|
|
||||||
### DDS против ODS
|
### Полнота заказов в DDS
|
||||||
|
|
||||||
Имя проверки - `orders_vs_ods`. Источник модели DDS здесь - текущее состояние
|
Имя проверки - `orders_vs_ods`. Источник модели DDS здесь - текущее состояние
|
||||||
`ods.order_v`. Внутреннее состояние генератора в этот договор не входит.
|
`ods.order_v`. С ним вы и сравниваете DDS; обращаться к внутреннему состоянию
|
||||||
Одним запросом соберите обе стороны на зерне дня заказа и сравните:
|
генератора для этой проверки не нужно.
|
||||||
|
Одним запросом рассчитайте итоги каждого дня заказа в обоих слоях и сравните:
|
||||||
|
|
||||||
- число заказов;
|
- число заказов;
|
||||||
- максимальный `updated_at`.
|
- максимальный `updated_at`.
|
||||||
|
|
||||||
День ODS вычисляется в `Europe/Samara`, как `order_date` в DDS. Область
|
День заказа в ODS вычисляйте в `Europe/Samara`, как `order_date` в DDS.
|
||||||
проверки - объединение дней обеих сторон, включая день, который полностью
|
Проверьте все дни, которые есть хотя бы в одном слое: так вы заметите день,
|
||||||
пропал из одного слоя. Перед сравнением приведите времена к одному часовому
|
целиком пропавший из другого. Перед сравнением приведите времена к одному
|
||||||
поясу. `reference_rows` - число заказов ODS, `actual_rows` - число заказов DDS,
|
часовому поясу. `reference_rows` содержит число заказов ODS, `actual_rows`
|
||||||
а `failed_rows` равен единице для дня, где расходится хотя бы одно значение.
|
число заказов DDS. В `failed_rows` запишите 1, если за день расходится
|
||||||
|
хотя бы одно значение, и 0, если оба совпали.
|
||||||
|
|
||||||
Обе стороны распределены по `order_id`. Дневные агрегаты соедините через
|
Обе стороны распределены по `order_id`. Дневные агрегаты соедините через
|
||||||
`GLOBAL FULL OUTER JOIN` с `join_use_nulls = 1`, как в существующих сверках.
|
`GLOBAL FULL OUTER JOIN` с `join_use_nulls = 1`, как в существующих сверках.
|
||||||
|
|
||||||
Если разбор ODS и сборка DDS одинаково потеряют строку, слои останутся равны
|
Если строка потерялась при разборе в ODS, она не попадет и в DDS. Эта проверка
|
||||||
по этой проверке. Общая потеря при разборе проявится в отдельном балансе по
|
не заметит потерю: данные слоев совпадут. В [следующем задании](5-2-order-parsing.md)
|
||||||
`_load_id` из [следующего задания](5-2-order-parsing.md).
|
вы отдельно сравните число исходных строк с числом разобранных заказов
|
||||||
|
и ошибок за один запуск, обозначенный `_load_id`.
|
||||||
|
|
||||||
## Получите красный и зеленый результаты
|
## Проверка на намеренных ошибках
|
||||||
|
|
||||||
Сначала запустите `dq_check` на нетронутых данных. Обе новые задачи должны
|
Сначала запустите `dq_check` на нетронутых данных. Обе новые задачи должны
|
||||||
завершиться успешно, а в `dm.dq_summary_v` у обоих имен не должно быть
|
завершиться успешно, а в `dm.dq_summary_v` у обоих имен не должно быть
|
||||||
нарушенных дней.
|
дней с расхождениями.
|
||||||
|
|
||||||
Выберите оплаченный заказ из последнего слепка с непустым массивом позиций и
|
Выберите оплаченный заказ из последнего слепка с непустым массивом позиций и
|
||||||
сохраните его `order_id` и `order_date`. Подключитесь к первой ноде из DBeaver
|
сохраните его `order_id` и `order_date`. Подключитесь к первому узлу из DBeaver
|
||||||
по [инструкции из README](../../README.md#подключиться-из-dbeaver) под
|
по [инструкции из README](../../README.md#подключиться-из-dbeaver) под
|
||||||
пользователем `etl`. Прибавьте одну копейку к `items_total` этого заказа в
|
пользователем `etl`. Прибавьте одну копейку к `items_total` этого заказа в
|
||||||
локальной таблице `dds.order_rep` на всем кластере:
|
локальной таблице `dds.order_rep` на всем кластере:
|
||||||
@@ -101,7 +104,7 @@ SETTINGS mutations_sync = 2;
|
|||||||
|
|
||||||
Запустите `etl_pipeline`. Текущее окно заказов будет заново собрано из ODS, и
|
Запустите `etl_pipeline`. Текущее окно заказов будет заново собрано из ODS, и
|
||||||
исходная строка вернется в DDS. Повторный `dq_check` должен дать зеленый
|
исходная строка вернется в DDS. Повторный `dq_check` должен дать зеленый
|
||||||
результат для нового сторожа.
|
результат для новой проверки.
|
||||||
|
|
||||||
Теперь удалите тот же заказ из `dds.order_rep` на всем кластере:
|
Теперь удалите тот же заказ из `dds.order_rep` на всем кластере:
|
||||||
|
|
||||||
@@ -117,9 +120,9 @@ SETTINGS mutations_sync = 2;
|
|||||||
Если `orders_vs_ods` не обнаружил удаление, исправьте запрос до следующего
|
Если `orders_vs_ods` не обнаружил удаление, исправьте запрос до следующего
|
||||||
задания.
|
задания.
|
||||||
|
|
||||||
## Как проверить себя
|
## Проверка результата
|
||||||
|
|
||||||
В `dm.dq_summary_v` должны быть зелены обе новые и пять прежних проверок:
|
В `dm.dq_summary_v` обе новые и пять прежних проверок должны быть успешны:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT check_name, sum(failed_rows) AS failed_rows, groupUniqArray(status)
|
SELECT check_name, sum(failed_rows) AS failed_rows, groupUniqArray(status)
|
||||||
@@ -131,18 +134,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 остались прежними.
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
# Перепишите разбор заказов в ODS
|
# Разбор заказов в ODS
|
||||||
|
|
||||||
**Чему учит:** сначала измерить обе ветви разбора на управляемом срезе, затем
|
Вы перепишете разбор заказов в ODS. На четырех пробных сообщениях проверите,
|
||||||
переписать переход и повторить тот же опыт.
|
что правильно заполненный заказ и сообщения с ошибками попадают
|
||||||
|
в соответствующие таблицы без потерь.
|
||||||
|
|
||||||
## Сначала прочитать
|
## Подготовка
|
||||||
|
|
||||||
- [проверки заказов](5-1-order-checks.md);
|
- [проверки заказов](5-1-order-checks.md);
|
||||||
- [прием заказов в ODS](../architecture/orders/ingestion.md);
|
- [прием заказов в ODS](../architecture/orders/ingestion.md);
|
||||||
@@ -11,24 +12,22 @@
|
|||||||
- [`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,
|
||||||
соберите разбор заново и повторите опыт с другим маркером.
|
соберите разбор заново и повторите опыт с другим маркером.
|
||||||
|
|
||||||
Не меняйте устройство `orders_ingest`, DDL, Kafka, таблиц и преобразований
|
Сохраните устройство `orders_ingest`, DDL, Kafka, таблиц и преобразований
|
||||||
DDS. Оба опыта должны закончиться адресной очисткой только своих строк.
|
DDS. После каждого опыта удалите только добавленные вами строки.
|
||||||
|
|
||||||
Перед началом убедитесь, что после [предыдущего задания](5-1-order-checks.md)
|
Перед началом убедитесь, что после [предыдущего задания](5-1-order-checks.md)
|
||||||
все семь DQ остались зелеными. Поставьте `world_live` на паузу, дождитесь
|
все семь проверок качества прошли успешно. Поставьте `world_live` на паузу, дождитесь
|
||||||
активных прогонов и до завершения опыта не запускайте `world_next_day`,
|
активных прогонов и до завершения опыта не запускайте `world_next_day`,
|
||||||
`world_live_day`, `world_initialize` и `world_recreate`. Не запускайте
|
`world_live_day`, `world_initialize` и `world_recreate`. Не запускайте
|
||||||
`etl_pipeline` между
|
`etl_pipeline` между
|
||||||
отправкой пробной пачки и ее очисткой.
|
отправкой пробной пачки и ее очисткой.
|
||||||
|
|
||||||
## Четыре строки для двух ветвей
|
## Пробные сообщения
|
||||||
|
|
||||||
Сделайте четыре сообщения с общим уникальным маркером:
|
Сделайте четыре сообщения с общим уникальным маркером:
|
||||||
|
|
||||||
@@ -39,9 +38,9 @@ DDS. Оба опыта должны закончиться адресной оч
|
|||||||
`field_invalid`.
|
`field_invalid`.
|
||||||
|
|
||||||
`not_an_object` означает, что корень JSON не объект. `keyset_mismatch` означает,
|
`not_an_object` означает, что корень JSON не объект. `keyset_mismatch` означает,
|
||||||
что у объекта отличается точный набор верхнеуровневых ключей. `field_invalid`
|
что у объекта есть лишние верхнеуровневые ключи или не хватает обязательных.
|
||||||
означает, что ключи на месте, но значение не прошло проверку типа, формата или
|
`field_invalid` означает, что ключи на месте, но значение имеет неверный тип,
|
||||||
вместимости.
|
формат или не помещается в заданный тип данных.
|
||||||
|
|
||||||
Соберите строки в `/tmp/<маркер>.jsonl`. Заготовка дает одну годную строку и
|
Соберите строки в `/tmp/<маркер>.jsonl`. Заготовка дает одну годную строку и
|
||||||
по одному примеру каждого класса брака:
|
по одному примеру каждого класса брака:
|
||||||
@@ -72,38 +71,40 @@ 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 \
|
||||||
--bootstrap-server kafka:9092 --topic orders < "$LAB_BATCH"
|
--bootstrap-server kafka:9092 --topic orders < "$LAB_BATCH"
|
||||||
```
|
```
|
||||||
|
|
||||||
Запустите `orders_ingest` и найдите `_load_id` по маркеру в
|
Запустите `orders_ingest`. Найдите свои строки по маркеру в
|
||||||
`stg.orders_raw_dist`. У четырех строк должен быть один идентификатор прогона.
|
`stg.orders_raw_dist` и сохраните их `_load_id`: это идентификатор запуска,
|
||||||
|
который принял сообщения. Он должен быть одинаковым у всех четырех строк.
|
||||||
|
|
||||||
## Зафиксируйте исходный результат
|
## Проверка готового разбора
|
||||||
|
|
||||||
До любого запуска `etl_pipeline` проверьте срез по найденному `_load_id`:
|
До запуска `etl_pipeline` проверьте строки с найденным `_load_id`:
|
||||||
|
|
||||||
- в STG четыре строки;
|
- в STG четыре строки;
|
||||||
- в физической `ods.order_dist` одна годная строка;
|
- в таблице `ods.order_dist` одна годная строка;
|
||||||
- в `ods.order_errors_dist` три строки, по одной каждого класса;
|
- в `ods.order_errors_dist` три строки, по одной каждого класса;
|
||||||
- `STG = годные + брак`;
|
- `STG = годные + брак`;
|
||||||
- текущая строка `ods.order_v` совпадает по деловым полям с результатом
|
- поля заказа в текущей строке `ods.order_v` совпадают с полями из годного
|
||||||
`argMax` по `snapshot_date` из годного сырья среза.
|
исходного сообщения; последнюю версию для сравнения выберите через
|
||||||
|
`argMax` по `snapshot_date`.
|
||||||
|
|
||||||
Счет физической таблицы нужен именно по `_load_id`: представление
|
Считайте строки `ods.order_dist` именно по `_load_id`: представление
|
||||||
`ods.order_v` оставляет текущую версию сущности и не показывает, сколько строк
|
`ods.order_v` оставляет только текущую версию каждого заказа и не показывает,
|
||||||
разобрал запуск. Запрос сверки с `argMax` напишите сами. Эталонные деловые поля
|
сколько строк разобрал запуск. Запрос сверки с `argMax` напишите сами.
|
||||||
стройте только из `raw`; отдельно сравните различающиеся `created_at` и
|
Значения для сравнения извлеките из `raw` пробных сообщений. Отдельно
|
||||||
`updated_at`.
|
сравните `created_at` и `updated_at`: в заготовке они намеренно различаются.
|
||||||
|
|
||||||
Проверьте раскладку годной строки через `hostName()` на `ods.order_dist`.
|
Проверьте раскладку годной строки через `hostName()` на `ods.order_dist`.
|
||||||
Фактическая нода должна совпасть с остатком `cityHash64(order_id) % 2`:
|
Узел, на котором лежит строка, должен соответствовать остатку `cityHash64(order_id) % 2`:
|
||||||
0 - `clickhouse-01`, 1 - `clickhouse-02`.
|
0 - `clickhouse-01`, 1 - `clickhouse-02`.
|
||||||
|
|
||||||
## Уберите пробную пачку
|
## Удаление пробных сообщений
|
||||||
|
|
||||||
Удалите только строки маркера из локальных таблиц:
|
Удалите только строки маркера из локальных таблиц:
|
||||||
|
|
||||||
@@ -123,7 +124,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 они остаются до удаления по
|
||||||
@@ -135,47 +136,47 @@ SETTINGS mutations_sync = 2;
|
|||||||
запустите `etl_pipeline`: пересборка текущего окна удалит заказ из DDS и
|
запустите `etl_pipeline`: пересборка текущего окна удалит заказ из DDS и
|
||||||
обновит витрины.
|
обновит витрины.
|
||||||
|
|
||||||
## Перепишите разбор
|
## Собственный разбор заказов
|
||||||
|
|
||||||
Удалите готовый 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}`;
|
||||||
- каждая строка среза попадает ровно в один результат: годный заказ или брак;
|
- каждая прочитанная строка попадает ровно в один результат: годный заказ или брак;
|
||||||
- произвольный `raw` не останавливает весь разбор исключением;
|
- ошибка в `raw` одной строки не прерывает разбор остальных;
|
||||||
- годные строки записываются в `ods.order_dist`, остальные - в
|
- годные строки записываются в `ods.order_dist`, остальные - в
|
||||||
`ods.order_errors_dist`;
|
`ods.order_errors_dist`;
|
||||||
- классы брака и их порядок - `not_an_object`, `keyset_mismatch`,
|
- классы брака и их порядок - `not_an_object`, `keyset_mismatch`,
|
||||||
`field_invalid`;
|
`field_invalid`;
|
||||||
- метки доставки и `_load_id` переходят из STG без пересчета;
|
- метки доставки и `_load_id` переходят из STG без пересчета;
|
||||||
- задача завершается после доставки обоих результатов через Distributed, а
|
- задача завершается после записи и заказов, и ошибок на шарды через
|
||||||
строки распределяются по ключу целевой таблицы.
|
Distributed; строки распределяются по ключу соответствующей целевой таблицы.
|
||||||
|
|
||||||
Точный набор полей, типы и граница строгого приема описаны в
|
Точный набор полей, типы и условия проверки значений описаны в
|
||||||
[документе приема](../architecture/orders/ingestion.md#граница-строгого-приёма),
|
[документе приема](../architecture/orders/ingestion.md#граница-строгого-приёма),
|
||||||
а целевые колонки - в [`20-ods-tables.sql`](../../sql/ddl/20-ods-tables.sql).
|
а целевые колонки - в [`20-ods-tables.sql`](../../sql/ddl/20-ods-tables.sql).
|
||||||
Внутренность `items` остается сырым JSON: ее разбирает DDS.
|
Содержимое `items` сохраните как JSON: его разбирает DDS.
|
||||||
|
|
||||||
Повторите опыт с новым маркером. Получите тот же баланс 4 = 1 + 3, те же три
|
Повторите опыт с новым маркером. Получите тот же баланс 4 = 1 + 3, те же три
|
||||||
класса, верную текущую строку по `snapshot_date`, разные времена и правильный
|
класса, верную текущую версию заказа по `snapshot_date`, разные значения
|
||||||
шард. Затем очистите второй срез теми же адресными командами и запустите
|
`created_at` и `updated_at` и правильный шард. Затем удалите строки второго
|
||||||
|
опыта теми же командами и запустите
|
||||||
`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 находятся в зеленом состоянии.
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
# Перепишите сборку заказов в DDS
|
# Сборка заказов в DDS
|
||||||
|
|
||||||
**Чему учит:** переписать пересборку заказов по состоянию слоев и доказать ее
|
Вы перепишете загрузку заказов из ODS в DDS и выбор дней для пересчета.
|
||||||
результат на пропущенном закрытом дне.
|
Затем удалите данные за один из прошлых дней и проверите, что ваша
|
||||||
|
загрузка восстановила их.
|
||||||
|
|
||||||
## Сначала прочитать
|
## Подготовка
|
||||||
|
|
||||||
- [проверки заказов](5-1-order-checks.md);
|
- [проверки заказов](5-1-order-checks.md);
|
||||||
- [разбор заказов в ODS](5-2-order-parsing.md);
|
- [разбор заказов в ODS](5-2-order-parsing.md);
|
||||||
@@ -11,7 +12,7 @@
|
|||||||
- [дневной конвейер](../architecture/etl.md#объём-прогона);
|
- [дневной конвейер](../architecture/etl.md#объём-прогона);
|
||||||
- [раскладку по шардам](../architecture/storage.md#раскладка-по-шардам).
|
- [раскладку по шардам](../architecture/storage.md#раскладка-по-шардам).
|
||||||
|
|
||||||
## Граница задания
|
## Файлы для изменения
|
||||||
|
|
||||||
Удалите готовый SQL и перепишите только три файла:
|
Удалите готовый SQL и перепишите только три файла:
|
||||||
|
|
||||||
@@ -19,18 +20,18 @@
|
|||||||
- `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 дают зеленый результат.
|
задания удалены, а семь проверок качества дают зеленый результат.
|
||||||
|
|
||||||
Поставьте `world_live` на паузу и дождитесь завершения активных прогонов. До
|
Поставьте `world_live` на паузу и дождитесь завершения активных прогонов. До
|
||||||
завершения опыта не запускайте `world_live_day`, `world_initialize` и
|
завершения опыта не запускайте `world_live_day`, `world_initialize` и
|
||||||
`world_recreate`. `world_next_day` понадобится только в явно указанном ниже
|
`world_recreate`. `world_next_day` понадобится только в явно указанном ниже
|
||||||
случае.
|
случае.
|
||||||
|
|
||||||
## Договор с дагом
|
## Выполнение запросов в Airflow
|
||||||
|
|
||||||
Сохраните интерфейс `dds_transform`:
|
Даг `dds_transform` ожидает от файлов следующие параметры и результаты:
|
||||||
|
|
||||||
| Файл | Вход | Результат |
|
| Файл | Вход | Результат |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -38,39 +39,46 @@
|
|||||||
| `order_rebuild.sql` | `{first_day:Date}`, `{last_day:Date}`, `{load_id:String}` | выбранные дни в `dds.order_stage_dist` |
|
| `order_rebuild.sql` | `{first_day:Date}`, `{last_day:Date}`, `{load_id:String}` | выбранные дни в `dds.order_stage_dist` |
|
||||||
| `order_replace.sql` | день в `{{ params.day }}` | партиция дня в `dds.order_rep` |
|
| `order_replace.sql` | день в `{{ params.day }}` | партиция дня в `dds.order_rep` |
|
||||||
|
|
||||||
Объем состоит из текущего окна источника и хвоста от первого дня, который есть
|
Сначала определите дни, за которые заказы еще могут измениться. Возьмите
|
||||||
в ODS, но отсутствует в DDS. Текущее окно дают строки последнего
|
строки с последним `snapshot_date` и найдите минимальную и максимальную
|
||||||
`snapshot_date`: их минимальный и максимальный день заказа. Если источник
|
дату заказа. Этот диапазон далее называется текущим окном источника.
|
||||||
пуст, `order_scope.sql` не возвращает строку. Если дыр нет, `first_day` равен
|
|
||||||
началу окна. Если дыра старше, непрерывный отрезок начинается с нее.
|
Затем найдите первый день, который есть в ODS, но отсутствует в DDS.
|
||||||
|
Если он раньше начала текущего окна, начните расчет с него. Иначе начните
|
||||||
|
с начала окна. Полученная дата станет `first_day`, а конец окна `last_day`.
|
||||||
|
Один запуск пересчитывает все дни между ними. Если источник пуст,
|
||||||
|
`order_scope.sql` не возвращает строку.
|
||||||
|
|
||||||
`order_rebuild.sql` сначала очищает все партиции `dds.order_stage_rep` на
|
`order_rebuild.sql` сначала очищает все партиции `dds.order_stage_rep` на
|
||||||
кластере, затем записывает в `dds.order_stage_dist` весь выбранный отрезок. Он
|
кластере, затем записывает в `dds.order_stage_dist` весь выбранный отрезок. Он
|
||||||
читает текущее состояние `ods.order_v`, вычисляет `order_date`, переводит
|
читает текущее состояние `ods.order_v`, вычисляет `order_date`, переводит
|
||||||
времена в `Europe/Samara`, разбирает `items` в три параллельных массива и
|
времена в `Europe/Samara`, разбирает `items` в три параллельных массива и
|
||||||
ставит координаты запуска.
|
заполняет служебные поля запуска.
|
||||||
|
|
||||||
Запись через Distributed должна завершиться до замены и использовать ключ
|
Дождитесь записи всех строк на шарды через Distributed перед заменой партиций.
|
||||||
целевой таблицы для раскладки строк. Способ собрать весь отрезок внутри файла
|
Строки должны распределяться по ключу целевой таблицы. Способ рассчитать
|
||||||
выберите сами.
|
выбранные дни внутри файла выберите сами.
|
||||||
|
|
||||||
`order_replace.sql` работает с локальными `dds.order_rep` и
|
`order_replace.sql` работает с локальными `dds.order_rep` и
|
||||||
`dds.order_stage_rep` на всем кластере. `REPLACE PARTITION` переносит готовую
|
`dds.order_stage_rep` на всем кластере. `REPLACE PARTITION` заменяет дневную
|
||||||
дневную партицию из донора и оставляет донор на месте. Структура, ключи и
|
партицию основной таблицы готовой партицией из промежуточной. Промежуточная
|
||||||
политика хранения совпадают: DDL объявляет донор через `AS dds.order_rep`.
|
партиция при этом сохраняется. Структура, ключи и политика хранения таблиц
|
||||||
|
совпадают: DDL создает промежуточную через `AS dds.order_rep`.
|
||||||
|
|
||||||
## Восстановите ближайший закрытый день
|
## Восстановление пропущенного дня
|
||||||
|
|
||||||
Найдите последний по дате день DDS, который лежит левее начала текущего окна
|
Найдите самый поздний день DDS перед началом текущего окна ODS. Заказы за этот
|
||||||
ODS. Это ближайший к окну закрытый день: его удаление показывает расширение
|
день уже не меняются, поэтому он считается закрытым. Удалив его, вы проверите,
|
||||||
объема на дыру без пересборки всей истории.
|
что ваш запрос заметил пропуск и включил этот день в расчет.
|
||||||
|
|
||||||
Если такого дня еще нет, один раз продвиньте мир через `world_next_day`,
|
Если такого дня еще нет, один раз продвиньте мир через `world_next_day`,
|
||||||
дождитесь `orders_ingest`, затем запустите `etl_pipeline`. После этого снова
|
дождитесь `orders_ingest`, затем запустите `etl_pipeline`. После этого снова
|
||||||
зафиксируйте модельное время до конца опыта и выберите ближайший закрытый день.
|
оставьте модельное время неизменным до конца опыта и выберите этот день.
|
||||||
|
|
||||||
Сохраните для него число заказов и контрольную сумму деловых полей без
|
Сохраните для него число заказов и контрольную сумму их полей, исключив
|
||||||
`_load_id` и `_load_ts`. Удалите только его партицию:
|
служебные `_load_id` и `_load_ts`. Контрольная сумма позволит проверить,
|
||||||
|
что после восстановления значения остались прежними. Удалите только
|
||||||
|
партицию выбранного дня:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
ALTER TABLE dds.order_rep ON CLUSTER clickstream_cluster
|
ALTER TABLE dds.order_rep ON CLUSTER clickstream_cluster
|
||||||
@@ -90,35 +98,35 @@ DROP PARTITION 'YYYY-MM-DD';
|
|||||||
прогона число заказов и контрольная сумма выбранного дня должны совпасть с
|
прогона число заказов и контрольная сумма выбранного дня должны совпасть с
|
||||||
сохраненными.
|
сохраненными.
|
||||||
|
|
||||||
## Проверьте раскладку и повтор
|
## Распределение по шардам и повторный запуск
|
||||||
|
|
||||||
Напишите отдельный запрос к `dds.order_dist`, который получает фактическую
|
Напишите отдельный запрос к `dds.order_dist`, который получает фактический
|
||||||
ноду через `hostName()`, вычисляет ожидаемую по `cityHash64(order_id) % 2` и
|
узел каждой строки через `hostName()`, вычисляет ожидаемый по
|
||||||
считает несовпадения по каждой ноде. Остаток 0 соответствует
|
`cityHash64(order_id) % 2` и считает несовпадения по каждому узлу.
|
||||||
`clickhouse-01`, остаток 1 - `clickhouse-02`. На обеих нодах число строк на
|
Остаток 0 соответствует `clickhouse-01`, остаток 1 - `clickhouse-02`.
|
||||||
чужом шарде должно быть нулевым. Счетные DQ читают Distributed-таблицы и такую
|
На обоих узлах число строк на чужом шарде должно быть нулевым.
|
||||||
ошибку раскладки не обнаруживают.
|
Проверки количества строк читают данные со всех шардов через Distributed
|
||||||
|
и такую ошибку не обнаруживают.
|
||||||
|
|
||||||
Запустите `etl_pipeline` еще раз при неизменном модельном времени. Группа
|
Запустите `etl_pipeline` еще раз при неизменном модельном времени. Группа
|
||||||
заказов снова должна пересобрать текущее изменяемое окно. Контрольные суммы
|
заказов снова должна пересобрать текущее изменяемое окно. Контрольные суммы
|
||||||
деловых полей остаются прежними, а все семь проверок дают зеленый результат.
|
полей заказов остаются прежними, а все семь проверок дают зеленый результат.
|
||||||
|
|
||||||
## Когда можно остановиться
|
## Завершение работы
|
||||||
|
|
||||||
Удаленная партиция восстановлена, ее число строк и контрольная сумма совпали
|
После восстановления партиции и успешных проверок снимите `world_live`
|
||||||
с исходными. На чужих шардах строк нет, повторный прогон не изменил деловые
|
с паузы, если он работал до упражнения.
|
||||||
данные, семь DQ зелены. Если до упражнения `world_live` работал, теперь его
|
|
||||||
можно снять с паузы.
|
|
||||||
|
|
||||||
## Приемка
|
## Результат работы
|
||||||
|
|
||||||
- [ ] переписаны только три файла `sql/dds/order_*.sql`, инфраструктура
|
- [ ] вы переписали три файла `sql/dds/order_*.sql`, сохранив остальные
|
||||||
упражнения не изменена;
|
части стенда;
|
||||||
- [ ] удален ближайший к окну закрытый день, а красный 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` поля заказов остались прежними, проверки прошли.
|
||||||
+102
-91
@@ -1,9 +1,9 @@
|
|||||||
# Разберите сырые события рядом со стендом
|
# Разбор сырых событий
|
||||||
|
|
||||||
**Чему учит:** пакетно разобрать сообщения отдельно от живого приема и
|
В этой работе вы разберете сохраненные сообщения о событиях в свою таблицу.
|
||||||
построчно проверить свой результат по нетронутому ODS.
|
Каждую полученную строку сравните с результатом обработки в стенде.
|
||||||
|
|
||||||
## Сначала прочитать
|
## Подготовка
|
||||||
|
|
||||||
- [формат события](../formats/clickstream-event.md);
|
- [формат события](../formats/clickstream-event.md);
|
||||||
- [справочник хранилища](../architecture/storage.md), разделы про имена
|
- [справочник хранилища](../architecture/storage.md), разделы про имена
|
||||||
@@ -15,26 +15,26 @@
|
|||||||
Готовый стендовый разбор находится в `sql/ddl/30-ods-views.sql`. Откройте его
|
Готовый стендовый разбор находится в `sql/ddl/30-ods-views.sql`. Откройте его
|
||||||
после собственной сверки: в нем лежит ответ на основную часть задания.
|
после собственной сверки: в нем лежит ответ на основную часть задания.
|
||||||
|
|
||||||
## Граница задания
|
## Порядок работы
|
||||||
|
|
||||||
Создайте в базе `default` одну свою таблицу годных событий и наполните ее одним
|
Создайте в базе `default` свою таблицу для правильно заполненных событий.
|
||||||
пакетным `INSERT SELECT` из `stg.hits_raw_dist`. Работайте с первой нодой под
|
Выберите небольшой набор сообщений из `stg.hits_raw_dist`, разберите их
|
||||||
пользователем `etl`.
|
и загрузите в таблицу одним `INSERT SELECT`. Работайте с первым узлом под
|
||||||
|
пользователем `etl`. Затем сравните результат с событиями в ODS.
|
||||||
|
|
||||||
Матвью `ods.event_mv` и `ods.event_errors_mv` продолжают принимать живой
|
Материализованные представления `ods.event_mv` и `ods.event_errors_mv`
|
||||||
поток. Оставьте их, STG, ODS, Kafka и даги без изменений. Пересоздание матвью
|
продолжают обрабатывать поступающие сообщения. Оставьте их, STG, ODS, Kafka
|
||||||
на работающем стенде оставило бы зазор в приеме, а для пакетного опыта оно не
|
и даги без изменений. Если пересоздать представления на работающем стенде,
|
||||||
нужно.
|
часть сообщений может остаться без обработки.
|
||||||
|
|
||||||
Дайте своей таблице имя, которое не пересечется с чужой работой, и запишите
|
Дайте своей таблице имя, которое не пересечется с чужой работой, и запишите
|
||||||
его вместе с запросами. В конце вы удалите только эту таблицу.
|
его вместе с запросами.
|
||||||
|
|
||||||
## Выберите сырой срез
|
## Выбор исходных сообщений
|
||||||
|
|
||||||
STG хранит строки примерно три дня по `_load_ts`. Эта колонка отмечает время
|
STG хранит строки примерно три дня по `_load_ts`. Эта колонка отмечает время
|
||||||
поступления в хранилище. Модельный день события лежит в `EventDate`. Сначала
|
поступления в хранилище. Модельный день события лежит в `EventDate`.
|
||||||
найдите среди недавних данных завершившийся интервал со строками. Посмотрите
|
Посмотрите, сколько строк поступило за каждую из последних минут:
|
||||||
счет по минутам:
|
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT
|
SELECT
|
||||||
@@ -48,7 +48,8 @@ ORDER BY minute DESC;
|
|||||||
```
|
```
|
||||||
|
|
||||||
Выберите короткий интервал `[slice_from, slice_to)`, который уже закончился и
|
Выберите короткий интервал `[slice_from, slice_to)`, который уже закончился и
|
||||||
дает удобное для построчной проверки число строк. Если одна минута велика,
|
дает удобное для построчной проверки число строк. Этот набор строк дальше
|
||||||
|
называется срезом. Если за одну минуту пришло слишком много строк,
|
||||||
сузьте ее точными границами `_load_ts`. Если недавних строк нет, откройте
|
сузьте ее точными границами `_load_ts`. Если недавних строк нет, откройте
|
||||||
Airflow. При включенном `world_live` посмотрите его текущий запуск и дочерний
|
Airflow. При включенном `world_live` посмотрите его текущий запуск и дочерний
|
||||||
`world_live_day`: дождитесь свежих строк от текущего или следующего планового
|
`world_live_day`: дождитесь свежих строк от текущего или следующего планового
|
||||||
@@ -64,29 +65,28 @@ Airflow. При включенном `world_live` посмотрите его т
|
|||||||
|
|
||||||
Во всех дальнейших запросах читайте `stg.hits_raw_dist` только с этими
|
Во всех дальнейших запросах читайте `stg.hits_raw_dist` только с этими
|
||||||
границами. Новые строки с более поздним `_load_ts` в срез не попадут. Повторите
|
границами. Новые строки с более поздним `_load_ts` в срез не попадут. Повторите
|
||||||
опыт до очистки по TTL; после нее прежний срез уже не восстановить из ODS.
|
опыт до удаления исходных строк по TTL; после него прежний срез уже не восстановить из ODS.
|
||||||
|
|
||||||
## Соберите собственную таблицу
|
## Таблица для результата
|
||||||
|
|
||||||
Таблица содержит все 47 полей источника с именами и типами
|
Таблица содержит все 47 полей источника с именами и типами
|
||||||
`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). Но `ReplacingMergeTree`
|
||||||
шире: в `ReplacingMergeTree` версиями считаются строки с одинаковым полным
|
считает версиями строки, у которых совпадает весь ключ сортировки.
|
||||||
ключом сортировки. Используйте его при загрузке и сверке.
|
Поэтому для загрузки и сверки используйте все четыре части ключа ODS,
|
||||||
|
перечисленные выше.
|
||||||
|
|
||||||
## Разберите годные строки
|
## Разбор сообщений
|
||||||
|
|
||||||
Для каждой строки среза примите решение по тому же строгому входу, что и
|
Для каждой строки среза проверьте те же условия приема, что и на стенде:
|
||||||
стенд:
|
|
||||||
|
|
||||||
1. корень `raw` — JSON-объект;
|
1. корень `raw` — JSON-объект;
|
||||||
2. набор верхнеуровневых ключей в точности равен 47 ключам формата;
|
2. набор верхнеуровневых ключей в точности равен 47 ключам формата;
|
||||||
@@ -94,121 +94,132 @@ 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`.
|
||||||
|
|
||||||
Свою сторону тоже читайте с `FINAL`. Ожидаемый счет равен числу различных
|
Свою таблицу тоже читайте с `FINAL`. Число строк должно совпасть с числом
|
||||||
полных ключей среди годных доставок. Разница между числом доставок и этим
|
различных полных ключей среди годных исходных сообщений. Разница между числом
|
||||||
счетом показывает повторы в исходном срезе.
|
годных доставок и этим числом показывает повторы в исходном срезе.
|
||||||
|
|
||||||
Область эталона постройте независимо от своего предиката и результата вставки.
|
### События ODS для сравнения
|
||||||
Снова возьмите весь исходный STG-срез. По координатам Kafka исключите доставки,
|
|
||||||
которые есть в `ods.event_errors_dist`. У оставшихся доставок извлеките
|
Теперь нужно найти в ODS события из выбранного среза. Список стройте по
|
||||||
|
исходным доставкам: если взять его из своей таблицы, ошибочно отвергнутое
|
||||||
|
событие исчезнет с обеих сторон сверки и потеря останется незамеченной.
|
||||||
|
|
||||||
|
Снова возьмите все выбранные строки STG. По координатам Kafka исключите
|
||||||
|
доставки, которые есть в `ods.event_errors_dist`. У оставшихся извлеките
|
||||||
`WatchID` без риска прервать запрос и выберите эти события из свежего
|
`WatchID` без риска прервать запрос и выберите эти события из свежего
|
||||||
`ods.event_v`.
|
`ods.event_v`. Свое условие отбора здесь не применяйте: решение о годности
|
||||||
|
доставки вы берете из результата стендового разбора.
|
||||||
|
|
||||||
Свяжите сырую доставку с ODS по `WatchID`: по договору генератора это уникальный
|
`WatchID` связывает доставку с событием в ODS. Для удаления повторов и
|
||||||
идентификатор события. Он задает только область эталона. Дедупликация и
|
сравнения полей дальше используйте полный физический ключ из схемы таблицы.
|
||||||
последующее соединение используют полный физический ключ. Не берите область из
|
|
||||||
своей таблицы и не применяйте к ней свой предикат. Иначе ошибочно отвергнутая
|
|
||||||
строка исчезнет с обеих сторон сверки.
|
|
||||||
|
|
||||||
## Сверьте 47 полей
|
## Сверка полей
|
||||||
|
|
||||||
Соедините свою таблицу и выбранные строки `ods.event_v` через `FULL OUTER
|
Соедините свою таблицу и выбранные строки `ods.event_v` через `FULL OUTER
|
||||||
JOIN` по полному ключу замены. Полное соединение сохраняет строки,
|
JOIN` по полному ключу замены. Полное соединение сохраняет строки,
|
||||||
которых нет на одной из сторон. Разделите результат на три исхода:
|
которых нет на одной из сторон. Найдите три вида расхождений:
|
||||||
|
|
||||||
- строка есть только в ODS — вы ее потеряли;
|
- строка есть только в ODS — вы ее потеряли;
|
||||||
- строка есть только у вас — вы приняли лишнее или иначе разобрали часть
|
- строка есть только у вас — вы приняли лишнее или иначе разобрали часть
|
||||||
ключа;
|
ключа;
|
||||||
- ключ есть с обеих сторон, но различается хотя бы одно из 47 полей.
|
- ключ есть с обеих сторон, но различается хотя бы одно из 47 полей.
|
||||||
|
|
||||||
ClickHouse по умолчанию заполняет отсутствующую сторону значениями типов.
|
ClickHouse по умолчанию заполняет отсутствующую сторону значениями по умолчанию
|
||||||
|
для соответствующих типов, например нулем или пустой строкой.
|
||||||
Включите для сверки `join_use_nulls = 1` или добавьте явный признак присутствия
|
Включите для сверки `join_use_nulls = 1` или добавьте явный признак присутствия
|
||||||
в каждую сторону. Не определяйте отсутствие по значениям полей события.
|
в каждую сторону. Не определяйте отсутствие по значениям полей события.
|
||||||
|
|
||||||
Сравните все поля явно, включая массивы, `ecommerce` и точное десятичное
|
Сравните все поля явно, включая массивы, `ecommerce` и точное десятичное
|
||||||
`purchaseRevenue`. Удобный вид диагностики выберите сами: важно, чтобы по
|
`purchaseRevenue`. Выберите, как показать расхождения, чтобы было видно,
|
||||||
результату можно было назвать отличающиеся колонки одной строки. Выведите число
|
какие колонки различаются в конкретной строке. Выведите число строк каждого
|
||||||
строк каждого из трех исходов. В верном разборе все три числа равны нулю.
|
из трех видов расхождений. В верном разборе все три числа равны нулю.
|
||||||
|
|
||||||
`_load_ts` не входит в 47 полей источника и не проверяет JSON-разбор.
|
`_load_ts` не входит в 47 полей источника и не проверяет JSON-разбор.
|
||||||
Проверьте его отдельно. Свяжите каждую строку своей таблицы с меткой одной из
|
Проверьте его отдельно. Для каждой строки своей таблицы найдите самую позднюю
|
||||||
доставок того же полного ключа в исходном срезе. После `FINAL` остается самая
|
метку доставки того же полного ключа в исходном срезе. Она должна совпасть
|
||||||
поздняя метка этого ключа внутри среза. С ODS ее не сравнивайте: там может
|
с `_load_ts`, который вы читаете после `FINAL`. С ODS эту метку не сравнивайте: там может
|
||||||
храниться более поздняя доставка того же события из-за границ среза.
|
храниться более поздняя доставка того же события из-за границ среза.
|
||||||
|
|
||||||
## Расследуйте расхождение
|
## Поиск ошибок разбора
|
||||||
|
|
||||||
Если сверка не пуста, возьмите одну строку. Вернитесь к независимому списку
|
Если нашлись расхождения, выберите одну строку. Найдите ее ключ в списке,
|
||||||
ключей и исходному `raw`, выведите рядом координаты Kafka и `_load_ts`.
|
который вы получили из STG, и исходное сообщение `raw`. Выведите рядом
|
||||||
|
координаты Kafka и `_load_ts`.
|
||||||
|
|
||||||
Для измененной строки назовите поля, которые расходятся, и сравните каждое с
|
Для измененной строки назовите поля, которые расходятся, и сравните каждое с
|
||||||
сырой строкой. Для пропавшей или лишней строки проверьте пять опорных полей и
|
сырой строкой. Для пропавшей или лишней строки проверьте пять опорных полей и
|
||||||
решение строгого предиката. Если координаты доставки есть в
|
результат условия отбора. Если координаты доставки есть в
|
||||||
`ods.event_errors_dist`, посмотрите `error_class`.
|
`ods.event_errors_dist`, посмотрите `error_class`.
|
||||||
|
|
||||||
Исправьте разбор, удалите и заново создайте только собственную таблицу, затем
|
Исправьте разбор, удалите и заново создайте только собственную таблицу, затем
|
||||||
повторите вставку того же среза. Остановитесь, когда счет исходных доставок по
|
повторите вставку того же среза. Остановитесь, когда число исходных доставок по
|
||||||
прежним границам не изменился, баланс строгого входа сошелся, а построчная
|
прежним границам не изменилось, число годных и отвергнутых строк в сумме дает
|
||||||
сверка пуста.
|
число исходных доставок, а построчная сверка не находит расхождений.
|
||||||
|
|
||||||
## Где заканчивается проверка
|
## Ограничения проверки
|
||||||
|
|
||||||
`ods.event_v` — образец для сравнения. Оба разбора могли повторить одну
|
`ods.event_v` — образец для сравнения. Оба разбора могли повторить одну
|
||||||
ошибку. Для пяти опорных полей у вас есть проверка разбираемости и исходный
|
ошибку. Для пяти опорных полей у вас есть проверка преобразования в нужный тип
|
||||||
JSON. Остальные 42 поля можно вручную сопоставить с `raw`, однако второго
|
и исходный JSON. Остальные 42 поля можно вручную сопоставить с `raw`, однако
|
||||||
независимого источника для всех значений на стенде нет.
|
второго независимого источника для всех значений на стенде нет.
|
||||||
|
|
||||||
## Уберите свой объект
|
## Сохранение результата и удаление таблицы
|
||||||
|
|
||||||
Сохраните DDL таблицы, запрос разбора, границы среза, баланс строгого входа и
|
Сохраните DDL таблицы, запрос разбора, границы среза, число исходных, годных
|
||||||
пустую итоговую сверку. Затем удалите свою таблицу командой `DROP TABLE` с ее
|
и отвергнутых строк и итоговую сверку без расхождений. Запишите также, чего
|
||||||
точным именем. Убедитесь через `system.tables`, что объекта больше нет.
|
не подтверждает сравнение с ODS и как TTL ограничивает время для повторения
|
||||||
Прикладные строки стенда вы не меняли, поэтому удалять данные из STG, ODS или
|
опыта. Затем удалите свою таблицу командой `DROP TABLE` с ее точным именем.
|
||||||
|
Убедитесь через `system.tables`, что объекта больше нет.
|
||||||
|
Данные стенда вы не меняли, поэтому удалять строки из 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 и срок повторения опыта;
|
||||||
- [ ] собственная таблица удалена, объекты и данные стенда не изменены.
|
- [ ] после опыта вы удалили свою таблицу, сохранив объекты и данные стенда.
|
||||||
+43
-40
@@ -1,9 +1,10 @@
|
|||||||
# Мониторинг: куда смотреть
|
# Мониторинг: куда смотреть
|
||||||
|
|
||||||
**Чему учит:** начать расследование с подходящего экрана, найти источник
|
Вы познакомитесь с дашбордами мониторинга и проведете два расследования:
|
||||||
нагрузки и объяснить свежесть данных через путь доставки.
|
найдете запросы, которые создают нагрузку, и выясните, почему события сайта
|
||||||
|
и заказы обновляются с разной частотой.
|
||||||
|
|
||||||
## Перед началом
|
## Знакомство с дашбордами
|
||||||
|
|
||||||
Откройте Grafana и Airflow по адресам из раздела [«Адреса и
|
Откройте Grafana и Airflow по адресам из раздела [«Адреса и
|
||||||
пароли»](../../README.md#адреса-и-пароли). Для первого знакомства в Grafana
|
пароли»](../../README.md#адреса-и-пароли). Для первого знакомства в Grafana
|
||||||
@@ -11,7 +12,7 @@
|
|||||||
|
|
||||||
- «Данные» — путь сообщений от источника до хранилища;
|
- «Данные» — путь сообщений от источника до хранилища;
|
||||||
- «Кластер» — процессор, память, диски и внутренняя работа ClickHouse;
|
- «Кластер» — процессор, память, диски и внутренняя работа ClickHouse;
|
||||||
- «Запросы» — выполненные запросы, их стоимость и ошибки.
|
- «Запросы» — выполненные запросы, затраченные на них ресурсы и ошибки.
|
||||||
|
|
||||||
Откройте все три дашборда и запишите по одному вопросу, на который каждый из
|
Откройте все три дашборда и запишите по одному вопросу, на который каждый из
|
||||||
них помогает ответить. «Данные» оставьте открытым в соседней вкладке.
|
них помогает ответить. «Данные» оставьте открытым в соседней вкладке.
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
Проведите два расследования на работающем стенде. Службы останавливать и данные
|
Проведите два расследования на работающем стенде. Службы останавливать и данные
|
||||||
портить не понадобится.
|
портить не понадобится.
|
||||||
|
|
||||||
## Расследование 1. Кто читает больше всех
|
## Расследование 1. Какие запросы читают больше всего данных
|
||||||
|
|
||||||
Начните в спокойный момент: не запускайте одновременно свои тяжелые запросы.
|
Начните в спокойный момент: не запускайте одновременно свои тяжелые запросы.
|
||||||
Откройте дашборд «Запросы». На панели «Тяжёлые запросы: что прочитало больше
|
Откройте дашборд «Запросы». На панели «Тяжёлые запросы: что прочитало больше
|
||||||
@@ -27,8 +28,8 @@
|
|||||||
|
|
||||||
Сохраните:
|
Сохраните:
|
||||||
|
|
||||||
- границы окна и время запроса;
|
- начало и конец выбранного периода, время запроса;
|
||||||
- место запроса в списке, пользователя и ноду;
|
- место запроса в списке, пользователя и узел;
|
||||||
- текст запроса;
|
- текст запроса;
|
||||||
- длительность, память, число прочитанных и отданных строк.
|
- длительность, память, число прочитанных и отданных строк.
|
||||||
|
|
||||||
@@ -44,22 +45,25 @@
|
|||||||
В меню панели «Кто спрашивает: сколько запросов и какой у них аппетит»
|
В меню панели «Кто спрашивает: сколько запросов и какой у них аппетит»
|
||||||
откройте Inspect, затем Query. Ответьте на два вопроса:
|
откройте Inspect, затем Query. Ответьте на два вопроса:
|
||||||
|
|
||||||
1. Почему журнал читают сразу с обеих нод?
|
1. Почему журнал читают сразу с обоих узлов?
|
||||||
2. Почему из счета исключены строки начала запроса и работа шардов?
|
2. Почему при подсчете запросов исключают записи о начале выполнения и
|
||||||
|
записи о выполнении частей запроса на шардах?
|
||||||
|
|
||||||
Проверьте второе объяснение по панели ошибок: ее фильтр не исключает записи
|
Проверьте второе объяснение по панели ошибок: ее фильтр не исключает записи
|
||||||
шардов. Объясните, почему различаются вопросы панелей, а не только их SQL.
|
шардов. Объясните, на какие разные вопросы отвечают эти две панели
|
||||||
|
и зачем им разные условия отбора.
|
||||||
|
|
||||||
## Расследование 2. Как меняется свежесть заказов
|
## Расследование 2. Как меняется свежесть заказов
|
||||||
|
|
||||||
Откройте дашборд «Данные». Панель «Свежесть: сколько прошло с последней
|
Откройте дашборд «Данные». Панель «Свежесть: сколько прошло с последней
|
||||||
доехавшей строки» показывает текущее число, а не историю. До наблюдения
|
доехавшей строки» показывает, сколько времени прошло с последней загрузки.
|
||||||
нарисуйте ожидаемую форму свежести событий и заказов в течение одного живого
|
Истории значений на ней нет. До наблюдения нарисуйте, как, по вашему ожиданию,
|
||||||
дня. Отдельно отметьте момент доставки слепка заказов.
|
будут меняться эти числа для событий и заказов в течение одного запуска
|
||||||
|
`world_live_day`. Отдельно отметьте ожидаемый момент доставки слепка заказов.
|
||||||
|
|
||||||
Если `world_live` снят с паузы, наблюдайте его текущий или следующий
|
Если `world_live` снят с паузы, наблюдайте его текущий или следующий
|
||||||
`world_live_day`: новый ручной запуск встанет в очередь. Если выключатель на
|
`world_live_day`: новый ручной запуск встанет в очередь. Если `world_live` на
|
||||||
паузе и активного живого дня нет, запустите один `world_live_day` вручную.
|
паузе и активного `world_live_day` нет, запустите один `world_live_day` вручную.
|
||||||
Модельные сутки занимают около 24 реальных минут.
|
Модельные сутки занимают около 24 реальных минут.
|
||||||
|
|
||||||
Запишите несколько наблюдений до конца дня и сразу после него:
|
Запишите несколько наблюдений до конца дня и сразу после него:
|
||||||
@@ -69,29 +73,29 @@
|
|||||||
| | | | |
|
| | | | |
|
||||||
|
|
||||||
Дашборд обновляется раз в минуту, поэтому точного совпадения момента
|
Дашборд обновляется раз в минуту, поэтому точного совпадения момента
|
||||||
доставки со строкой таблицы ждать не нужно. Рост и сброс свежести заказов
|
доставки с записью наблюдения ждать не нужно. Перенесите рост и сброс
|
||||||
перенесите на свой набросок. Объясните форму линии: какую метку времени
|
показателя заказов на свой рисунок. Объясните форму линии: какую метку времени
|
||||||
вычитает панель из текущего времени, почему число растет и какое событие его
|
вычитает панель из текущего времени, почему число растет и какое событие его
|
||||||
сбрасывает. Модельную дату сыгранного дня подпишите отдельно от реального
|
сбрасывает. Модельную дату наблюдаемого дня подпишите отдельно от реального
|
||||||
времени наблюдения.
|
времени наблюдения.
|
||||||
|
|
||||||
Подтвердите сброс двумя следами:
|
Подтвердите, что сброс совпал с загрузкой заказов:
|
||||||
|
|
||||||
- в Airflow завершился прием слепка `orders_ingest`;
|
- в Airflow завершился прием слепка `orders_ingest`;
|
||||||
- на панели «Дневной слепок заказов: какой доехал, когда и после какой паузы»
|
- на панели «Дневной слепок заказов: какой доехал, когда и после какой паузы»
|
||||||
появилась новая верхняя строка.
|
появилась новая верхняя строка.
|
||||||
|
|
||||||
Если вы начали после доставки, дождитесь следующего дня либо честно запишите,
|
Если вы начали после доставки, дождитесь следующего дня либо запишите,
|
||||||
какую часть формы увидели. Одно текущее значение не доказывает ни рост, ни
|
какую часть изменений успели увидеть. Одно текущее значение не доказывает ни рост, ни
|
||||||
сброс.
|
сброс.
|
||||||
|
|
||||||
## Объясните путь данных
|
## Источники показателей
|
||||||
|
|
||||||
Не открывая справочник, объясните своими словами:
|
Не открывая справочник, объясните своими словами:
|
||||||
|
|
||||||
- почему свежесть считают в ODS, а поток — в STG;
|
- почему время с последней загрузки считают по ODS, а число поступивших сообщений по STG;
|
||||||
- почему знаменатель доли брака берут из STG;
|
- почему знаменатель доли брака берут из STG;
|
||||||
- почему у числителя и знаменателя доли брака одно окно.
|
- почему числитель и знаменатель доли брака берут за один период.
|
||||||
|
|
||||||
Свяжите ответы с тем, что вы видели на панелях. Затем сверьте объяснение с
|
Свяжите ответы с тем, что вы видели на панелях. Затем сверьте объяснение с
|
||||||
[главой о мониторинге](../handbook/monitoring.md) и справочниками дашбордов
|
[главой о мониторинге](../handbook/monitoring.md) и справочниками дашбордов
|
||||||
@@ -99,21 +103,20 @@
|
|||||||
[«Данные»](../architecture/monitoring/data.md). Устройство живого дня и
|
[«Данные»](../architecture/monitoring/data.md). Устройство живого дня и
|
||||||
доставки слепка описано в [главе об Airflow](../handbook/airflow-dags.md).
|
доставки слепка описано в [главе об Airflow](../handbook/airflow-dags.md).
|
||||||
|
|
||||||
## Приемка
|
## Результат работы
|
||||||
|
|
||||||
- [ ] для каждого из трех дашбордов записан вопрос, на который он отвечает;
|
- [ ] для каждого из трех дашбордов записан вопрос, на который он отвечает;
|
||||||
- [ ] сохранены окно, текст и показатели верхнего запроса, определены его
|
- [ ] у вас сохранены выбранный период, текст и показатели верхнего запроса;
|
||||||
источник и назначение;
|
вы выяснили, кто его отправил и зачем;
|
||||||
- [ ] среди запросов открытых дашбордов найден тот, который прочитал больше
|
- [ ] вы нашли запрос открытых дашбордов, который прочитал больше всего
|
||||||
всего строк, и соответствующая панель;
|
строк, и панель, которая его отправила;
|
||||||
- [ ] объяснено, как журнал собирают с двух нод и какие записи учитывают для
|
- [ ] вы можете объяснить, зачем журнал собирают с двух узлов и какие записи
|
||||||
счета запросов;
|
учитывают при подсчете запросов;
|
||||||
- [ ] до наблюдения нарисован прогноз свежести событий и заказов, а форма
|
- [ ] у вас есть прогноз свежести, сделанный до наблюдения, и несколько
|
||||||
линии объяснена через метку последней доставки;
|
замеров; рост и сброс на рисунке объяснены через время последней доставки;
|
||||||
- [ ] записаны несколько наблюдений, на наброске видны рост и сброс свежести
|
- [ ] вы подтвердили доставку заказов в Airflow и на панели слепков,
|
||||||
заказов, а доставка подтверждена в Airflow и на панели слепков;
|
подписали модельную дату отдельно от реального времени наблюдений;
|
||||||
- [ ] модельная дата отделена от реального времени наблюдения;
|
- [ ] вы можете объяснить выбор слоев для свежести, потока и доли брака
|
||||||
- [ ] объяснен выбор слоев для свежести, потока и доли брака, а также общее
|
и почему числитель и знаменатель берут за один период;
|
||||||
окно числителя и знаменателя;
|
- [ ] после наблюдений службы продолжают работать, данные и стендовые
|
||||||
- [ ] службы не остановлены, данные вручную не правились, стендовые дашборды
|
дашборды остались без изменений.
|
||||||
не изменены.
|
|
||||||
+153
-108
@@ -1,84 +1,121 @@
|
|||||||
# Учебные задания
|
# Учебные задания
|
||||||
|
|
||||||
Программа состоит из двух частей: основная включает семь ступеней, которые вы
|
Вы будете достраивать хранилище небольшого интернет-магазина. Стенд уже
|
||||||
проходите по порядку, а факультативы выбираете по желанию. Причины такого
|
загружает события сайта и заказы, обрабатывает их и строит несколько отчетов.
|
||||||
порядка и формы заданий описаны в [программе учебных лаб](../specs/2026-09-01-labs-program.md#2-устройство-программы-основная-программа-и-факультативы).
|
Сначала вы подготовите данные для новых отчетов, затем разберетесь, как
|
||||||
|
работают расчеты и загрузка, и перепишете часть из них.
|
||||||
|
|
||||||
## Основная программа
|
Основная программа состоит из семи ступеней. Проходите их по порядку,
|
||||||
|
факультативы выбирайте по желанию. Перед первым заданием прочитайте главу
|
||||||
|
[«Хранилище и его слои»](../handbook/storage-layers.md) и выполните запросы
|
||||||
|
из нее: они покажут данные, с которыми предстоит работать.
|
||||||
|
|
||||||
| Ступень | Задание | Чему учит |
|
## Порядок заданий
|
||||||
|
|
||||||
|
| Ступень | Задания | Содержание |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 1 | Пять витрин слоя DM | построить результат для потребителя хранилища; уроки каждой витрины — в таблице ниже |
|
| 1 | Пять витрин для отчетов | подготовить данные о событиях, страницах, визитах, ошибках загрузки и рекламе; задания перечислены ниже |
|
||||||
| 2 | [Свой график в Superset](2-1-superset-chart.md) | превратить свой результат из хранилища в отчет в BI |
|
| 2 | [Свой график в Superset](2-1-superset-chart.md) | построить график по своей витрине и проверить числа на нем |
|
||||||
| 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) | сохранить итоги в таблице и настроить их обновление через Airflow |
|
||||||
| 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) | различать суммы покупок из событий сайта и выручку по заказам магазина |
|
||||||
|
|
||||||
Общее устройство слоя — [дока DM](../architecture/dm.md). Она отвечает на
|
Начните с [первого задания](1-1-events-enriched.md): для него достаточно
|
||||||
вопросы «почему так», задание отвечает на вопрос «что сделать».
|
одного представления. В заданиях 2–5 понадобятся таблицы и их обновление
|
||||||
|
по дням. К общим правилам ниже возвращайтесь по мере работы.
|
||||||
|
|
||||||
## Как принято на ступени 1
|
## Создание и обновление витрин
|
||||||
|
|
||||||
**Читать — через `_v`.** Физические `_rep` и `_dist` служат загрузке и
|
### Источники данных
|
||||||
обслуживанию, а публичный договор DDS — `dds.event_v` или `dds.session_v` —
|
|
||||||
остается прежним даже при изменении физических объектов. Чтение DDS
|
|
||||||
напрямую делает задания независимыми: соседняя витрина может быть еще не
|
|
||||||
построена.
|
|
||||||
Исключение — задание 4: у таблиц брака публичного договора нет.
|
|
||||||
|
|
||||||
**Писать — в `_dist`.** Так строки распределяются по ключу шардирования. При
|
События читайте из `dds.event_v`, сессии из `dds.session_v`. Суффикс `_v`
|
||||||
записи в локальную таблицу распределение зависит от узла, выполнившего
|
обозначает обычное представление: оно выполняет сохраненный запрос при
|
||||||
запрос, и при следующем запуске может измениться. Подробности — раздел
|
чтении. Такие представления сохраняют привычные имена и состав колонок,
|
||||||
«Раскладка по шардам» [справочника хранилища](../architecture/storage.md).
|
когда внутреннее устройство таблиц меняется.
|
||||||
|
|
||||||
**Форма — по зерну.** На стенде агрегат с новым зерном принято хранить
|
Каждая учебная витрина читает готовый источник и не зависит от других
|
||||||
готовым: потребитель читает его много раз без повторного расчета по
|
учебных витрин. В задании 4 источниками служат таблицы ошибок ODS:
|
||||||
источнику. Создайте `dm.<имя>_rep` / `_dist`, донор `dm.<имя>_stage_rep` /
|
представлений для них нет.
|
||||||
`_stage_dist` и публичное `dm.<имя>_v` с явным списком колонок. При прежнем
|
|
||||||
зерне достаточно представления: таблица дублировала бы данные. Доводы —
|
|
||||||
раздел «Таблица или представление» [справочника
|
|
||||||
DM](../architecture/dm.md).
|
|
||||||
|
|
||||||
**Ключ шардирования — ваше решение.** Объясните, почему выбранный ключ не
|
### Таблицы и представления
|
||||||
дает существенного перекоса и почему более сложный ничего не добавит.
|
|
||||||
Объявите донор через `AS <цель>_rep` с ключом цели: иначе замена партиции
|
В заданиях 2–5 вы группируете исходные строки. Например, в `top_pages_daily`
|
||||||
потеряет строку на одном шарде и удвоит на другом. Детали — раздел
|
одна строка содержит итоги для одной страницы за день. То, чему соответствует
|
||||||
«Раскладка по шардам» [справочника хранилища](../architecture/storage.md).
|
одна строка, называют **зерном**. На стенде итоги с новым зерном сохраняют
|
||||||
Пример обоснования — `dm.revenue_daily_dist` в
|
в таблицах. Отчеты читают готовые числа.
|
||||||
|
|
||||||
|
Новый расчет сначала записывают в промежуточную таблицу. Пока он готовится,
|
||||||
|
отчеты читают прежние данные из основной. Затем готовыми разделами
|
||||||
|
промежуточной таблицы заменяют разделы основной. Раздел называется
|
||||||
|
**партицией**; в этих заданиях он содержит данные одного дня.
|
||||||
|
Промежуточную таблицу также называют **донором**.
|
||||||
|
|
||||||
|
На примере `top_pages_daily` пять объектов выглядят так:
|
||||||
|
|
||||||
|
| Объект | Для чего нужен |
|
||||||
|
|---|---|
|
||||||
|
| `dm.top_pages_daily_rep` | Хранит рассчитанные строки на каждом шарде |
|
||||||
|
| `dm.top_pages_daily_dist` | Читает строки со всех шардов и распределяет запись между ними |
|
||||||
|
| `dm.top_pages_daily_stage_rep` | Хранит новый расчет до замены данных в основной таблице |
|
||||||
|
| `dm.top_pages_daily_stage_dist` | Распределяет запись нового расчета между шардами |
|
||||||
|
| `dm.top_pages_daily_v` | Дает отчетам доступ к готовой витрине; явно перечисляет ее колонки |
|
||||||
|
|
||||||
|
### Распределение по шардам
|
||||||
|
|
||||||
|
Каждый из двух шардов стенда хранит свою часть данных. Записывайте строки
|
||||||
|
через таблицы `_dist`: они выбирают шард по ключу шардирования. Прямая запись
|
||||||
|
в `_rep` оставит строки на том узле, где выполнен запрос.
|
||||||
|
|
||||||
|
Ключ для своей витрины выберите сами. Проверьте распределение строк между
|
||||||
|
шардами и объясните, почему выбранного ключа достаточно.
|
||||||
|
Пример обоснования для `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`.
|
Создайте промежуточную таблицу через `AS <цель>_rep`, сохранив структуру и
|
||||||
При создании мира он применяется после стендовых файлов.
|
ключи основной. У обеих таблиц `_dist` должен быть одинаковый ключ
|
||||||
В уже работающем стенде сохранение файла и повторный `make up` новый DDL
|
шардирования: замена партиции выполняется внутри каждого шарда и не
|
||||||
не применят — выполните его сами, как описано ниже. В
|
перераспределяет строки. Подробности есть в разделе
|
||||||
`sql/dm/<витрина>_scope.sql` определяются дни прогона, в `_rebuild.sql` они
|
[«Раскладка по шардам»](../architecture/storage.md#раскладка-по-шардам).
|
||||||
собираются в донор, в `_replace.sql` заменяется партиция. Задачу сборки
|
|
||||||
добавьте в [`dags/dm_transform.py`](../../dags/dm_transform.py): без нее
|
|
||||||
конвейер не обновит витрину. Представлению достаточно `CREATE VIEW`.
|
|
||||||
|
|
||||||
**Образец — витрина `revenue_daily`.** У нее есть пять объектов, донор,
|
### SQL-файлы и запуск расчета
|
||||||
сборка, замена дневных партиций и задача в конвейере. Используйте ее как
|
|
||||||
образец всего порядка пересборки. Состав колонок берите из своего ТЗ, иначе
|
|
||||||
получите показатели другой витрины.
|
|
||||||
|
|
||||||
## Как применить свой DDL
|
Команды создания таблиц и представлений (DDL) сохраняйте в
|
||||||
|
`sql/ddl/70-labs.sql`. При создании мира этот файл выполняется после
|
||||||
|
стендовых файлов. В работающем стенде примените команды сами по инструкции
|
||||||
|
ниже: сохранение файла и повторный `make up` их не выполнят.
|
||||||
|
|
||||||
|
Для витрины с таблицами нужны три файла в `sql/dm/`:
|
||||||
|
|
||||||
|
- `<витрина>_scope.sql` определяет, какие дни пересчитать;
|
||||||
|
- `<витрина>_rebuild.sql` рассчитывает их и записывает в промежуточную таблицу;
|
||||||
|
- `<витрина>_replace.sql` заменяет партицию одного дня в основной таблице.
|
||||||
|
|
||||||
|
Добавьте запуск расчета в [`dags/dm_transform.py`](../../dags/dm_transform.py),
|
||||||
|
чтобы витрина обновлялась вместе с остальными. Для представления из задания 1
|
||||||
|
достаточно выполнить `CREATE VIEW`.
|
||||||
|
|
||||||
|
Образец этого порядка работы есть у готовой витрины `revenue_daily`.
|
||||||
|
Ее таблицы, SQL-файлы и задача Airflow разобраны в
|
||||||
|
[справочнике DM](../architecture/dm.md#dmrevenue_daily_v).
|
||||||
|
Состав колонок и выбор дней для пересчета берите из своего задания.
|
||||||
|
|
||||||
|
## Выполнение SQL в DBeaver
|
||||||
|
|
||||||
В DBeaver создайте подключение по [инструкции в README](../../README.md#подключиться-из-dbeaver),
|
В DBeaver создайте подключение по [инструкции в README](../../README.md#подключиться-из-dbeaver),
|
||||||
но укажите пользователя `etl` и пароль из `CLICKHOUSE_ETL_PASSWORD` в `.env`.
|
но укажите пользователя `etl` и пароль из `CLICKHOUSE_ETL_PASSWORD` в `.env`.
|
||||||
@@ -88,73 +125,81 @@ DM](../architecture/dm.md).
|
|||||||
Создайте файл `sql/ddl/70-labs.sql` и сохраняйте в нем DDL своих витрин.
|
Создайте файл `sql/ddl/70-labs.sql` и сохраняйте в нем DDL своих витрин.
|
||||||
Откройте его в SQL-редакторе этого подключения и выполните DDL текущего
|
Откройте его в SQL-редакторе этого подключения и выполните DDL текущего
|
||||||
задания. Создавайте объекты с
|
задания. Создавайте объекты с
|
||||||
`ON CLUSTER clickstream_cluster`, чтобы они появились на обеих нодах;
|
`ON CLUSTER clickstream_cluster`, чтобы они появились на обоих узлах;
|
||||||
образец для представления — [`sql/ddl/60-dm-views.sql`](../../sql/ddl/60-dm-views.sql).
|
образец для представления — [`sql/ddl/60-dm-views.sql`](../../sql/ddl/60-dm-views.sql).
|
||||||
Сохраненный файл позволит воссоздать ваши объекты при пересборке мира.
|
Сохраненный файл позволит воссоздать ваши объекты при пересборке мира.
|
||||||
|
|
||||||
## Самопроверка ступени 1
|
## Проверка витрин
|
||||||
|
|
||||||
Даг `homework_check` запускается руками, расписания у него нет. Задача
|
В Airflow есть даг `homework_check` с отдельной задачей для каждой учебной
|
||||||
витрины красная, пока витрины нет или счет не сходится, и зеленая, когда
|
витрины. Запускайте его вручную: расписания у него нет. Задача завершится
|
||||||
собрано верно. В сводку качества конвейера `dm.dq_summary` он не пишет: это
|
ошибкой, если витрина еще не создана или ее данные расходятся с ожидаемыми.
|
||||||
инструмент лабы, а не история качества стенда.
|
Причину и примеры расхождений ищите в журнале задачи. Проверки еще не
|
||||||
|
выполненных заданий тоже будут красными.
|
||||||
|
|
||||||
Проверка пяти заданий по витринам устроена одинаково и золотых констант не
|
Проверка заново рассчитывает показатели по источнику и сравнивает их с вашей
|
||||||
содержит: рядом с витриной независимым запросом собирается тот же ответ —
|
витриной. Строки сопоставляются по полям, которые их определяют: например,
|
||||||
прямо из источника витрины, минуя ее саму. У четырех заданий источник
|
по дню и странице. `FULL OUTER JOIN` позволяет найти и разные значения, и
|
||||||
эталона тот же, что у витрины, `dds.session_v` или `dds.event_v`; у задания
|
строки, которых нет с одной стороны. `INNER JOIN` пропустил бы отсутствующие
|
||||||
4 это таблицы брака ODS, потому что в DDS нет сущности брака. Обе стороны
|
строки. Когда расхождений нет, задача становится зеленой.
|
||||||
соединяются по деловому ключу через `FULL OUTER JOIN`, и считается число
|
|
||||||
строк, где стороны разошлись или одной стороны нет. Ноль — зеленый. Полное
|
|
||||||
соединение сохраняет ключи обеих сторон: при внутреннем пропавший день или
|
|
||||||
ключ не попал бы в результат, и потеря осталась бы незамеченной.
|
|
||||||
|
|
||||||
Раздел «Как проверить себя» каждого ТЗ по витрине называет деловой ключ и
|
`homework_check` не сохраняет результаты в сводку качества `dm.dq_summary`.
|
||||||
сравниваемые величины. Тот же прием в стендовом исполнении — [даг
|
|
||||||
`dq_check`](../../dags/dq_check.py) и запросы в [`sql/dq/`](../../sql/dq).
|
|
||||||
|
|
||||||
**Предупреждение.** Запросы самой проверки лежат в `sql/dq/` открыто, и
|
Запросы проверок лежат в [`sql/dq/`](../../sql/dq/). Откройте их после собственного решения:
|
||||||
имена называют витрину и роль сверки. Проверка на инвариантах неизбежно
|
они заново рассчитывают показатели витрины, поэтому содержат ответ на
|
||||||
близка к ответу: чтобы сверить витрину, надо ее пересчитать. Если открыть
|
существенную часть задания. Сравните свой способ расчета с готовым.
|
||||||
эти файлы до выполнения задания, можно скопировать готовый SQL, не
|
|
||||||
разобравшись в расчете самостоятельно. Смотреть туда стоит после своей
|
|
||||||
сборки: сравнить два решения полезно, списать одно — нет.
|
|
||||||
|
|
||||||
## Факультативы
|
## Задания по желанию
|
||||||
|
|
||||||
Эти задания вы берете по желанию. Полный список сюжетов — в разделе
|
Полный список сюжетов — в разделе
|
||||||
[«Факультативы — пять разделов»](../specs/2026-09-01-labs-program.md#4-факультативы--пять-разделов)
|
[«Факультативы — пять разделов»](../specs/2026-09-01-labs-program.md#4-факультативы--пять-разделов)
|
||||||
программы.
|
программы.
|
||||||
|
|
||||||
### Почему выручка не сходится
|
### Почему выручка не сходится
|
||||||
|
|
||||||
Открывается после ступени 1. Расхождения между источниками, повторы
|
Открывается после ступени 1. Четыре расследования качества данных:
|
||||||
покупок и день, после которого отчет можно считать окончательным.
|
|
||||||
|
|
||||||
### Тонкий анализ данных
|
- [Куда пропали покупки](optional/missing-purchases.md): найти заказы без
|
||||||
|
события, выбрать источник витрины и условие проверки полноты.
|
||||||
|
- [Почему повтор покупки завышает выручку](optional/duplicate-purchases.md):
|
||||||
|
рассчитать выручку с одной строкой на заказ и проверить результат на
|
||||||
|
повторных событиях.
|
||||||
|
- [Какая позиция объясняет разницу в сумме](optional/order-amount-difference.md):
|
||||||
|
разобрать товары заказа без потери точности и найти причину разницы в суммах.
|
||||||
|
- [Почему вчерашняя выручка меняется сегодня](optional/revenue-over-time.md):
|
||||||
|
определить, за какие дни пересчитывать выручку и какую историю заказов сохранять.
|
||||||
|
|
||||||
Открывается после ступени 1. Знание о пользователе на дату и обогащение
|
Первые три работы используют готовые данные. Четвертая продвигает мир через
|
||||||
отчета своим словарем.
|
Airflow; для нее вы сохраняете состояния заказов после каждого модельного дня.
|
||||||
|
|
||||||
### Superset и BI
|
### Данные о посетителях
|
||||||
|
|
||||||
Открывается после ступени 2. В задании
|
Открывается после ступени 1.
|
||||||
[Свой дашборд в Superset](optional/superset-dashboard.md) вы собираете
|
|
||||||
отдельный дашборд, не меняя объекты стенда.
|
|
||||||
|
|
||||||
### Инженерия стенда
|
- [Как новые заказы меняют отчет о посетителях](optional/identity-over-time.md):
|
||||||
|
сохранить историю связей, сравнить расчеты на прежних сессиях и определить,
|
||||||
|
какие сведения нужны для воспроизведения результата.
|
||||||
|
|
||||||
Открывается после ступени 1. Прием и хранение данных, раскладка по шардам,
|
### Отчеты в Superset
|
||||||
повторная обработка дня и обновление витрин.
|
|
||||||
|
|
||||||
### Что делать, когда стенд сломался
|
Открывается после ступени 2. Два самостоятельных задания:
|
||||||
|
|
||||||
Открывается после ступени 7. Поломки стенда и runbook по итогам
|
- [Свой дашборд в Superset](optional/superset-dashboard.md): собрать отчет
|
||||||
расследований; весь раздел — повышенная сложность.
|
из своих витрин и подтвердить выводы запросами.
|
||||||
|
- [В DBeaver работает, в Superset нет](optional/superset-missing-object.md):
|
||||||
|
выяснить, почему клиенты по-разному видят один объект хранилища.
|
||||||
|
|
||||||
## Уборка
|
### Загрузка и обновление данных
|
||||||
|
|
||||||
Черновики и опыты за собой сносить: правило проверок стенда —
|
Открывается после ступени 1.
|
||||||
[дока тестирования](../architecture/testing.md). Снесенная витрина обязана
|
|
||||||
снова покраснить свою задачу в `homework_check` — если не покраснила,
|
- [Как обновлять витрину без полной пересборки](optional/incremental-daily-traffic.md):
|
||||||
проверка ничего и не проверяла.
|
продолжить работу о связях пользователей, пересчитать затронутые дни
|
||||||
|
и проверить повтор незавершенной загрузки. Повышенная сложность.
|
||||||
|
|
||||||
|
## Удаление временных объектов
|
||||||
|
|
||||||
|
После опытов удаляйте свои временные объекты. Если удалили учебную витрину,
|
||||||
|
ее задача в `homework_check` должна снова завершиться ошибкой: отсутствие
|
||||||
|
витрины тоже проверяется. Подробнее — в
|
||||||
|
[справочнике проверок](../architecture/testing.md).
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# Почему повтор покупки завышает выручку
|
||||||
|
|
||||||
|
Несколько событий покупки могут относиться к одному заказу. Вы проверите,
|
||||||
|
как они завышают сумму, и составите расчет, который учитывает каждый заказ
|
||||||
|
один раз.
|
||||||
|
|
||||||
|
Факультатив доступен после ступени 1. Работайте в DBeaver пользователем
|
||||||
|
`analyst` по [инструкции](../../../README.md#подключиться-из-dbeaver).
|
||||||
|
Дождитесь завершения `etl_pipeline`; оставьте `world_live` на паузе и
|
||||||
|
дождитесь уже начатых прогонов, чтобы данные не менялись между запросами.
|
||||||
|
|
||||||
|
## Расчет по событиям покупки
|
||||||
|
|
||||||
|
В ODS уже устранены повторные доставки событий. Достаточно ли этого,
|
||||||
|
чтобы сложить суммы всех `purchase` для витрины заказов? Проверьте на данных.
|
||||||
|
|
||||||
|
Возьмите день, за который уже получены все слепки заказов: его дата должна быть не позднее
|
||||||
|
`max(snapshot_date) - 6` из `ods.order_v`. Во всех запросах к `dds.event_v`
|
||||||
|
отбирайте этот `event_date` и `event_type = 'purchase'`.
|
||||||
|
|
||||||
|
Номер заказа находится в `purchase_id[1]`, заявленная сумма — в
|
||||||
|
`purchase_revenue[1]`. По формату источника у события покупки один номер заказа.
|
||||||
|
|
||||||
|
Сначала посчитайте число событий и сумму всех заявленных сумм. Затем найдите
|
||||||
|
номера заказов, которые встречаются несколько раз. Если таких нет, выберите
|
||||||
|
другой день с полученными слепками.
|
||||||
|
|
||||||
|
Для двух повторившихся покупок выпишите `event_id`, `client_id`, `event_time`,
|
||||||
|
номер заказа и сумму. Сопоставьте число событий и заказов в `dds.order_v`.
|
||||||
|
|
||||||
|
## Расчет по заказам
|
||||||
|
|
||||||
|
Напишите запрос с одной строкой на номер заказа. Сохраните в результате
|
||||||
|
номер, число исходных событий и одну заявленную сумму. До выбора суммы
|
||||||
|
проверьте, что во всех событиях заказа она одинакова. Затем посчитайте итог
|
||||||
|
дня по результату этого запроса и сравните с расчетом по событиям.
|
||||||
|
|
||||||
|
Проверьте, что номера в результате уникальны, а число строк совпадает с числом
|
||||||
|
разных номеров в исходных событиях. Отдельно проверьте свое решение на случае
|
||||||
|
двух разных заказов с одинаковой суммой: сколько денег оно учтет?
|
||||||
|
|
||||||
|
## Заказы с повторными событиями
|
||||||
|
|
||||||
|
Возьмите один повторившийся номер заказа. Сколько денег он добавил в первый
|
||||||
|
расчет и сколько — во второй? Подтвердите разность его событиями.
|
||||||
|
|
||||||
|
Повторите расчет для остальных повторившихся номеров. Сохраните таблицу:
|
||||||
|
номер заказа, число событий и вклад в разность сумм. Проверьте, объясняют ли
|
||||||
|
эти заказы все расхождение за день до копейки.
|
||||||
|
|
||||||
|
Слой ODS убирает повторную доставку одного события по его идентификатору;
|
||||||
|
`dds.event_v` читает уже это состояние. Сравните число разных `event_id`
|
||||||
|
и число разных номеров заказа в выбранных строках. Почему устранение
|
||||||
|
повторов по первому ключу не дало тот же результат, что по второму?
|
||||||
|
Подтвердите ответ конкретными идентификаторами.
|
||||||
|
|
||||||
|
После собственного расчета найдите эти заказы в `dm.purchase_vs_orders_v`.
|
||||||
|
Если у повторившегося заказа класс отличается от `duplicate_event`, проверьте
|
||||||
|
его статус и [порядок присвоения классов](../../architecture/dm.md#dmpurchase_vs_orders_v).
|
||||||
|
Класс витрины не заменяет поиск всех повторов в событиях.
|
||||||
|
|
||||||
|
## Сравнение с выручкой магазина
|
||||||
|
|
||||||
|
Сложите `revenue` из `dm.revenue_daily_v` за тот же `report_date` по всем
|
||||||
|
категориям. Эта витрина считает стоимость позиций оплаченных заказов,
|
||||||
|
без скидки и доставки.
|
||||||
|
|
||||||
|
Сравните результат с суммой по событиям после устранения повторов. Совпадение не требуется:
|
||||||
|
удаление повторов устраняет одну причину расхождения. Найдите хотя бы один
|
||||||
|
заказ, который объясняет, почему деньги трекера и магазина все еще могут
|
||||||
|
различаться. Для этого сопоставьте события, заказ и готовую сверку.
|
||||||
|
|
||||||
|
Рядом с запросом укажите, чему соответствует одна строка результата
|
||||||
|
и какие поля ее определяют. Обоснуйте, где нужна одна строка на событие,
|
||||||
|
а где одна на заказ: следует ли ради этого расчета
|
||||||
|
удалять повторные покупки из `dds.event_v`? Используйте найденные `event_id`.
|
||||||
|
|
||||||
|
## Результат работы
|
||||||
|
|
||||||
|
- [ ] сохранен запрос с одной строкой на заказ, числом событий и суммой;
|
||||||
|
- [ ] уникальность ключа и полнота набора номеров проверены по источнику;
|
||||||
|
- [ ] разность сумм полностью объяснена вкладом лишних событий;
|
||||||
|
- [ ] обоснованы ключ расчета и место устранения повторов;
|
||||||
|
- [ ] найден случай, показывающий границы расчета денег по трекеру.
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
# Как новые заказы меняют отчет о посетителях
|
||||||
|
|
||||||
|
Вы сохраните историю связей посетителей сайта с покупателями магазина.
|
||||||
|
На ее основе проверите, как новые связи меняют старые отчеты и что нужно,
|
||||||
|
чтобы повторить прежний расчет.
|
||||||
|
|
||||||
|
Факультатив доступен после ступени 1. Понадобятся Airflow и DBeaver с
|
||||||
|
подключением `student` по [инструкции](../../../README.md#подключиться-из-dbeaver).
|
||||||
|
Результаты запросов сохраняйте в `default` через одно подключение:
|
||||||
|
ваши таблицы будут доступны только на узле этого подключения.
|
||||||
|
Если предложенное имя таблицы занято, выберите свое.
|
||||||
|
|
||||||
|
В событиях сайта посетитель обозначен `client_id` из cookie браузера,
|
||||||
|
а в заказах покупатель обозначен `user_id`. Сопоставив событие покупки
|
||||||
|
с заказом, можно связать эти идентификаторы. Тогда и к прежним визитам
|
||||||
|
этого `client_id` можно добавить
|
||||||
|
пользователя магазина. Вы проверите, как появление такой связи меняет число
|
||||||
|
известных пользователей в отчете за прошедший день.
|
||||||
|
|
||||||
|
## Связи посетителей с покупателями
|
||||||
|
|
||||||
|
Поставьте `world_live` на паузу, дождитесь завершения начатых прогонов
|
||||||
|
генерации и обработки. Запустите `etl_pipeline` и дождитесь успеха.
|
||||||
|
На время работы мир будете продвигать вручную; новые дни останутся в стенде.
|
||||||
|
|
||||||
|
Напишите запрос по двум источникам:
|
||||||
|
|
||||||
|
| Источник | Нужные поля |
|
||||||
|
|---|---|
|
||||||
|
| `dds.event_v` | `event_type`, `event_time`, `client_id`, `purchase_id` |
|
||||||
|
| `dds.order_v` | `order_id`, `user_id` |
|
||||||
|
|
||||||
|
В результате нужна одна строка на пару `client_id`, `user_id`. Для каждой пары
|
||||||
|
найдите самое раннее время события покупки, которое удалось сопоставить
|
||||||
|
с заказом. Назовите эту колонку `first_purchase_at`.
|
||||||
|
В событии `purchase` массив `purchase_id` содержит один номер заказа.
|
||||||
|
Статус заказа не ограничивайте: отмена покупки не меняет найденную связь
|
||||||
|
посетителя с покупателем.
|
||||||
|
|
||||||
|
Источники распределены по разным ключам. Для соединения используйте
|
||||||
|
`GLOBAL INNER JOIN`, заключив выборки событий и заказов в подзапросы.
|
||||||
|
Устройство источников описано в [карте идентичностей](../../architecture/dds/identity.md).
|
||||||
|
|
||||||
|
Проверьте уникальность пар и совпадение их полного набора с
|
||||||
|
`dds.identity_map_v`: одного равенства количества недостаточно.
|
||||||
|
Для одной пары покажите номера заказов и события, по которым получили
|
||||||
|
`first_purchase_at`.
|
||||||
|
|
||||||
|
## История связей
|
||||||
|
|
||||||
|
Создайте таблицу:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE default.lab_identity_history
|
||||||
|
(
|
||||||
|
observation_day Date,
|
||||||
|
client_id UInt64,
|
||||||
|
user_id UInt64,
|
||||||
|
first_purchase_at DateTime('Europe/Samara')
|
||||||
|
)
|
||||||
|
ENGINE = MergeTree
|
||||||
|
ORDER BY (observation_day, client_id, user_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
Каждая копия списка должна содержать все найденные пары. В колонку
|
||||||
|
`observation_day` запишите последний обработанный день событий:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT max(event_date) AS observation_day FROM dds.event_v;
|
||||||
|
```
|
||||||
|
|
||||||
|
Обозначим полученную дату **H0**. Она показывает, после обработки какого
|
||||||
|
модельного дня сохранен список; календарная дата запуска Airflow здесь
|
||||||
|
не используется. Сохраните полный результат своего запроса через
|
||||||
|
`INSERT INTO ... SELECT`, добавив H0 в `observation_day` каждой строки.
|
||||||
|
Сверьте число записанных строк с результатом исходного запроса и проверьте
|
||||||
|
уникальность пар для этой даты.
|
||||||
|
|
||||||
|
Затем дважды выполните последовательность:
|
||||||
|
|
||||||
|
1. Запустите `world_next_day` с `days = 1` и дождитесь успеха.
|
||||||
|
2. Запустите `etl_pipeline` и дождитесь успеха.
|
||||||
|
3. Получите новую `observation_day`, сохраните полный набор связей и проверьте его.
|
||||||
|
|
||||||
|
Получатся три копии списка: после дней **H0**, **H1** и **H2**.
|
||||||
|
Для каждой даты сохраняйте список один раз: обычный `MergeTree`
|
||||||
|
не устраняет повторы вставки. Сохраните и проверьте список перед тем,
|
||||||
|
как запускать следующий модельный день.
|
||||||
|
|
||||||
|
## Дата покупки и дата появления связи
|
||||||
|
|
||||||
|
Сравните списки за соседние даты: H0 с H1, затем H1 с H2. Найдите пары,
|
||||||
|
которых не было в прежнем списке. Для нескольких таких пар покажите
|
||||||
|
`client_id`, `user_id`, `first_purchase_at` и первую `observation_day`,
|
||||||
|
в которой пара появилась. Найдите пример, где покупка произошла раньше
|
||||||
|
этой даты.
|
||||||
|
|
||||||
|
Для одной такой связи вернитесь к заказу и событию. По данным и
|
||||||
|
[порядку доставки заказов](../../architecture/orders/ingestion.md) объясните,
|
||||||
|
почему пары не было в прежнем списке. Какие данные понадобились для ее
|
||||||
|
появления и когда они стали доступны?
|
||||||
|
|
||||||
|
Для пары, которая присутствовала уже в H0, нельзя определить дату появления:
|
||||||
|
у вас нет более ранних копий списка. Для новой пары можно установить
|
||||||
|
интервал между двумя сохранениями, но не точное время внутри него.
|
||||||
|
|
||||||
|
## Пересчет прежних отчетов
|
||||||
|
|
||||||
|
Возьмите один и тот же набор сессий из `dds.session_v` за даты до H0
|
||||||
|
включительно. Постройте два дневных расчета:
|
||||||
|
|
||||||
|
- со списком связей на H0;
|
||||||
|
- со списком связей на H2.
|
||||||
|
|
||||||
|
Для каждого дня посчитайте посетителей по разным `client_id` и известных
|
||||||
|
пользователей по разным `user_id`. Сессии без связи должны остаться в расчете,
|
||||||
|
но неизвестный пользователь не должен увеличивать число известных.
|
||||||
|
В соединении сессий с вашей таблицей используйте `GLOBAL LEFT JOIN`.
|
||||||
|
В таблице связей заранее отберите одну `observation_day`, чтобы копии
|
||||||
|
списка за разные дни не смешались.
|
||||||
|
|
||||||
|
Найдите день с разным числом известных пользователей. Проследите изменение
|
||||||
|
до конкретных связей и прежних сессий. Проверьте, что число посетителей
|
||||||
|
не изменилось, а число известных пользователей не больше числа посетителей.
|
||||||
|
|
||||||
|
Теперь проверьте, можно ли воспроизвести расчет на H0 без сохраненного
|
||||||
|
списка за эту дату. Возьмите список на H2 и оставьте только пары,
|
||||||
|
у которых `first_purchase_at` приходится на H0 или более ранний день.
|
||||||
|
Сравните полный набор пар с копией на H0. Выведите пары, которые есть
|
||||||
|
только в одном из списков.
|
||||||
|
На примере найденных различий объясните, какие данные нужно сохранять,
|
||||||
|
чтобы повторить прежний расчет.
|
||||||
|
|
||||||
|
## Результат работы
|
||||||
|
|
||||||
|
У вас есть запрос сопоставления посетителей с покупателями, три сохраненных
|
||||||
|
списка связей и сравнение двух расчетов на одном наборе сессий. На конкретных
|
||||||
|
парах вы можете показать разницу между датой покупки и датой появления пары
|
||||||
|
в сохраненном списке, а также объяснить, что нужно для повторения прежнего
|
||||||
|
расчета.
|
||||||
|
|
||||||
|
Сохраните SQL и таблицу `lab_identity_history`: они нужны в продолжении
|
||||||
|
[«Как обновлять витрину без полной пересборки»](incremental-daily-traffic.md).
|
||||||
|
Если продолжать не планируете, удалите свою таблицу через `DROP TABLE`.
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
# Как обновлять витрину без полной пересборки
|
||||||
|
|
||||||
|
Вы настроите обновление витрины только за дни, затронутые новыми данными.
|
||||||
|
Затем проверите, можно ли продолжить загрузку, которая успела записать
|
||||||
|
лишь часть результатов.
|
||||||
|
|
||||||
|
Факультатив повышенной сложности продолжает работу
|
||||||
|
[«Как новые заказы меняют отчет о посетителях»](identity-over-time.md).
|
||||||
|
Понадобятся ее запрос сохранения списка связей и таблица `default.lab_identity_history`.
|
||||||
|
Работайте под `student` через то же подключение DBeaver.
|
||||||
|
|
||||||
|
Стенд пересчитывает `dm.daily_traffic_v` целиком: новая связь может изменить
|
||||||
|
число известных пользователей в старом дне. Вы построите рядом свою витрину,
|
||||||
|
которая пересчитывает только затронутые дни и дает тот же результат.
|
||||||
|
|
||||||
|
При обычном продвижении модельного времени появляются новые дни сессий
|
||||||
|
и новые связи. Уже найденные пары сохраняются, а сессии прежних дней
|
||||||
|
не меняются.
|
||||||
|
Удаление данных, исправления старых сессий и пересоздание мира потребовали бы
|
||||||
|
другого способа обнаружения изменений.
|
||||||
|
|
||||||
|
## Витрина и журнал загрузок
|
||||||
|
|
||||||
|
Создайте две локальные таблицы. Если имена заняты, выберите свои.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE default.lab_daily_traffic
|
||||||
|
(
|
||||||
|
report_date Date,
|
||||||
|
visitors UInt64,
|
||||||
|
known_users UInt64,
|
||||||
|
observation_day Date
|
||||||
|
)
|
||||||
|
ENGINE = ReplacingMergeTree(observation_day)
|
||||||
|
ORDER BY report_date;
|
||||||
|
|
||||||
|
CREATE TABLE default.lab_traffic_runs
|
||||||
|
(
|
||||||
|
observation_day Date
|
||||||
|
)
|
||||||
|
ENGINE = MergeTree
|
||||||
|
ORDER BY observation_day;
|
||||||
|
```
|
||||||
|
|
||||||
|
В витрине одна актуальная строка на день. Для обновления вставляйте полный
|
||||||
|
новый результат дня с новой `observation_day`. Читайте витрину с `FINAL`:
|
||||||
|
для каждого `report_date` останется строка с максимальной `observation_day`.
|
||||||
|
Старые версии могут исчезнуть при слиянии,
|
||||||
|
поэтому для воспроизведения прежнего расчета нужна сохраненная история связей.
|
||||||
|
|
||||||
|
В журнал записывайте `observation_day` списка связей, использованного
|
||||||
|
для расчета. Добавляйте дату только после записи всех выбранных дней
|
||||||
|
и успешной сверки. Максимальная дата в журнале укажет, какой список
|
||||||
|
использовался в последней завершенной загрузке.
|
||||||
|
|
||||||
|
## Первая загрузка
|
||||||
|
|
||||||
|
Убедитесь, что `world_live` стоит на паузе и нет активных прогонов генерации
|
||||||
|
и обработки. Запустите `etl_pipeline` и дождитесь успеха. Получите текущую
|
||||||
|
`observation_day` способом из первой работы. Если списка за эту дату еще нет
|
||||||
|
в вашей истории, сохраните его. Повторно вставлять уже сохраненный список
|
||||||
|
не требуется.
|
||||||
|
|
||||||
|
Напишите `INSERT INTO ... SELECT`, который рассчитывает все даты сессий из
|
||||||
|
`dds.session_v` со списком связей на текущую дату. Правила результата:
|
||||||
|
|
||||||
|
| Колонка | Что считать |
|
||||||
|
|---|---|
|
||||||
|
| `report_date` | `session_date` |
|
||||||
|
| `visitors` | число разных `client_id` |
|
||||||
|
| `known_users` | число разных известных `user_id` |
|
||||||
|
| `observation_day` | дата выбранной копии списка связей |
|
||||||
|
|
||||||
|
Сессии без известного пользователя сохраняйте в расчете. Несколько кук одного
|
||||||
|
пользователя не должны увеличивать `known_users`. Для соединения с вашей
|
||||||
|
таблицей связей используйте `GLOBAL LEFT JOIN`, отобрав одну `observation_day`.
|
||||||
|
|
||||||
|
Построчно сверьте свои `report_date`, `visitors`, `known_users` с
|
||||||
|
`dm.daily_traffic_v`. Сверка должна находить лишние и отсутствующие даты
|
||||||
|
и различия обоих показателей. Учебную витрину читайте с `FINAL`.
|
||||||
|
После совпадения запишите дату наблюдения в журнал один раз.
|
||||||
|
|
||||||
|
## Выбор дней для пересчета
|
||||||
|
|
||||||
|
Продвиньте мир на один день: `world_next_day` с `days = 1`, затем
|
||||||
|
`etl_pipeline`; после каждого запуска дождитесь успеха. Сохраните новый
|
||||||
|
список связей запросом из первой работы.
|
||||||
|
|
||||||
|
Обозначим максимальную `observation_day` в журнале **P**, а дату нового
|
||||||
|
списка связей — **H**. До записи результата составьте запрос, возвращающий
|
||||||
|
даты для пересчета без повторов. Он должен учесть два изменения:
|
||||||
|
|
||||||
|
- в сессиях появились дни, которых еще нет в вашей витрине;
|
||||||
|
- в списке на H появились пары, которых не было в списке на P; у этих `client_id`
|
||||||
|
есть сессии, в том числе за прежние дни.
|
||||||
|
|
||||||
|
Сначала выведите даты и выберите один прежний день: какая новая пара
|
||||||
|
потребовала его пересчета и какие сессии с ней связаны? Рядом посчитайте,
|
||||||
|
сколько дат пересчитала бы полная загрузка. Зафиксируйте оба числа;
|
||||||
|
равенство тоже возможно, если новые связи затронули всю историю.
|
||||||
|
|
||||||
|
Для сравнения выберите из списка на H пары, у которых день покупки
|
||||||
|
по `first_purchase_at` позже P. Совпадет ли результат с новыми парами,
|
||||||
|
найденными сравнением списков? Покажите пару, которую ограничение по дате
|
||||||
|
покупки пропускает.
|
||||||
|
|
||||||
|
## Обновление витрины
|
||||||
|
|
||||||
|
Напишите второй `INSERT INTO ... SELECT`. Для выбранных дат он должен
|
||||||
|
рассчитать все сессии с полным списком связей на H. Прибавление числа новых
|
||||||
|
связей к прежнему `known_users` не заменяет пересчет разных пользователей.
|
||||||
|
|
||||||
|
Вставьте результаты с версией H. Полностью сверьте актуальную учебную витрину
|
||||||
|
со стендовой, включая дни, которые не пересчитывали. Только после совпадения
|
||||||
|
добавьте H в журнал.
|
||||||
|
|
||||||
|
Без продвижения мира снова определите P по журналу и повторите выбор дат.
|
||||||
|
Запрос должен вернуть пустой результат, а витрина — остаться прежней.
|
||||||
|
Повторно записывать H в журнал не требуется.
|
||||||
|
|
||||||
|
## Повтор незавершенной загрузки
|
||||||
|
|
||||||
|
Снова запустите `world_next_day` с `days = 1`, затем `etl_pipeline`,
|
||||||
|
дожидаясь успеха каждого. Сохраните новый список связей с его датой H.
|
||||||
|
Получите даты для пересчета, но запишите результат только за одну из
|
||||||
|
них. Остальные пока не загружайте и дату в журнал не добавляйте.
|
||||||
|
Так вы воспроизведете остановку между записями дней, не ломая стенд.
|
||||||
|
|
||||||
|
Снова определите P по журналу, выполните запрос выбора дат и пересчет
|
||||||
|
этих дней. Проверьте, что результат совпал со стендовой витриной без очистки
|
||||||
|
уже записанных строк. После сверки запишите H в журнал.
|
||||||
|
|
||||||
|
Объясните на этом опыте, почему сохранение нового списка связей еще
|
||||||
|
не означает, что витрина обновлена. Что изменится, если записать H в журнал
|
||||||
|
до обновления витрины?
|
||||||
|
|
||||||
|
## Результат работы
|
||||||
|
|
||||||
|
Сохраните SQL первой загрузки, выбора дат, обновления и сверки.
|
||||||
|
После двух новых модельных дней ваша витрина совпадает со стендовой,
|
||||||
|
повтор без новых данных сохраняет результат, а незавершенную загрузку
|
||||||
|
можно продолжить без полной пересборки.
|
||||||
|
|
||||||
|
Сравните число пересчитанных дней с полной загрузкой и перечислите,
|
||||||
|
какие дополнительные данные пришлось хранить. Так можно оценить объем работы.
|
||||||
|
На небольшом стенде полный пересчет может быть дешевле.
|
||||||
|
В вашей реализации поиск связей пока читает всю историю покупок.
|
||||||
|
По новым данным обновляется только дневная витрина; такое обновление
|
||||||
|
называют инкрементальным.
|
||||||
|
|
||||||
|
После работы удалите свои `lab_daily_traffic`, `lab_traffic_runs` и
|
||||||
|
`lab_identity_history` через `DROP TABLE`, если они больше не нужны.
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# Куда пропали покупки
|
||||||
|
|
||||||
|
Вы сравните покупки из событий сайта с заказами магазина. Найденные
|
||||||
|
расхождения помогут выбрать источник для полной витрины заказов.
|
||||||
|
|
||||||
|
Факультатив доступен после ступени 1. В DBeaver подключитесь пользователем
|
||||||
|
`analyst` по [инструкции](../../../README.md#подключиться-из-dbeaver).
|
||||||
|
Дождитесь завершения `etl_pipeline`; на время расследования оставьте
|
||||||
|
`world_live` на паузе и дождитесь уже начатых прогонов.
|
||||||
|
|
||||||
|
## Выбор дня
|
||||||
|
|
||||||
|
Можно ли построить полную витрину заказов по событиям `purchase`?
|
||||||
|
Проверьте это на одном дне: сначала сопоставьте события сайта, затем сверьте
|
||||||
|
номера заказов с выгрузкой магазина.
|
||||||
|
|
||||||
|
Слепок заказов содержит их состояние на конец модельного дня. Состояние
|
||||||
|
заказа может меняться семь дней, поэтому для окончательного сравнения нужны
|
||||||
|
все слепки за этот период. Возьмите день, для которого уже получен последний
|
||||||
|
из них. Такой день называется закрытым. Запрос показывает самую позднюю
|
||||||
|
подходящую дату:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT max(snapshot_date) - 6 AS last_closed_day
|
||||||
|
FROM ods.order_v;
|
||||||
|
```
|
||||||
|
|
||||||
|
Используйте выбранный день во всех запросах. События отбирайте по
|
||||||
|
`dds.event_v.event_date`, заказы — по `dds.order_v.order_date`.
|
||||||
|
Сравнивайте все заказы дня, включая отмененные: сейчас вопрос в том,
|
||||||
|
зарегистрировал ли магазин заказ, а не в том, был ли он оплачен.
|
||||||
|
|
||||||
|
## События оформления заказа
|
||||||
|
|
||||||
|
При оформлении заказа посетитель видит страницу подтверждения, а трекер
|
||||||
|
отправляет событие покупки. Просмотр страницы и покупка — отдельные события.
|
||||||
|
|
||||||
|
Посчитайте просмотры страницы подтверждения и события покупки:
|
||||||
|
|
||||||
|
- у просмотра `event_type = 'pageview'`, а `url` заканчивается на `/confirmation`;
|
||||||
|
- у покупки `event_type = 'purchase'`.
|
||||||
|
|
||||||
|
Вычтите число событий покупки из числа просмотров подтверждения. Сохраните
|
||||||
|
оба счетчика и разность. Как соотносились бы эти числа, если бы для каждого
|
||||||
|
заказа пришло ровно по одному событию каждого вида? Что можно предположить
|
||||||
|
по вашему результату и чего эти два числа еще не доказывают?
|
||||||
|
|
||||||
|
## Сверка с заказами магазина
|
||||||
|
|
||||||
|
В событии номер заказа находится в `purchase_id[1]`, в заказах — в `order_id`.
|
||||||
|
Соберите список заказов выбранного дня, для которых нет события покупки.
|
||||||
|
Несколько событий с одним номером заказа должны оставаться одной покупкой.
|
||||||
|
Сохраните запрос сверки: на каждый такой заказ он должен возвращать дату,
|
||||||
|
номер и статус. Дату задавайте в одном месте, чтобы запрос можно было
|
||||||
|
повторить для другого дня.
|
||||||
|
|
||||||
|
Проверьте два равенства:
|
||||||
|
|
||||||
|
- число заказов магазина равно числу разных заказов из событий плюс число
|
||||||
|
заказов без события;
|
||||||
|
- разность просмотров подтверждения и событий покупки равна числу заказов
|
||||||
|
без события.
|
||||||
|
|
||||||
|
Первое равенство проверяет полноту сопоставления источников. Второе связывает
|
||||||
|
разность счетчиков с найденными заказами без события. Проверьте, как на него
|
||||||
|
влияют повторные покупки.
|
||||||
|
|
||||||
|
Найдите номер заказа с несколькими событиями `purchase`. Для его `client_id`
|
||||||
|
выведите события рядом с этими покупками по времени: `event_time`,
|
||||||
|
`event_type` и `url`. Найдите просмотры `/confirmation` перед первой
|
||||||
|
и повторной покупкой. Посчитайте, сколько повтор добавил просмотров
|
||||||
|
подтверждения и сколько — покупок. Изменилась ли от этих добавлений разность
|
||||||
|
счетчиков? Подтвердите ответ найденными строками. Второе равенство относится
|
||||||
|
к устройству этого стенда; для другого источника его нужно проверять заново.
|
||||||
|
|
||||||
|
Если в выбранном дне повторов нет, возьмите другой закрытый день.
|
||||||
|
|
||||||
|
## Сравнение с готовой витриной
|
||||||
|
|
||||||
|
Найдите свои заказы без события в `dm.purchase_vs_orders_v` по `order_id`.
|
||||||
|
Сравните их `status` и `mismatch_class`. Витрина присваивает заказу один класс,
|
||||||
|
хотя причин расхождения может быть несколько. По
|
||||||
|
[правилу классификации](../../architecture/dm.md#dmpurchase_vs_orders_v)
|
||||||
|
объясните, почему потеря события не всегда попадает в `lost_event`.
|
||||||
|
|
||||||
|
Возьмите один заказ из своего списка и сохраните два запроса: первый находит
|
||||||
|
его в магазине, второй показывает отсутствие покупки в трекере.
|
||||||
|
|
||||||
|
## Выбор источника для витрины
|
||||||
|
|
||||||
|
По результатам сверки укажите источник для витрины, в которой должен быть
|
||||||
|
каждый зарегистрированный заказ. Обоснуйте выбор найденными номерами.
|
||||||
|
|
||||||
|
Рядом с запросом запишите, когда его результат можно считать окончательным.
|
||||||
|
Что изменится, если запустить его до получения всех слепков выбранного дня?
|
||||||
|
Позволяет ли отсутствие события в `dds.event_v` установить место потери —
|
||||||
|
до приема в хранилище или при обработке? Какие данные нужны, чтобы это выяснить?
|
||||||
|
|
||||||
|
## Результат работы
|
||||||
|
|
||||||
|
- [ ] сохранен запрос сверки по дате, возвращающий заказы без события;
|
||||||
|
- [ ] оба равенства сходятся, влияние повторного просмотра объяснено;
|
||||||
|
- [ ] один случай подтвержден запросами к обоим источникам;
|
||||||
|
- [ ] источник витрины выбран по результатам сверки;
|
||||||
|
- [ ] указаны условие окончательной проверки и границы ее выводов.
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# Какая позиция объясняет разницу в сумме
|
||||||
|
|
||||||
|
Вы сравните товары в событии покупки и заказе магазина, чтобы найти причину
|
||||||
|
разницы в суммах. При разборе вложенных данных понадобится сохранить точность
|
||||||
|
цен до копейки.
|
||||||
|
|
||||||
|
Факультатив доступен после ступени 1. В DBeaver используйте пользователя
|
||||||
|
`analyst` из [инструкции подключения](../../../README.md#подключиться-из-dbeaver).
|
||||||
|
Дождитесь завершения `etl_pipeline`; оставьте `world_live` на паузе и
|
||||||
|
дождитесь уже начатых прогонов.
|
||||||
|
|
||||||
|
## Выбор заказа
|
||||||
|
|
||||||
|
Суммы покупки в трекере и заказа в магазине различаются. Разберите их позиции
|
||||||
|
и проверьте отдельно точность своего запроса и различия между событием и заказом.
|
||||||
|
|
||||||
|
В `dm.purchase_vs_orders_v` выберите один заказ с
|
||||||
|
`mismatch_class = 'amount_delta'`. Возьмите закрытый день: `order_day`
|
||||||
|
не позднее `max(snapshot_date) - 6` из `ods.order_v`. Сохраните `order_id`,
|
||||||
|
`order_day`, `declared_revenue` и `items_total`.
|
||||||
|
|
||||||
|
Найдите этот заказ в `dds.order_v`. Сопоставьте `items_total`, `discount`,
|
||||||
|
`delivery` и `total`. Запишите равенство, связывающее эти четыре значения,
|
||||||
|
и проверьте его на заказе. С какой суммой магазина следует сравнивать
|
||||||
|
`declared_revenue`, если мы ищем расхождение именно в составе корзины?
|
||||||
|
Проверьте свой выбор после разбора позиций.
|
||||||
|
|
||||||
|
## Товарные позиции события и заказа
|
||||||
|
|
||||||
|
Найдите событие покупки в `ods.event_v`: `purchaseID[1]` совпадает с номером
|
||||||
|
заказа, `EventDate` — с выбранным днем, `EventType = 'purchase'`.
|
||||||
|
Откройте поле `ecommerce`. Внутри JSON массив `purchase.products` содержит
|
||||||
|
товары из события покупки с полями `id`, `price` и `quantity`.
|
||||||
|
|
||||||
|
Разверните этот массив в строки и получите артикул, точную цену, количество
|
||||||
|
и стоимость каждой позиции. Для разбора пригодятся `JSONExtractArrayRaw`
|
||||||
|
и `JSONExtract`, для разворачивания массива — `ARRAY JOIN`.
|
||||||
|
Деньги извлекайте как `Decimal` с двумя знаками после запятой.
|
||||||
|
|
||||||
|
С другой стороны используйте массивы `item_sku`, `item_price` и
|
||||||
|
`item_quantity` заказа в `dds.order_v`. Элементы с одним индексом относятся
|
||||||
|
к одной позиции; при разворачивании сохраняйте эту связь.
|
||||||
|
|
||||||
|
Сопоставьте позиции по артикулу так, чтобы остались видны товары,
|
||||||
|
присутствующие только с одной стороны. Для каждой позиции сравните наличие,
|
||||||
|
цену и количество. Что различается и сколько это добавляет к расхождению
|
||||||
|
сумм?
|
||||||
|
Сохраните запрос сверки для выбранных даты и номера заказа; в результате
|
||||||
|
оставьте артикул, значения с обеих сторон и разность стоимостей позиций.
|
||||||
|
|
||||||
|
## Проверка сумм
|
||||||
|
|
||||||
|
Подтвердите три равенства:
|
||||||
|
|
||||||
|
- сумма стоимостей клиентских позиций равна `declared_revenue`;
|
||||||
|
- сумма стоимостей позиций магазина равна `items_total`;
|
||||||
|
- сумма расхождений по позициям равна
|
||||||
|
`declared_revenue - items_total`.
|
||||||
|
|
||||||
|
Затем сравните точные цены из JSON с готовым массивом `productPrice`
|
||||||
|
события. Пересчитайте стоимость клиентской корзины по этому массиву:
|
||||||
|
сойдется ли она с `declared_revenue` до копейки? Найдите цены, на которых
|
||||||
|
результаты различаются, и сверьте наблюдение с
|
||||||
|
[форматом выгрузки](../../formats/clickstream-event.md). Если все цены
|
||||||
|
выбранной корзины целые, возьмите еще один заказ с расхождением и дробной ценой.
|
||||||
|
|
||||||
|
Какие строки подтверждают найденное расхождение? Есть ли в источниках
|
||||||
|
сведения о том, почему оно возникло? Отделите наблюдения от предположений.
|
||||||
|
|
||||||
|
## Поля для точного расчета
|
||||||
|
|
||||||
|
Укажите рядом с запросом, откуда брать цену, каким типом ее читать и по какому
|
||||||
|
ключу сопоставлять позиции. Подтвердите выбор результатами обеих сверок:
|
||||||
|
суммы разобранных позиций с суммой своего источника и позиций двух источников
|
||||||
|
между собой. Какой результат укажет на ошибку вашего разбора, а какой
|
||||||
|
потребует искать причину выше — в данных, которые получил запрос?
|
||||||
|
|
||||||
|
## Результат работы
|
||||||
|
|
||||||
|
- [ ] сохранен запрос сверки позиций, не теряющий товары без пары;
|
||||||
|
- [ ] все три равенства сходятся до копейки;
|
||||||
|
- [ ] найден пример потери точности в готовом массиве цен;
|
||||||
|
- [ ] выбор полей, типа денег и ключа сопоставления подтвержден расчетами;
|
||||||
|
- [ ] расхождение источников сверки отделено от ошибки своего разбора и различия
|
||||||
|
между стоимостью корзины и итогом со скидкой и доставкой.
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
# Почему вчерашняя выручка меняется сегодня
|
||||||
|
|
||||||
|
Вы проследите, как опоздавшие заказы и смена статусов меняют выручку
|
||||||
|
за прошедший день. По этим наблюдениям определите, какие дни пересчитывать
|
||||||
|
и какую историю заказов сохранять.
|
||||||
|
|
||||||
|
Факультатив доступен после ступени 1. Понадобятся Airflow и DBeaver с
|
||||||
|
подключением `student` по [инструкции](../../../README.md#подключиться-из-dbeaver).
|
||||||
|
Пользователь `student` может читать данные стенда и записывать ваши наблюдения
|
||||||
|
в базу `default`.
|
||||||
|
Вы будете наблюдать один день оформления заказов, продвигая мир по одному
|
||||||
|
модельному дню. Проверьте, можно ли при загрузке выручки каждый раз считать
|
||||||
|
только новый день. Новые данные останутся в стенде.
|
||||||
|
|
||||||
|
## Подготовка
|
||||||
|
|
||||||
|
Поставьте `world_live` на паузу. Дождитесь завершения начатых прогонов
|
||||||
|
генерации и обработки, затем запустите `etl_pipeline` и дождитесь успеха.
|
||||||
|
Так исходное наблюдение будет снято после обновления витрин.
|
||||||
|
|
||||||
|
Узнайте последний день событий и дату последнего полученного слепка:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
(SELECT max(event_date) FROM dds.event_v) AS event_day,
|
||||||
|
(SELECT max(snapshot_date) FROM ods.order_v) AS snapshot_day;
|
||||||
|
```
|
||||||
|
|
||||||
|
День `event_day` обозначим **D**. Наблюдайте именно его до конца работы.
|
||||||
|
Дата слепка, по которому сделано очередное наблюдение, будет **S**.
|
||||||
|
Это модельные даты магазина. Календарная дата запуска Airflow день D не задает.
|
||||||
|
|
||||||
|
Слепок содержит состояние заказов на конец модельных суток и доставляется
|
||||||
|
при следующем запуске генератора. Поэтому покупки последнего завершившегося
|
||||||
|
дня уже видны в событиях, а их заказов в
|
||||||
|
хранилище еще нет. Проверьте это для D в `dds.order_v` и
|
||||||
|
`dm.purchase_vs_orders_v`.
|
||||||
|
|
||||||
|
## Таблицы для истории заказов
|
||||||
|
|
||||||
|
Представление `dds.order_v` показывает текущее состояние заказов, а сырые слепки
|
||||||
|
хранятся ограниченное время. Сохраните историю в двух своих таблицах: потом
|
||||||
|
вы сможете сравнивать наблюдения обычными SQL-запросами в том же DBeaver.
|
||||||
|
|
||||||
|
Выполните DDL ниже. Это локальные таблицы на узле вашего подключения;
|
||||||
|
всю работу выполняйте через одно подключение. Если имена уже заняты вашей
|
||||||
|
прошлой работой, выберите другие и используйте их в дальнейших запросах.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE default.lab_order_history
|
||||||
|
(
|
||||||
|
order_date Date,
|
||||||
|
snapshot_date Date,
|
||||||
|
order_id String,
|
||||||
|
status String,
|
||||||
|
items_total Decimal(18, 2)
|
||||||
|
)
|
||||||
|
ENGINE = MergeTree
|
||||||
|
ORDER BY (order_date, snapshot_date, order_id);
|
||||||
|
|
||||||
|
CREATE TABLE default.lab_revenue_history
|
||||||
|
(
|
||||||
|
order_date Date,
|
||||||
|
snapshot_date Date,
|
||||||
|
revenue Decimal(18, 2),
|
||||||
|
awaiting_order_ids Array(String)
|
||||||
|
)
|
||||||
|
ENGINE = MergeTree
|
||||||
|
ORDER BY (order_date, snapshot_date);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Первое наблюдение
|
||||||
|
|
||||||
|
Напишите два `INSERT INTO ... SELECT` с явным списком колонок назначения:
|
||||||
|
|
||||||
|
| Куда записать | Что взять из стенда |
|
||||||
|
|---|---|
|
||||||
|
| `default.lab_order_history` | По строке на заказ дня D из `dds.order_v`: `order_date`, `order_id`, `status`, `items_total`; добавьте дату наблюдения S |
|
||||||
|
| `default.lab_revenue_history` | Одну строку с D, S, суммой `revenue` за `report_date = D` из `dm.revenue_daily_v` и массивом номеров `awaiting_order` за `order_day = D` из `dm.purchase_vs_orders_v` |
|
||||||
|
|
||||||
|
Во втором запросе сумму и массив получите отдельными агрегатами, чтобы
|
||||||
|
заказы не умножили строки выручки при соединении. Для массива пригодится
|
||||||
|
`groupArray(order_id)`. Дату S берите из `max(snapshot_date)` в `ods.order_v`.
|
||||||
|
|
||||||
|
Сначала выполните части `SELECT` отдельно и проверьте результаты, затем
|
||||||
|
сохраните их. До первого слепка заказов дня D еще нет: в таблицу заказов
|
||||||
|
строки не добавятся, но в таблице итогов должна появиться строка с нулевой
|
||||||
|
выручкой. Она зафиксирует, что наблюдение сделано.
|
||||||
|
|
||||||
|
Каждую пару D и S сохраняйте один раз: повторный `INSERT` добавит копии строк.
|
||||||
|
После записи проверьте, что на эту пару приходится одна строка итогов,
|
||||||
|
а число заказов и число разных `order_id` совпадают с `dds.order_v` за D.
|
||||||
|
|
||||||
|
Выручка этой витрины — стоимость позиций оплаченных заказов без скидки и
|
||||||
|
доставки. Для каждого сохраненного наблюдения посчитайте число заказов по
|
||||||
|
статусам и сумму `items_total` только у `paid`. Эта сумма должна сходиться
|
||||||
|
с сохраненной выручкой, сложенной по всем категориям.
|
||||||
|
|
||||||
|
## Наблюдения за следующие дни
|
||||||
|
|
||||||
|
Для каждого следующего наблюдения:
|
||||||
|
|
||||||
|
1. Запустите `world_next_day` с `days = 1` и дождитесь успеха.
|
||||||
|
2. Запустите `etl_pipeline` и дождитесь успеха: `world_next_day` сам витрины
|
||||||
|
не обновляет.
|
||||||
|
3. Повторите два запроса сохранения с новой датой S и проверьте записанные строки.
|
||||||
|
|
||||||
|
Во всех запросах остается прежний день D. За один запуск добавляйте один
|
||||||
|
день, чтобы сохранить промежуточные состояния. Пакетный `world_next_day`
|
||||||
|
отправляет события без пауз; ждать 24 минуты живого дня здесь не нужно.
|
||||||
|
|
||||||
|
Продолжайте до слепка **D + 7** включительно. Заказы дня D могут меняться
|
||||||
|
семь дней, от D до D + 6. Этот период называется окном изменяемости.
|
||||||
|
Последний шаг нужен для сравнения состояния в конце окна и после него.
|
||||||
|
От исходного наблюдения потребуется восемь однодневных запусков.
|
||||||
|
|
||||||
|
## Изменения заказов и выручки
|
||||||
|
|
||||||
|
Для сравнения прочитайте `default.lab_order_history` дважды: в одном
|
||||||
|
подзапросе отберите прежнюю S, в другом — новую; день D остается общим.
|
||||||
|
Соедините состояния по `order_id`, сохранив и заказы без пары. Итоги для этих
|
||||||
|
же D и S возьмите из `default.lab_revenue_history`. Найдите:
|
||||||
|
|
||||||
|
- заказы, отсутствовавшие в первом слепке D, но появившиеся в следующем;
|
||||||
|
- переходы из `created` в `paid`;
|
||||||
|
- отмены заказов, для которых раньше был виден статус `paid`.
|
||||||
|
|
||||||
|
Первый слепок, полученный на следующий день, — обычная доставка заказов.
|
||||||
|
Опоздавшими здесь считаются те, которых не было уже в этом слепке.
|
||||||
|
Покажите на нескольких номерах, как они исчезают из `awaiting_order`.
|
||||||
|
|
||||||
|
Выберите два соседних наблюдения, между которыми хотя бы один заказ перешел
|
||||||
|
из `paid` в `cancelled`. Разложите изменение выручки по заказам.
|
||||||
|
Составьте таблицу: номер заказа, прежний и новый статусы, вклад в выручку
|
||||||
|
до и после изменения. Вклад определите по правилу расчета витрины, указанному
|
||||||
|
выше; учтите и заказы, которых еще не было в первом наблюдении. Сумма
|
||||||
|
изменений вкладов должна совпасть с изменением `revenue` до копейки.
|
||||||
|
|
||||||
|
Покажите на этих наблюдениях, какой результат остался бы в витрине,
|
||||||
|
если бы конвейер после первого расчета обрабатывал только новые дни заказов.
|
||||||
|
Достаточно ли добавлять в старый день только опоздавшие заказы? Проверьте
|
||||||
|
свой ответ на найденных изменениях статусов.
|
||||||
|
|
||||||
|
## Требования к истории заказов
|
||||||
|
|
||||||
|
Возьмите отмененный заказ, у которого в вашей истории был статус `paid`.
|
||||||
|
Укажите два наблюдения, доказывающих, что он был оплачен, а затем отменен.
|
||||||
|
|
||||||
|
Теперь возьмите отмененный заказ, для которого ни в одном вашем наблюдении
|
||||||
|
нет `paid`. Проверьте утверждение «этот заказ никогда не был оплачен»:
|
||||||
|
что известно о его состоянии между двумя слепками? Достаточно ли сохраненных
|
||||||
|
наблюдений для такого вывода?
|
||||||
|
|
||||||
|
Сформулируйте требования к данным для двух задач: восстановить состояние
|
||||||
|
витрины на момент прошлого расчета и узнать, был ли заказ когда-либо оплачен.
|
||||||
|
Какие сведения для каждой задачи должен передавать источник и сохранять
|
||||||
|
хранилище? Проверьте, каких нужных сведений нет в ваших таблицах наблюдений.
|
||||||
|
|
||||||
|
## Правило пересчета витрины
|
||||||
|
|
||||||
|
Сравните сохраненные состояния при S = D + 6 и S = D + 7: состав заказов,
|
||||||
|
их статусы, суммы и выручку. Какой слепок последним мог изменить заказы дня D
|
||||||
|
и каким прогоном он доставлен? Укажите наблюдение, после которого отчет
|
||||||
|
можно считать окончательным. Было бы достаточно совпадения двух соседних
|
||||||
|
сумм, если бы окно еще не закончилось?
|
||||||
|
|
||||||
|
Запишите правило выбора дней для пересчета после очередного слепка.
|
||||||
|
Считайте, что начальная история уже загружена и ежедневные прогоны не
|
||||||
|
пропускались. Укажите, по какой дате определяются границы, какие дни нужно
|
||||||
|
пересчитать и когда день можно перестать включать в расчет. Подтвердите
|
||||||
|
правило своими наблюдениями, включая опоздание и отмену.
|
||||||
|
|
||||||
|
После собственных выводов сверьтесь с описанием
|
||||||
|
[слепка и его доставки](../../architecture/orders/snapshot.md#слепок-и-его-доставка)
|
||||||
|
и [границ истории отмен](../../architecture/orders/fate.md#судьба-заказа).
|
||||||
|
|
||||||
|
## Результат работы
|
||||||
|
|
||||||
|
- [ ] сохранена последовательность наблюдений одного дня D с датами слепков S;
|
||||||
|
- [ ] на опоздавших заказах и изменениях статусов показаны ошибки загрузки
|
||||||
|
только новых дней или только новых заказов;
|
||||||
|
- [ ] изменение выручки между наблюдениями сходится с изменением вкладов заказов;
|
||||||
|
- [ ] для одного отмененного заказа найдены два наблюдения: с оплатой и с отменой;
|
||||||
|
- [ ] сформулированы требования к истории расчетов и истории оплаты;
|
||||||
|
- [ ] правило пересчета и момент закрытия дня обоснованы наблюдениями
|
||||||
|
и правилом доставки слепков.
|
||||||
|
|
||||||
|
После работы оставьте `world_live` на паузе либо верните автоматическое
|
||||||
|
продвижение, если пользовались им до наблюдения. Таблицы истории останутся
|
||||||
|
для повторной проверки выводов. Когда они больше не нужны, удалите только
|
||||||
|
свои `default.lab_order_history` и `default.lab_revenue_history`.
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
# Свой дашборд в Superset
|
# Свой дашборд в Superset
|
||||||
|
|
||||||
**Чему учит:** довести свои витрины до потребителя — завести в Superset
|
Из своих витрин вы соберете отчет о поведении посетителей: подберете графики
|
||||||
датасеты поверх публичных представлений, подобрать графики под вопросы и
|
и разместите их на одном экране так, чтобы отчет можно было понять
|
||||||
собрать из них один связный экран.
|
без ваших пояснений.
|
||||||
|
|
||||||
Это факультатив раздела «Superset и BI». Его можно пройти после ступени 2,
|
Это задание раздела «Отчеты в Superset». Его можно пройти после ступени 2,
|
||||||
когда готовы витрины ступени 1.
|
когда готовы витрины ступени 1.
|
||||||
|
|
||||||
## Роль дашборда
|
## Отчет о поведении посетителей
|
||||||
|
|
||||||
Соберите обзор трафика и поведения посетителей. Он должен отвечать на три
|
Соберите обзор трафика и поведения посетителей. Он должен отвечать на три
|
||||||
вопроса: что происходило в магазине, какие страницы, устройства и рекламные
|
вопроса: что происходило в магазине, какие страницы, устройства и рекламные
|
||||||
@@ -18,22 +18,23 @@
|
|||||||
стендового дашборда не нужно: выберите форму, в которой ответы читаются без
|
стендового дашборда не нужно: выберите форму, в которой ответы читаются без
|
||||||
пояснений рядом с экраном.
|
пояснений рядом с экраном.
|
||||||
|
|
||||||
## Свои объекты — отдельно
|
## Отдельный дашборд для задания
|
||||||
|
|
||||||
Не добавляйте графики в стендовый дашборд и не правьте его датасеты. При
|
Не добавляйте графики в стендовый дашборд и не правьте его датасеты. При
|
||||||
инициализации Superset импорт с `--overwrite` заменит ручные правки стендовых
|
инициализации Superset импорт с `--overwrite` заменит ручные правки стендовых
|
||||||
объектов файлами из репозитория. `make up` свой дашборд не стирает; срок жизни
|
объектов файлами из репозитория. `make up` свой дашборд не стирает; срок жизни
|
||||||
таких объектов описан в [доке Superset](../../architecture/superset.md), «Импорт
|
таких объектов описан в [справочнике Superset](../../architecture/superset.md), «Импорт
|
||||||
при каждой инициализации».
|
при каждой инициализации».
|
||||||
|
|
||||||
Заведите отдельные датасеты и графики, соберите из них свой дашборд.
|
Используйте свои датасеты и графики, соберите из них свой дашборд.
|
||||||
Назовите его, например, «Мои витрины DM». Это то же правило, по которому DDL
|
Назовите его, например, «Мои витрины DM».
|
||||||
домашек лежит в `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`;
|
||||||
@@ -54,16 +55,27 @@
|
|||||||
| `dq_errors_daily_v` | число ошибок по дню загрузки, источнику и классу |
|
| `dq_errors_daily_v` | число ошибок по дню загрузки, источнику и классу |
|
||||||
| `utm_effectiveness_v` | трафик, покупки и заявленную выручку по рекламным каналам |
|
| `utm_effectiveness_v` | трафик, покупки и заявленную выручку по рекламным каналам |
|
||||||
|
|
||||||
Для долей используйте слагаемые из витрин. Например, доля отказов считается
|
Для долей используйте суммы счетчиков из витрин. Например, доля отказов считается
|
||||||
как сумма `bounce_sessions`, деленная на сумму `sessions`. Среднее по готовым
|
как сумма `bounce_sessions`, деленная на сумму `sessions`. Среднее по готовым
|
||||||
долям для такого графика не подходит — урок задания 3 остается в силе.
|
долям для такого графика не подходит.
|
||||||
|
|
||||||
Названия графиков, осей и показателей должны объяснять экран без чтения SQL.
|
Названия графиков, осей и показателей должны объяснять экран без чтения SQL.
|
||||||
|
|
||||||
## Как проверить себя
|
## Период отчета
|
||||||
|
|
||||||
Эта лаба проверяется глазами. У расположения панелей и понятных подписей нет
|
Выберите явный период из модельного календаря и задайте его в графиках
|
||||||
счетного инварианта, поэтому задачи для нее в `homework_check` нет.
|
поведения посетителей. Последнюю доступную дату посмотрите в своих витринах.
|
||||||
|
Относительный период вроде `Last week` отсчитывается от настоящего времени
|
||||||
|
и может дать пустой экран.
|
||||||
|
|
||||||
|
Ошибки приема считаются по `load_date`, дню загрузки, а поведение посетителей
|
||||||
|
по модельному времени. Для панели ошибок выберите период загрузки отдельно
|
||||||
|
и укажите его в подписи. Общий фильтр времени для всего дашборда не требуется.
|
||||||
|
|
||||||
|
## Проверка результата
|
||||||
|
|
||||||
|
Автоматической проверки этого задания нет. Числа вы сверите запросами,
|
||||||
|
а понятность графиков проверите по подписям.
|
||||||
|
|
||||||
Откройте список дашбордов и проверьте:
|
Откройте список дашбордов и проверьте:
|
||||||
|
|
||||||
@@ -75,14 +87,26 @@
|
|||||||
серверную;
|
серверную;
|
||||||
- пустой график ошибок нормален, если в таблицах брака сейчас нет строк.
|
- пустой график ошибок нормален, если в таблицах брака сейчас нет строк.
|
||||||
|
|
||||||
Последний проход сделайте как читатель: найдите на экране самый заметный
|
По готовому экрану запишите три коротких вывода за выбранный период:
|
||||||
канал, страницу и устройство, затем проверьте, понятно ли из подписей, что
|
|
||||||
именно сравнивается. Если ответ приходится угадывать, поправьте график или
|
|
||||||
его название.
|
|
||||||
|
|
||||||
## Приемка
|
- какой рекламный канал дал больше покупок;
|
||||||
|
- какая страница получила больше просмотров;
|
||||||
|
- у какой категории устройств выше доля отказов.
|
||||||
|
|
||||||
- [ ] пять датасетов читают публичные представления витрин заданий 1–5;
|
Для каждого вывода укажите период и число, на которое опираетесь. Проверьте
|
||||||
- [ ] создан отдельный дашборд, стендовые объекты не изменены;
|
его запросом к своей витрине в SQL Lab с тем же периодом и способом расчета.
|
||||||
- [ ] каждая витрина представлена хотя бы одним графиком;
|
Сохраните запросы рядом с выводами. При равенстве значений назовите всех
|
||||||
- [ ] дашборд проверен глазами по разделу выше.
|
лидеров. Если данных нет, это тоже результат: подтвердите отсутствие строк
|
||||||
|
запросом.
|
||||||
|
|
||||||
|
Проверьте, можно ли найти эти ответы по одним подписям графиков. Если есть
|
||||||
|
возможность, попросите другого человека прочитать экран без ваших пояснений.
|
||||||
|
|
||||||
|
## Результат работы
|
||||||
|
|
||||||
|
- [ ] у вас есть пять датасетов по представлениям витрин заданий 1–5;
|
||||||
|
- [ ] вы собрали свой дашборд, сохранив стендовые объекты без изменений;
|
||||||
|
- [ ] на дашборде есть хотя бы один график от каждой витрины;
|
||||||
|
- [ ] вы проверили, что графики и подписи понятны по вопросам из раздела выше;
|
||||||
|
- [ ] вы записали три вывода и сверили числа с SQL; в отчете указаны периоды
|
||||||
|
поведения посетителей и ошибок приема.
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# В DBeaver работает, в Superset нет
|
||||||
|
|
||||||
|
Вы создадите представление, которое читается из DBeaver, но недоступно
|
||||||
|
в Superset. Затем найдете причину и исправите ее.
|
||||||
|
|
||||||
|
Факультатив доступен после [ступени 2](../2-1-superset-chart.md).
|
||||||
|
Свой дашборд для опыта не нужен. Используйте готовую витрину стенда
|
||||||
|
`dm.daily_traffic_v`.
|
||||||
|
|
||||||
|
## Подготовка
|
||||||
|
|
||||||
|
В DBeaver подключитесь к первому узлу пользователем `etl`, как описано
|
||||||
|
в разделе [«Выполнение SQL в DBeaver»](../README.md#выполнение-sql-в-dbeaver).
|
||||||
|
Для опыта создайте отдельное представление:
|
||||||
|
|
||||||
|
```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; оба клиента возвращают столько же строк,
|
||||||
|
сколько исходная витрина;
|
||||||
|
- [ ] после опыта вы удалили учебное представление с обоих узлов.
|
||||||
@@ -40,6 +40,24 @@
|
|||||||
- *расследование* — данные уже лежат на стенде или стенд намеренно ломается, а
|
- *расследование* — данные уже лежат на стенде или стенд намеренно ломается, а
|
||||||
менти ищет причину и объясняет ее.
|
менти ищет причину и объясняет ее.
|
||||||
|
|
||||||
|
### Как писать задания
|
||||||
|
|
||||||
|
Пишите для учащегося: объясняйте, чему он научится, зачем нужны действия
|
||||||
|
и как понять результат. Историю разработки оставляйте за пределами задания.
|
||||||
|
|
||||||
|
Начинайте задание коротким описанием предстоящей работы. Заголовки должны
|
||||||
|
называть содержание разделов: «Подготовка», «Создание витрины», «Проверка
|
||||||
|
результата». Шаблонные вопросы «Чему учит» и «Что сделать» не нужны.
|
||||||
|
|
||||||
|
Раздел проверки описывает действия для проверки и поиска ошибок.
|
||||||
|
Раздел результата перечисляет итог работы и то, что учащийся теперь может
|
||||||
|
объяснить. Если проверка уже встроена в шаги упражнения, повторять ее
|
||||||
|
отдельным разделом не нужно.
|
||||||
|
|
||||||
|
Структуру выбирайте по содержанию. Образцы — задания
|
||||||
|
про [дашборд Superset](../labs/optional/superset-dashboard.md) и
|
||||||
|
[расследование подключения](../labs/optional/superset-missing-object.md).
|
||||||
|
|
||||||
## 3. Основная программа — семь ступеней
|
## 3. Основная программа — семь ступеней
|
||||||
|
|
||||||
### Ступень 1. Построй пять витрин слоя DM
|
### Ступень 1. Построй пять витрин слоя DM
|
||||||
@@ -64,7 +82,7 @@
|
|||||||
ступени предстоит написать при реализации программы. Полный собственный дашборд
|
ступени предстоит написать при реализации программы. Полный собственный дашборд
|
||||||
остается факультативом в разделе «Superset и BI».
|
остается факультативом в разделе «Superset и BI».
|
||||||
|
|
||||||
### Ступень 3. Нарежь сессии сам
|
### Ступень 3. Нарежьте сессии сами
|
||||||
|
|
||||||
До того как открыть стендовый SQL нарезки сессий в DDS, менти самостоятельно
|
До того как открыть стендовый SQL нарезки сессий в DDS, менти самостоятельно
|
||||||
делит события на визиты. Сначала он сравнивает число своих сессий с эталоном
|
делит события на визиты. Сначала он сравнивает число своих сессий с эталоном
|
||||||
@@ -75,7 +93,7 @@
|
|||||||
Форма — построить свое по ТЗ и провести расследование. Обратная связь состоит
|
Форма — построить свое по ТЗ и провести расследование. Обратная связь состоит
|
||||||
из счетной сверки с `VisitID` и ручного сопоставления границ.
|
из счетной сверки с `VisitID` и ручного сопоставления границ.
|
||||||
|
|
||||||
### Ступень 4. Перепиши нарезку стенда
|
### Ступень 4. Перепишите нарезку стенда
|
||||||
|
|
||||||
Менти удаляет и заново пишет SQL нарезки сессий DDS: расчет объема, пересборку
|
Менти удаляет и заново пишет SQL нарезки сессий DDS: расчет объема, пересборку
|
||||||
и замену. Урок — изменить расчетную сущность внутри готового конвейера и
|
и замену. Урок — изменить расчетную сущность внутри готового конвейера и
|
||||||
@@ -86,7 +104,7 @@
|
|||||||
Форма — вычитание. Обратную связь дают `dq_check` и поштучная сверка,
|
Форма — вычитание. Обратную связь дают `dq_check` и поштучная сверка,
|
||||||
построенная на третьей ступени.
|
построенная на третьей ступени.
|
||||||
|
|
||||||
### Ступень 5. Сначала сторож, потом ломай: заказы
|
### Ступень 5. Научите проверки замечать ошибки в заказах
|
||||||
|
|
||||||
Сначала менти строит две недостающие DQ-проверки. Первая утверждает равенство
|
Сначала менти строит две недостающие DQ-проверки. Первая утверждает равенство
|
||||||
«цена × количество = items_total». Вторая сверяет слой с источником по дням. Их
|
«цена × количество = items_total». Вторая сверяет слой с источником по дням. Их
|
||||||
@@ -135,47 +153,106 @@
|
|||||||
|
|
||||||
## 4. Факультативы — пять разделов
|
## 4. Факультативы — пять разделов
|
||||||
|
|
||||||
Ниже перечислены только названия сюжетов и их уроки. Материал, формы и способы
|
Ниже перечислены сюжеты, их уроки и принятые решения по форме заданий. Исходный
|
||||||
проверки остаются в [инвентаризации](../research/2026-08-31-labs-inventory.md).
|
материал и способы проверки — в
|
||||||
|
[инвентаризации](../research/2026-08-31-labs-inventory.md).
|
||||||
|
|
||||||
### Почему выручка не сходится
|
### Почему выручка не сходится
|
||||||
|
|
||||||
Этот раздел рекомендован первым из факультативов. Он открывается после ступени
|
Этот раздел рекомендован первым из факультативов и открывается после ступени
|
||||||
1 основной программы: материал живет на стенде с этапа 6.
|
1 основной программы. Шесть сюжетов собраны в четыре лабораторные: три
|
||||||
|
расследования готовых данных и одно наблюдение во времени. Опоздания,
|
||||||
|
изменение выручки и история отмен используют одну историю заказов, поэтому
|
||||||
|
для них выбрана общая работа вместо трех отдельных.
|
||||||
|
|
||||||
- **Почему трекер недосчитывает покупки (#186).** Разрыв воронки подтверждений
|
Результат расследования — запрос сверки и инженерное решение, подтвержденное
|
||||||
показывает потерянные события и границу доверия к двум источникам.
|
данными: выбор источника, ключа, полей для точного расчета или правила
|
||||||
- **Почему повтор покупки завышает выручку (#187).** Движок убирает технический
|
пересчета и хранения истории. Объяснение суммы служит проверкой решения.
|
||||||
дубль строки, а повтор одной покупки приходится находить по деловому ключу.
|
Вариант закончить работы только объяснением показателей отклонен: он
|
||||||
- **Какая позиция объясняет разницу в сумме.** Расхождение суммы нужно
|
смещал акцент в анализ отчетов. Постройка новых DAG и подключение проверок
|
||||||
разложить до конкретной вычеркнутой позиции и объяснить до копейки.
|
к конвейеру оставлены основной программе, чтобы факультативы не повторяли
|
||||||
- **Был ли отмененный заказ оплачен.** Текущее состояние не заменяет историю, а
|
ее задания.
|
||||||
некоторые различия после истечения срока хранения сырых данных уже не
|
|
||||||
восстановить.
|
- **[Куда пропали покупки](../labs/optional/missing-purchases.md) (#186).**
|
||||||
- **Когда отчет за день можно считать окончательным (#188).** Отчет за день
|
Сверка по номерам заказов обосновывает источник витрины и условие
|
||||||
становится окончательным лишь после того, как источник больше не может
|
окончательной проверки полноты. Счетчики событий помогают обнаружить
|
||||||
изменить этот день.
|
расхождение, но не устанавливают место потери.
|
||||||
- **Почему выручка за день еще меняется.** Внутри окна изменяемости выручка
|
- **[Почему повтор покупки завышает выручку](../labs/optional/duplicate-purchases.md) (#187).**
|
||||||
движется, а после закрытия дня фиксируется.
|
Учащийся собирает расчет с одной строкой на заказ, проверяет ключ и полноту
|
||||||
|
набора номеров, отличает его зерно от зерна исходных событий.
|
||||||
|
- **[Какая позиция объясняет разницу в сумме](../labs/optional/order-amount-difference.md).**
|
||||||
|
Сверка позиций обосновывает выбор исходных полей и типа денег, отделяет
|
||||||
|
ошибку собственного разбора от различия данных события и заказа.
|
||||||
|
- **[Почему вчерашняя выручка меняется сегодня](../labs/optional/revenue-over-time.md) (#188).**
|
||||||
|
Наблюдение опозданий и смены статусов обосновывает правило пересчета при
|
||||||
|
ежедневной загрузке без пропусков. Учащийся определяет момент закрытия дня
|
||||||
|
и требования к истории для воспроизведения расчета и проверки факта оплаты.
|
||||||
|
|
||||||
|
В четвертой лабораторной выбрано собственное наблюдение: учащийся продвигает
|
||||||
|
мир по одному дню через Airflow и сохраняет результаты после обновления витрин
|
||||||
|
в две локальные таблицы ClickHouse под `student`: состояния заказов и итоги
|
||||||
|
наблюдений. Сравнение выполняется SQL-запросами в том же подключении DBeaver.
|
||||||
|
Учащийся может писать только в `default`; источники доступны ему на чтение.
|
||||||
|
Отдельная строка итогов фиксирует и наблюдение без заказов. Экспорт в CSV
|
||||||
|
отклонен как основной способ: он требовал неописанного перехода к другому
|
||||||
|
инструменту или обратному импорту файлов.
|
||||||
|
Готовые слепки и сочетание готового примера с собственным наблюдением
|
||||||
|
отклонены: нужную историю можно получить на стенде и самому увидеть,
|
||||||
|
как новые данные меняют уже рассчитанный отчет.
|
||||||
|
|
||||||
|
День считается закрытым после получения и обработки последнего слепка
|
||||||
|
семидневного окна изменяемости; этот слепок доставляется следующим модельным
|
||||||
|
днем. Одного совпадения сумм в соседних наблюдениях для такого вывода
|
||||||
|
недостаточно. История отмен тоже имеет границу: оплата и отмена между двумя
|
||||||
|
дневными слепками могут остаться невидимыми. Отсутствие наблюдаемой оплаты
|
||||||
|
не доказывает, что оплаты не было.
|
||||||
|
|
||||||
### Тонкий анализ данных
|
### Тонкий анализ данных
|
||||||
|
|
||||||
Открывается после ступени 1 основной программы.
|
Открывается после ступени 1 основной программы.
|
||||||
|
|
||||||
- **Что мы знали о пользователе в тот день (#145).** Текущее знание и знание на
|
- **[Как новые заказы меняют отчет о посетителях](../labs/optional/identity-over-time.md) (#145).**
|
||||||
дату дают разные отчеты; сведения из будущего меняют прошлое.
|
Учащийся собирает связи с подтверждающими покупками и сохраняет три
|
||||||
|
ежедневных наблюдения. На одном наборе прежних сессий сравнивает расчет
|
||||||
|
с прежними и новыми связями, выбирает историю для воспроизведения расчета.
|
||||||
- **Добавь названия регионов в отчет (#143).** Менти собирает свой словарь по
|
- **Добавь названия регионов в отчет (#143).** Менти собирает свой словарь по
|
||||||
образцу каталога и обогащает отчет через `dictGet`.
|
образцу каталога и обогащает отчет через `dictGet`.
|
||||||
|
|
||||||
|
Работа о связях продолжается в инженерном разделе лабораторной про
|
||||||
|
инкрементальный `daily_traffic`. В обеих учащийся продвигает мир через
|
||||||
|
`world_next_day` и `etl_pipeline`, сохраняет наблюдения под `student` в
|
||||||
|
свои локальные таблицы `default` и сравнивает их SQL-запросами.
|
||||||
|
|
||||||
|
Время первой подтверждающей покупки и время обнаружения связи разделены.
|
||||||
|
Минимум времени `purchase` не принят за границу появления новых связей:
|
||||||
|
заказ может поступить позже события. Нужные изменения определяются по
|
||||||
|
разности сохраненных наборов пар. Ежедневное наблюдение ограничивает момент
|
||||||
|
появления связи интервалом; для пар начального наблюдения более ранняя
|
||||||
|
история неизвестна. История сессий в этом опыте растет без исправлений
|
||||||
|
прошлых дней, а связи не отзываются — поэтому сохраненных наборов связей
|
||||||
|
достаточно для сравнения расчетов на прежних сессиях.
|
||||||
|
|
||||||
### Superset и BI
|
### Superset и BI
|
||||||
|
|
||||||
Открывается после ступени 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` на второй ноде; после создания
|
||||||
|
на кластере обе ноды возвращают тот же счет, что исходная витрина.
|
||||||
|
|
||||||
### Инженерия стенда
|
### Инженерия стенда
|
||||||
|
|
||||||
@@ -210,9 +287,28 @@
|
|||||||
- **Что будет, если день заменился не на всех шардах.** Замена дня завершается
|
- **Что будет, если день заменился не на всех шардах.** Замена дня завершается
|
||||||
отдельно на каждом шарде, поэтому целостность результата подтверждает
|
отдельно на каждом шарде, поэтому целостность результата подтверждает
|
||||||
отдельная сверка.
|
отдельная сверка.
|
||||||
- **Как обновлять витрину без полной пересборки (#149) — повышенная
|
- **[Как обновлять витрину без полной пересборки](../labs/optional/incremental-daily-traffic.md)
|
||||||
сложность.** Экономия на пересчете требует хранить историю изменений, которую
|
(#149) — повышенная сложность.** Продолжение работы о связях пользователей:
|
||||||
полная пересборка стирает.
|
учащийся строит свою витрину, выбирает затронутые дни по новым сессиям
|
||||||
|
и связям, сверяет весь результат с `dm.daily_traffic_v`. Журнал успешных
|
||||||
|
загрузок позволяет повторить опыт с частично записанным результатом.
|
||||||
|
|
||||||
|
Инкрементальная работа начинается с полной загрузки текущего состояния,
|
||||||
|
затем охватывает два новых модельных дня. Обновление собственной витрины
|
||||||
|
выполнено вставкой версий в `ReplacingMergeTree`, чтение — с `FINAL`.
|
||||||
|
Это оставляет права `student` прежними и не требует менять конвейер стенда.
|
||||||
|
Версия — дата наблюдения; ключ — день отчета. История связей хранится
|
||||||
|
отдельно от версий витрины. Выбор опирается на
|
||||||
|
[договор движка](https://clickhouse.com/docs/engines/table-engines/mergetree-family/replacingmergetree):
|
||||||
|
максимальная версия по ключу и удаление прежних версий при слияниях.
|
||||||
|
DDL, обновление вставкой и чтение с `FINAL` проверены под `student`
|
||||||
|
на ClickHouse 26.3.17.56.
|
||||||
|
|
||||||
|
Полный пересчет остается эталоном, а экономия оценивается числом выбранных
|
||||||
|
дней. На малом объеме ускорение не обещано. Поиск связей продолжает читать
|
||||||
|
историю источников целиком; инкрементальным становится расчет дневной витрины.
|
||||||
|
Полная замена стендового ETL отклонена: для учебного сравнения достаточно
|
||||||
|
своей витрины и журнала рядом с работающим конвейером.
|
||||||
|
|
||||||
### Что делать, когда стенд сломался
|
### Что делать, когда стенд сломался
|
||||||
|
|
||||||
|
|||||||
+78
-81
@@ -1,98 +1,95 @@
|
|||||||
# Генератор кликстрима
|
# Генератор событий и заказов
|
||||||
|
|
||||||
Клиентская сторона стенда: отсюда берётся поток событий — широкое событие по
|
Генератор создает данные двух источников интернет-магазина. Счетчик сайта
|
||||||
образцу облачной выгрузки Яндекс Метрики. Устройство и принятые решения —
|
отправляет в топик Kafka `hits` события: просмотр страницы, добавление товара
|
||||||
спека [«Генератор (этап 2)»](../docs/specs/2026-08-01-generator.md).
|
в корзину и покупку. Серверная часть магазина выгружает в `orders` дневной
|
||||||
|
слепок — состояние заказов, оформленных за последние семь модельных дней.
|
||||||
|
Оба источника работают; хранилище принимает события непрерывно, заказы —
|
||||||
|
по команде Airflow.
|
||||||
|
|
||||||
События уже есть и уже уезжают: день-функция отдаёт по паре (зерно, D)
|
Событие описывает отдельное действие посетителя. Заказ после оформления
|
||||||
упорядоченный поток трёх видов — просмотр страницы, корзина, покупка, —
|
может измениться, например перейти в статус `paid` или `cancelled`.
|
||||||
канонический сериализатор превращает его в JSON, а проигрыватель гонит в файл
|
Поэтому один `order_id` встречается в нескольких слепках. Часть заказов
|
||||||
или в Kafka. Клиентская сторона на этом целая. Второй источник строится:
|
впервые появляется с задержкой в один или два дня; конец семидневного окна
|
||||||
день отдаёт и заказы бэкенда — проекцию своих покупок с деньгами магазина, —
|
от этого не сдвигается. Правила изменений и задержек — в
|
||||||
а судьба заказа и отправка слепка идут следующими тикетами.
|
[описании судьбы заказа](../docs/architecture/orders/fate.md).
|
||||||
|
|
||||||
|
Слепок снимается на конец модельного дня и отправляется на следующий день.
|
||||||
|
Команда `snapshot --day 1` отправляет слепок дня 0, а `snapshot --day 0`
|
||||||
|
не отправляет ничего: до начала модели заказов нет. События и слепки
|
||||||
|
отправляются отдельными запусками генератора.
|
||||||
|
|
||||||
## Как это работает
|
## Как это работает
|
||||||
|
|
||||||
Корневое зерно ветвится на подпотоки, из них складываются когорты, аудитория
|
Зерно генерации задает случайные выборы: какие посетители придут,
|
||||||
дня, визиты, строки событий и заказы. План поднимает состав мира по требованию,
|
какие страницы откроют и что купят. При неизменных коде, каталоге
|
||||||
а день-функция проживает один день без накопленного состояния. Код этой
|
и настройках одинаковые зерно и номер дня дают одинаковые данные.
|
||||||
цепочки, ее схема и запуск D0 собраны в
|
День можно рассчитать без генерации всех предыдущих дней.
|
||||||
[путеводителе](../docs/architecture/generator/README.md).
|
|
||||||
|
Сначала рассчитываются посещения и покупки. Из покупки получаются событие
|
||||||
|
`purchase` со стороны сайта и заказ со стороны магазина. Номер связывает
|
||||||
|
их: `purchaseID` в событии соответствует `order_id` заказа. Суммы могут
|
||||||
|
различаться: магазин учитывает скидку, доставку и удаленные позиции.
|
||||||
|
Деньги генератор считает целыми копейками; в JSON заказа это строки
|
||||||
|
в рублях с двумя знаками после точки, например `"1289.99"`.
|
||||||
|
|
||||||
|
Готовые записи превращаются в JSON и отправляются в файл или Kafka:
|
||||||
|
одна запись занимает одну строку файла или одно сообщение. Событие содержит
|
||||||
|
47 полей по [описанию выгрузки](../docs/formats/clickstream-event.md),
|
||||||
|
запись заказа — поля [формата слепка](../docs/architecture/orders/snapshot.md#запись-на-проводе).
|
||||||
|
Подробное устройство генератора разобрано в
|
||||||
|
[техническом справочнике](../docs/architecture/generator/README.md).
|
||||||
|
|
||||||
## Что где лежит
|
## Что где лежит
|
||||||
|
|
||||||
- `src/clickstream_generator/world.py` — конфигурация мира: все его числа
|
Все перечисленные модули находятся в `src/clickstream_generator/`.
|
||||||
одним местом. Правка любого — смена мира; крутить их и предлагается.
|
|
||||||
- `src/clickstream_generator/seeds.py` — иерархия зёрен: кто из какого
|
| Модули | Назначение |
|
||||||
подпотока берёт случайность. На ней держится весь детерминизм.
|
|---|---|
|
||||||
- `src/clickstream_generator/plan.py` — план состава: кто есть в мире в
|
| `world.py`, `seeds.py`, `plan.py` | Настройки мира, случайные выборы и план посещений |
|
||||||
день D. Когорты, приток, двухкуковые пары, паспорта кук, человек за кукой
|
| `reference.py`, `catalog.py` | Устройства, города, источники трафика и каталог товаров |
|
||||||
и счётчики — до генерации событий.
|
| `day.py`, `commerce.py` | Визиты, события сайта и покупки |
|
||||||
- `src/clickstream_generator/weights.py` — выбор по целым весам: один приём
|
| `orders.py` | Состояние заказов на дату слепка, изменения и задержки |
|
||||||
на весь генератор, чтобы дисциплина целочисленной случайности не жила
|
| `serialize.py`, `player.py`, `sinks.py` | Запись JSON и отправка в файл или Kafka |
|
||||||
копиями.
|
| `cli.py` | Команды и параметры запуска |
|
||||||
- `src/clickstream_generator/reference.py` — справочники: устройства,
|
| `schema.py`, `schema_doc.py` | Описания колонок события и сборка документа выгрузки |
|
||||||
города, источники трафика, карта сайта. Таблицы-литералы: доля живёт в
|
| `inventory.py` | Сборка паспорта мира с контрольными суммами и счетчиками |
|
||||||
строке, которой принадлежит.
|
|
||||||
- `src/clickstream_generator/catalog.py` — каталог товаров из
|
Тесты в `tests/` проверяют формат, правила модели и повторяемость данных.
|
||||||
`data/catalog/products.csv`, общего у генератора и словаря ClickHouse.
|
Описание выгрузки и [паспорт мира](../data/world-inventory.json) собираются
|
||||||
- `src/clickstream_generator/day.py` — день-функция: визиты, страницы,
|
из кода; вручную их не редактируют.
|
||||||
атрибуция, устройство и гео. Там же правила резки визитов и шов, на
|
|
||||||
который садятся торговые события; прожитый день отдаёт обе половины —
|
|
||||||
поток событий и заказы.
|
|
||||||
- `src/clickstream_generator/commerce.py` — торговые события: что легло в
|
|
||||||
корзину, что из этого куплено, деньги, номера заказов и сырой `ecommerce`.
|
|
||||||
Своя случайность, поэтому правка торговли трафик не двигает.
|
|
||||||
- `src/clickstream_generator/orders.py` — заказы бэкенда: вторая проекция
|
|
||||||
покупки. Личность покупателя и деньги магазина — скидка по промокоду,
|
|
||||||
доставка, итог; своя случайность, ветвящаяся по дню рождения заказа.
|
|
||||||
- `src/clickstream_generator/ids.py` — номера событий: неповторяющиеся и
|
|
||||||
ниже 2^53. Обещание одно на обе половины дня, поэтому и живёт отдельно.
|
|
||||||
- `src/clickstream_generator/serialize.py` — канонический сериализатор:
|
|
||||||
единственное место, где событие целиком превращается в JSON. Порядок ключей,
|
|
||||||
все 47 колонок всегда и форма на проводе — ISO-8601.
|
|
||||||
- `src/clickstream_generator/sinks.py` — приёмники: файл (одно событие — одна
|
|
||||||
строка) и Kafka (одно событие — одно сообщение). Про содержимое они не
|
|
||||||
знают; там же довод, почему у сообщения нет ключа.
|
|
||||||
- `src/clickstream_generator/player.py` — проигрыватель: гонит дни в приёмник
|
|
||||||
пачкой или с темпом живого дня. Состояния не хранит.
|
|
||||||
- `src/clickstream_generator/cli.py` — интерфейс запуска: параметры
|
|
||||||
аргументами или окружением, логи в стандартный вывод, итог кодом возврата.
|
|
||||||
- `src/clickstream_generator/schema.py` — контракт схемы: чистые данные о
|
|
||||||
колонках выгрузки. Собственность генератора; из него выводятся сам
|
|
||||||
генератор, его валидация и описание выгрузки в доках.
|
|
||||||
- `src/clickstream_generator/schema_doc.py` — сборка «описания выгрузки»
|
|
||||||
([`docs/formats/clickstream-event.md`](../docs/formats/clickstream-event.md))
|
|
||||||
из контракта. Документ руками не правят — пересобирают.
|
|
||||||
- `src/clickstream_generator/inventory.py` — сборка паспорта мира
|
|
||||||
([`data/world-inventory.json`](../data/world-inventory.json)): шапка паспорта
|
|
||||||
и хеши восьми дней, которыми наполняется стенд, плюс счётчики классов
|
|
||||||
расхождений по заказам для дней с закрытым окном. Руками не правят —
|
|
||||||
пересобирают целью `make inventory`.
|
|
||||||
- `tests/` — инварианты контракта, свежесть описания и обещания мира:
|
|
||||||
чистота от зерна, приток, гарантия двухкуковых пар, форма суточной волны
|
|
||||||
и сборка визитов по задокументированным правилам. Там же побайтовое
|
|
||||||
обещание, доведённое до диска: два прогона дня в файл дают тот же файл, а
|
|
||||||
строк в нём ровно столько, сколько событий.
|
|
||||||
|
|
||||||
## Команды
|
## Команды
|
||||||
|
|
||||||
Проиграть день — раздел [«Как запустить
|
Для запуска из каталога `generator/` используйте
|
||||||
генератор»](../docs/architecture/world.md#как-запустить-генератор)
|
`uv run python -m clickstream_generator <команда>`. Справку можно открыть
|
||||||
справочника мира и `--help`.
|
через `--help`, в том числе после имени команды.
|
||||||
|
|
||||||
Из этого каталога — цели генератора живут здесь, а не в корне:
|
| Команда | Что отправляет | Особые параметры |
|
||||||
|
|---|---|---|
|
||||||
|
| `batch` | События дня без пауз | `--limit N` ограничивает число событий за весь запуск |
|
||||||
|
| `live` | События постепенно, с ускорением | `--speed X`, по умолчанию 60: сутки занимают около 24 минут |
|
||||||
|
| `snapshot` | Слепок заказов предыдущего дня | `--day D` задает день отправки |
|
||||||
|
|
||||||
|
Для всех команд нужны номер дня `--day` и приемник: `--file` либо
|
||||||
|
`--brokers` вместе с `--topic`. Общие параметры `--seed` и `--days`
|
||||||
|
задают зерно и число последовательных дней. Параметры можно передать
|
||||||
|
и через окружение; имена и примеры собраны в
|
||||||
|
[справочнике запуска](../docs/architecture/world.md#как-запустить-генератор).
|
||||||
|
Прямой запуск генератора не продвигает сохраненную позицию времени стенда —
|
||||||
|
ей управляют [даги модельного времени](../docs/handbook/airflow-dags.md#генерация-модельных-дней).
|
||||||
|
|
||||||
|
Из каталога `generator/` доступны:
|
||||||
|
|
||||||
- `make test` — тесты генератора;
|
- `make test` — тесты генератора;
|
||||||
- `make lint` — ruff: проверка и формат;
|
- `make lint` — проверка кода и форматирования через ruff;
|
||||||
- `make typecheck` — ty: проверка типов;
|
- `make typecheck` — проверка типов через ty;
|
||||||
- `make demo` — пройти стадии канонического D0 на одном экране;
|
- `make demo` — показать стадии расчета первого дня, D0;
|
||||||
- `make docs` — пересобрать описание выгрузки;
|
- `make docs` — пересобрать описание выгрузки;
|
||||||
- `make inventory` — пересобрать паспорт мира.
|
- `make inventory` — пересобрать паспорт мира.
|
||||||
|
|
||||||
В корне репозитория `make lint` тоже есть, но он про код стенда — даги и
|
В корне `make lint` проверяет даги, код Superset и сборку карты архитектуры.
|
||||||
Superset. Одноимённые цели за разными дверями охватывают разное:
|
Корневые `make generate-batch` и `make generate-live` запускают события
|
||||||
[карта проверок](../docs/architecture/testing.md).
|
через контейнер генератора. Состав проверок — в
|
||||||
|
[карте проверок](../docs/architecture/testing.md).
|
||||||
Python и зависимости — через `uv`, версии закреплены в `uv.lock`: на этом
|
Python и зависимости запускаются через `uv`; версии закреплены в `uv.lock`.
|
||||||
держится обещание побайтовой воспроизводимости (спека, раздел 2).
|
|
||||||
@@ -420,7 +420,7 @@ COLUMNS: tuple[Column, ...] = (
|
|||||||
numpy_dtype="int64",
|
numpy_dtype="int64",
|
||||||
normalized_name="product_price",
|
normalized_name="product_price",
|
||||||
group=ColumnGroup.ECOMMERCE,
|
group=ColumnGroup.ECOMMERCE,
|
||||||
comment="цена за штуку целым числом: деньги генератор считает целыми",
|
comment="цена за штуку, округленная до целых рублей",
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
name="productQuantity",
|
name="productQuantity",
|
||||||
|
|||||||
@@ -61,6 +61,11 @@
|
|||||||
<query>GRANT SELECT ON system.settings</query>
|
<query>GRANT SELECT ON system.settings</query>
|
||||||
</grants>
|
</grants>
|
||||||
</analyst_reader>
|
</analyst_reader>
|
||||||
|
<student_writer>
|
||||||
|
<grants>
|
||||||
|
<query>GRANT SELECT, INSERT, CREATE TABLE, DROP TABLE ON default.*</query>
|
||||||
|
</grants>
|
||||||
|
</student_writer>
|
||||||
<monitoring_reader>
|
<monitoring_reader>
|
||||||
<grants>
|
<grants>
|
||||||
<query>GRANT SELECT ON stg.*</query>
|
<query>GRANT SELECT ON stg.*</query>
|
||||||
@@ -113,6 +118,14 @@
|
|||||||
<query>GRANT analyst_reader</query>
|
<query>GRANT analyst_reader</query>
|
||||||
</grants>
|
</grants>
|
||||||
</analyst>
|
</analyst>
|
||||||
|
<student>
|
||||||
|
<password from_env="CLICKHOUSE_STUDENT_PASSWORD"/>
|
||||||
|
<quota>default</quota>
|
||||||
|
<grants>
|
||||||
|
<query>GRANT analyst_reader</query>
|
||||||
|
<query>GRANT student_writer</query>
|
||||||
|
</grants>
|
||||||
|
</student>
|
||||||
<grafana>
|
<grafana>
|
||||||
<password from_env="CLICKHOUSE_GRAFANA_PASSWORD"/>
|
<password from_env="CLICKHOUSE_GRAFANA_PASSWORD"/>
|
||||||
<quota>default</quota>
|
<quota>default</quota>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"panels": [
|
"panels": [
|
||||||
{
|
{
|
||||||
"type": "timeseries",
|
"type": "timeseries",
|
||||||
"title": "Процессор: сколько ядер занимает нода",
|
"title": "Процессор: сколько ядер использует узел",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "grafana-clickhouse-datasource",
|
"type": "grafana-clickhouse-datasource",
|
||||||
"uid": "clickhouse"
|
"uid": "clickhouse"
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "timeseries",
|
"type": "timeseries",
|
||||||
"title": "Память: сколько нода занимает и сколько ей отведено",
|
"title": "Память: использование и лимит узла",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "grafana-clickhouse-datasource",
|
"type": "grafana-clickhouse-datasource",
|
||||||
"uid": "clickhouse"
|
"uid": "clickhouse"
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "timeseries",
|
"type": "timeseries",
|
||||||
"title": "Диски: сколько нода читает и пишет",
|
"title": "Диски: скорость чтения и записи узла",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "grafana-clickhouse-datasource",
|
"type": "grafana-clickhouse-datasource",
|
||||||
"uid": "clickhouse"
|
"uid": "clickhouse"
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "table",
|
"type": "table",
|
||||||
"title": "Части: сколько их у таблицы и по сколько строк в каждой",
|
"title": "Части таблиц: количество и среднее число строк в части",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "grafana-clickhouse-datasource",
|
"type": "grafana-clickhouse-datasource",
|
||||||
"uid": "clickhouse"
|
"uid": "clickhouse"
|
||||||
@@ -199,7 +199,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "timeseries",
|
"type": "timeseries",
|
||||||
"title": "Слияния: сколько частей они съедают",
|
"title": "Слияния: число объединенных частей",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "grafana-clickhouse-datasource",
|
"type": "grafana-clickhouse-datasource",
|
||||||
"uid": "clickhouse"
|
"uid": "clickhouse"
|
||||||
@@ -280,11 +280,12 @@
|
|||||||
"min": 0
|
"min": 0
|
||||||
},
|
},
|
||||||
"overrides": []
|
"overrides": []
|
||||||
}
|
},
|
||||||
|
"description": "Узлы дерева Keeper (znode) — записи со служебными данными координации ClickHouse."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "table",
|
"type": "table",
|
||||||
"title": "Распределённый DDL: что прошло по кластеру и чем кончилось",
|
"title": "Распределенный DDL: результаты команд на узлах кластера",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "grafana-clickhouse-datasource",
|
"type": "grafana-clickhouse-datasource",
|
||||||
"uid": "clickhouse"
|
"uid": "clickhouse"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"panels": [
|
"panels": [
|
||||||
{
|
{
|
||||||
"type": "stat",
|
"type": "stat",
|
||||||
"title": "Свежесть: сколько прошло с последней доехавшей строки",
|
"title": "Свежесть: время с последней загрузки в ODS",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "grafana-clickhouse-datasource",
|
"type": "grafana-clickhouse-datasource",
|
||||||
"uid": "clickhouse"
|
"uid": "clickhouse"
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "table",
|
"type": "table",
|
||||||
"title": "Брак: сколько приехавшего не разобралось",
|
"title": "Брак: доля строк с ошибками разбора",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "grafana-clickhouse-datasource",
|
"type": "grafana-clickhouse-datasource",
|
||||||
"uid": "clickhouse"
|
"uid": "clickhouse"
|
||||||
@@ -72,11 +72,28 @@
|
|||||||
"format": 1,
|
"format": 1,
|
||||||
"rawSql": "SELECT\n source AS \"Источник\",\n countIf(NOT is_error) AS \"Приехало\",\n countIf(is_error) AS \"Из них в брак\",\n 100 * countIf(is_error) / countIf(NOT is_error) AS \"Доля брака, %\"\nFROM\n(\n SELECT 'события' AS source, 0 AS is_error, _load_ts FROM stg.hits_raw_dist\n UNION ALL\n SELECT 'события', 1, _load_ts FROM ods.event_errors_dist\n UNION ALL\n SELECT 'заказы', 0, _load_ts FROM stg.orders_raw_dist\n UNION ALL\n SELECT 'заказы', 1, _load_ts FROM ods.order_errors_dist\n)\nWHERE $__timeFilter(_load_ts)\nGROUP BY source\nORDER BY source"
|
"rawSql": "SELECT\n source AS \"Источник\",\n countIf(NOT is_error) AS \"Приехало\",\n countIf(is_error) AS \"Из них в брак\",\n 100 * countIf(is_error) / countIf(NOT is_error) AS \"Доля брака, %\"\nFROM\n(\n SELECT 'события' AS source, 0 AS is_error, _load_ts FROM stg.hits_raw_dist\n UNION ALL\n SELECT 'события', 1, _load_ts FROM ods.event_errors_dist\n UNION ALL\n SELECT 'заказы', 0, _load_ts FROM stg.orders_raw_dist\n UNION ALL\n SELECT 'заказы', 1, _load_ts FROM ods.order_errors_dist\n)\nWHERE $__timeFilter(_load_ts)\nGROUP BY source\nORDER BY source"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Приехало"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "displayName",
|
||||||
|
"value": "Загружено в STG"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "table",
|
"type": "table",
|
||||||
"title": "Дневной слепок заказов: какой доехал, когда и после какой паузы",
|
"title": "Дневной слепок заказов: дата и время загрузки",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "grafana-clickhouse-datasource",
|
"type": "grafana-clickhouse-datasource",
|
||||||
"uid": "clickhouse"
|
"uid": "clickhouse"
|
||||||
@@ -100,11 +117,28 @@
|
|||||||
"format": 1,
|
"format": 1,
|
||||||
"rawSql": "SELECT\n \"Слепок\",\n \"Доехал\",\n dateDiff('second', prev, \"Доехал\") AS \"После прошлого, с\",\n \"Строк\"\nFROM\n(\n SELECT\n JSONExtractString(raw, 'snapshot_date') AS \"Слепок\",\n max(_load_ts) AS \"Доехал\",\n count() AS \"Строк\",\n -- Обнуляемый тип нужен, чтобы у самого раннего слепка пауза осталась\n -- пустой, а не превратилась в разницу с началом эпохи.\n lagInFrame(toNullable(max(_load_ts))) OVER (ORDER BY \"Слепок\") AS prev\n FROM stg.orders_raw_dist\n GROUP BY \"Слепок\"\n)\nORDER BY \"Слепок\" DESC\nLIMIT 10"
|
"rawSql": "SELECT\n \"Слепок\",\n \"Доехал\",\n dateDiff('second', prev, \"Доехал\") AS \"После прошлого, с\",\n \"Строк\"\nFROM\n(\n SELECT\n JSONExtractString(raw, 'snapshot_date') AS \"Слепок\",\n max(_load_ts) AS \"Доехал\",\n count() AS \"Строк\",\n -- Обнуляемый тип нужен, чтобы у самого раннего слепка пауза осталась\n -- пустой, а не превратилась в разницу с началом эпохи.\n lagInFrame(toNullable(max(_load_ts))) OVER (ORDER BY \"Слепок\") AS prev\n FROM stg.orders_raw_dist\n GROUP BY \"Слепок\"\n)\nORDER BY \"Слепок\" DESC\nLIMIT 10"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Доехал"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "displayName",
|
||||||
|
"value": "Время загрузки"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "timeseries",
|
"type": "timeseries",
|
||||||
"title": "Поток: сколько строк приезжает в сырой слой",
|
"title": "Поток: число сообщений, загруженных в STG",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "grafana-clickhouse-datasource",
|
"type": "grafana-clickhouse-datasource",
|
||||||
"uid": "clickhouse"
|
"uid": "clickhouse"
|
||||||
@@ -136,7 +170,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "timeseries",
|
"type": "timeseries",
|
||||||
"title": "Сходимость: произведено в Kafka против приземлившегося в ClickHouse",
|
"title": "Сходимость: число событий в Kafka и ClickHouse",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "datasource",
|
"type": "datasource",
|
||||||
"uid": "-- Mixed --"
|
"uid": "-- Mixed --"
|
||||||
@@ -161,7 +195,7 @@
|
|||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "sum(kafka_topic_partition_current_offset{topic=\"hits\"}) - sum(kafka_topic_partition_current_offset{topic=\"hits\"} offset $__interval)",
|
"expr": "sum(kafka_topic_partition_current_offset{topic=\"hits\"}) - sum(kafka_topic_partition_current_offset{topic=\"hits\"} offset $__interval)",
|
||||||
"legendFormat": "произведено в Kafka",
|
"legendFormat": "поступило в Kafka",
|
||||||
"range": true
|
"range": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -187,7 +221,7 @@
|
|||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"id": "displayName",
|
"id": "displayName",
|
||||||
"value": "приземлилось в ClickHouse"
|
"value": "загружено в ClickHouse"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -197,7 +231,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "timeseries",
|
"type": "timeseries",
|
||||||
"title": "Отставание чтения из Kafka: непрочитанных сообщений",
|
"title": "Отставание чтения из Kafka: число непрочитанных сообщений",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "prometheus",
|
"type": "prometheus",
|
||||||
"uid": "prometheus"
|
"uid": "prometheus"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"panels": [
|
"panels": [
|
||||||
{
|
{
|
||||||
"type": "table",
|
"type": "table",
|
||||||
"title": "Тяжёлые запросы: что прочитало больше всех",
|
"title": "Запросы с наибольшим числом прочитанных строк",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "grafana-clickhouse-datasource",
|
"type": "grafana-clickhouse-datasource",
|
||||||
"uid": "clickhouse"
|
"uid": "clickhouse"
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "table",
|
"type": "table",
|
||||||
"title": "Кто спрашивает: сколько запросов и какой у них аппетит",
|
"title": "Пользователи: число запросов и потребление ресурсов",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "grafana-clickhouse-datasource",
|
"type": "grafana-clickhouse-datasource",
|
||||||
"uid": "clickhouse"
|
"uid": "clickhouse"
|
||||||
@@ -93,6 +93,10 @@
|
|||||||
{
|
{
|
||||||
"id": "unit",
|
"id": "unit",
|
||||||
"value": "bytes"
|
"value": "bytes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "displayName",
|
||||||
|
"value": "Максимальная память запроса"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -101,7 +105,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "table",
|
"type": "table",
|
||||||
"title": "Ошибки: на чём запросы споткнулись",
|
"title": "Ошибки выполнения запросов",
|
||||||
"datasource": {
|
"datasource": {
|
||||||
"type": "grafana-clickhouse-datasource",
|
"type": "grafana-clickhouse-datasource",
|
||||||
"uid": "clickhouse"
|
"uid": "clickhouse"
|
||||||
@@ -128,7 +132,20 @@
|
|||||||
],
|
],
|
||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {},
|
"defaults": {},
|
||||||
"overrides": []
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Чем кончилось"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "displayName",
|
||||||
|
"value": "Ошибка"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
slice_name: Сверка purchase и заказов
|
slice_name: Сверка событий покупки и заказов
|
||||||
description: null
|
description: null
|
||||||
certified_by: null
|
certified_by: null
|
||||||
certification_details: null
|
certification_details: null
|
||||||
@@ -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,9 +151,9 @@ position:
|
|||||||
children: []
|
children: []
|
||||||
meta:
|
meta:
|
||||||
chartId: 2
|
chartId: 2
|
||||||
height: 50
|
height: 44
|
||||||
width: 12
|
width: 12
|
||||||
sliceName: Сверка purchase и заказов
|
sliceName: Сверка событий покупки и заказов
|
||||||
uuid: 19600000-0000-4000-8000-000000000102
|
uuid: 19600000-0000-4000-8000-000000000102
|
||||||
ROW-traffic:
|
ROW-traffic:
|
||||||
id: ROW-traffic
|
id: ROW-traffic
|
||||||
@@ -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: Время загрузки
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ columns:
|
|||||||
datetime_format: null
|
datetime_format: null
|
||||||
extra: null
|
extra: null
|
||||||
- column_name: declared_revenue
|
- column_name: declared_revenue
|
||||||
verbose_name: Выручка purchase
|
verbose_name: Заявленная сумма покупки из события
|
||||||
is_dttm: false
|
is_dttm: false
|
||||||
is_active: true
|
is_active: true
|
||||||
type: NULLABLE(DECIMAL(18, 2))
|
type: NULLABLE(DECIMAL(18, 2))
|
||||||
@@ -120,7 +120,7 @@ columns:
|
|||||||
datetime_format: null
|
datetime_format: null
|
||||||
extra: null
|
extra: null
|
||||||
- column_name: order_id
|
- column_name: order_id
|
||||||
verbose_name: Заказ
|
verbose_name: Номер заказа
|
||||||
is_dttm: false
|
is_dttm: false
|
||||||
is_active: true
|
is_active: true
|
||||||
type: STRING
|
type: STRING
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -109,7 +125,7 @@ columns:
|
|||||||
datetime_format: null
|
datetime_format: null
|
||||||
extra: null
|
extra: null
|
||||||
- column_name: units
|
- column_name: units
|
||||||
verbose_name: Товары
|
verbose_name: Продано единиц товара
|
||||||
is_dttm: false
|
is_dttm: false
|
||||||
is_active: true
|
is_active: true
|
||||||
type: UINT64
|
type: UINT64
|
||||||
|
|||||||
+6
-6
@@ -24,10 +24,10 @@ compose() {
|
|||||||
next_step() {
|
next_step() {
|
||||||
case "$DAG_ID" in
|
case "$DAG_ID" in
|
||||||
world_initialize)
|
world_initialize)
|
||||||
printf 'посмотрите журнал; повторите make up, а для недействительного мира — make rebuild-storage'
|
printf 'проверьте состояние запуска в Airflow и журнал задачи с ошибкой; после устранения причины повторите make up'
|
||||||
;;
|
;;
|
||||||
world_recreate) printf 'устраните причину и повторите make rebuild-storage' ;;
|
world_recreate) printf 'проверьте состояние запуска в Airflow и журнал задачи с ошибкой; после устранения причины повторите make rebuild-storage' ;;
|
||||||
*) printf 'проверьте журнал дага и повторите команду' ;;
|
*) printf 'проверьте состояние запуска в Airflow и журнал задачи с ошибкой; после устранения причины повторите команду' ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ for ((attempt = 1; attempt <= ATTEMPTS; attempt++)); do
|
|||||||
done
|
done
|
||||||
jq -e --arg dag_id "$DAG_ID" '.dag_id == $dag_id' \
|
jq -e --arg dag_id "$DAG_ID" '.dag_id == $dag_id' \
|
||||||
>/dev/null 2>&1 <<<"$dag" || \
|
>/dev/null 2>&1 <<<"$dag" || \
|
||||||
fail "даг ${DAG_ID} не появился за $((ATTEMPTS * PAUSE_SECONDS)) с; $(next_step)"
|
fail "даг ${DAG_ID} не появился в API за $((ATTEMPTS * PAUSE_SECONDS)) с; проверьте ошибки импорта в Airflow и журнал службы airflow-dag-processor"
|
||||||
|
|
||||||
# GET читает DagModel по идентификатору, а POST запуска принимает только запись
|
# GET читает DagModel по идентификатору, а POST запуска принимает только запись
|
||||||
# с `is_stale = false`. На холодном старте GET уже может видеть даг, пока POST
|
# с `is_stale = false`. На холодном старте GET уже может видеть даг, пока POST
|
||||||
@@ -105,7 +105,7 @@ for ((attempt = 1; attempt <= TRIGGER_ATTEMPTS; attempt++)); do
|
|||||||
fail "Airflow не запустил даг ${DAG_ID}: HTTP ${http_status}${detail:+: ${detail}}; $(next_step)"
|
fail "Airflow не запустил даг ${DAG_ID}: HTTP ${http_status}${detail:+: ${detail}}; $(next_step)"
|
||||||
done
|
done
|
||||||
((triggered == 1)) || \
|
((triggered == 1)) || \
|
||||||
fail "даг ${DAG_ID} не стал доступен для запуска за $((TRIGGER_ATTEMPTS * PAUSE_SECONDS)) с; $(next_step)"
|
fail "даг ${DAG_ID} не стал доступен для запуска за $((TRIGGER_ATTEMPTS * PAUSE_SECONDS)) с; проверьте ошибки импорта в Airflow и журнал службы airflow-dag-processor"
|
||||||
run_id="$(jq -r '.dag_run_id // empty' <<<"$response")"
|
run_id="$(jq -r '.dag_run_id // empty' <<<"$response")"
|
||||||
state="$(jq -r '.state // "queued"' <<<"$response")"
|
state="$(jq -r '.state // "queued"' <<<"$response")"
|
||||||
[[ -n "$run_id" ]] || fail "Airflow не вернул идентификатор запуска ${DAG_ID}"
|
[[ -n "$run_id" ]] || fail "Airflow не вернул идентификатор запуска ${DAG_ID}"
|
||||||
@@ -121,7 +121,7 @@ for ((attempt = 1; attempt <= ATTEMPTS; attempt++)); do
|
|||||||
state="$(jq -r '.state // empty' <<<"$response" 2>/dev/null || true)"
|
state="$(jq -r '.state // empty' <<<"$response" 2>/dev/null || true)"
|
||||||
case "$state" in
|
case "$state" in
|
||||||
success)
|
success)
|
||||||
printf 'ЗЕЛЁНО: даг %s, запуск %s, состояние success.\n' "$DAG_ID" "$run_id"
|
printf 'УСПЕХ: даг %s, запуск %s, состояние success.\n' "$DAG_ID" "$run_id"
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
failed)
|
failed)
|
||||||
|
|||||||
+27
-68
@@ -1,49 +1,20 @@
|
|||||||
-- ODS: типизированное событие и таблица ошибок разбора.
|
|
||||||
--
|
|
||||||
-- Состав, имена и типы колонок списаны с «описания выгрузки»
|
|
||||||
-- (docs/formats/clickstream-event.md) — так же, как в бою хранилище пишут по
|
|
||||||
-- документации источника. Модуль контракта генератора здесь не читается:
|
|
||||||
-- граница «трекер | хранилище» проходит по документу.
|
|
||||||
-- Механика приёма и три класса брака — ADR 0005, конвенции имён, служебных
|
|
||||||
-- колонок и сроков — docs/architecture/storage.md.
|
|
||||||
|
|
||||||
-- Локальная таблица события.
|
-- Локальная таблица события.
|
||||||
|
-- Поля следуют docs/formats/clickstream-event.md; purchaseRevenue хранится
|
||||||
|
-- в Decimal для точного счета денег. Правила приема — ADR 0005.
|
||||||
|
-- ReplacingMergeTree оставляет последнюю доставку по _load_ts среди строк
|
||||||
|
-- с одинаковым полным ORDER BY. Метка переносится из STG без пересчета.
|
||||||
|
-- Она не входит в ключ сортировки или партиции: иначе повторные доставки
|
||||||
|
-- не считались бы версиями одной строки. Для чтения служит ods.event_v с FINAL.
|
||||||
--
|
--
|
||||||
-- Движок ReplacingMergeTree, колонка версии — _load_ts. Работа у версии ровно
|
-- Партиция EventDate хранит модельный день события. CounterID на стенде
|
||||||
-- одна: схлопнуть повтор доставки. Метка не ставится здесь заново, а
|
-- постоянен, EventDate постоянна внутри партиции; дальнейшая сортировка —
|
||||||
-- переносится из stg.hits_raw как есть — колонка отвечает на вопрос «когда
|
-- по intHash32(ClientID) и WatchID. WatchID отличает события посетителя:
|
||||||
-- строка приехала в хранилище», а не «когда её разобрали». У повтора
|
-- без него они заменяли бы друг друга.
|
||||||
-- содержимое то же самое, отличается только метка, поэтому какая из двух
|
|
||||||
-- строк переживёт мерж, безразлично.
|
|
||||||
--
|
--
|
||||||
-- Колонка версии не входит ни в ключ партиции, ни в ключ сортировки, и это
|
-- SAMPLE BY использует то же выражение, что ключ сортировки, и отбирает
|
||||||
-- не случайность: попади она туда — версии одной строки лягут в разные куски
|
-- посетителей целиком. Sign всегда равен 1: поле сохранено по формату
|
||||||
-- или в разные места ключа и не встретятся при мерже, то есть дедупликация
|
-- источника, но исправлений парой −1/+1 генератор не отправляет.
|
||||||
-- перестанет работать молча.
|
|
||||||
--
|
|
||||||
-- PARTITION BY EventDate — по дню события, а не по месяцу, как у Метрики.
|
|
||||||
-- Отступление осознанное: дневная партиция здесь единица переобработки, и
|
|
||||||
-- переиграть день X значит заменить одну партицию. Месячная партиция тянула
|
|
||||||
-- бы за собой тридцать чужих дней.
|
|
||||||
--
|
|
||||||
-- ORDER BY вырожден, и это учебный факт, а не недосмотр: CounterID на стенде
|
|
||||||
-- константа (сайт один), EventDate константа внутри своей партиции — обе
|
|
||||||
-- головные колонки ключа не различают ни одной строки. Реальная сортировка
|
|
||||||
-- идёт по посетителю и событию: intHash32(ClientID), WatchID. Ключ написан в
|
|
||||||
-- боевой форме «по сайту за период по посетителю» — на стенде она
|
|
||||||
-- вырождается, в бою нет. Хвост WatchID работает ещё и на дедупликацию: без
|
|
||||||
-- него ReplacingMergeTree схлопнул бы в одну строку все события посетителя за
|
|
||||||
-- день.
|
|
||||||
--
|
|
||||||
-- SAMPLE BY intHash32(ClientID) — по тому же выражению, что стоит в ключе
|
|
||||||
-- сортировки (иначе семплирование не разрешено). Семплирование берёт целиком
|
|
||||||
-- посетителей, а не события вразнобой, поэтому SAMPLE 0.1 не портит
|
|
||||||
-- uniq-метрики.
|
|
||||||
--
|
|
||||||
-- Sign всегда равен 1: это колонка формата, взятая без механики. В бою
|
|
||||||
-- исправление записи шлют парой −1/+1 и считают через sum(Sign) поверх
|
|
||||||
-- CollapsingMergeTree; наш генератор исправлений не шлёт, поэтому колонка
|
|
||||||
-- есть, а механики за ней нет.
|
|
||||||
CREATE TABLE IF NOT EXISTS ods.event_rep ON CLUSTER clickstream_cluster
|
CREATE TABLE IF NOT EXISTS ods.event_rep ON CLUSTER clickstream_cluster
|
||||||
(
|
(
|
||||||
WatchID UInt64,
|
WatchID UInt64,
|
||||||
@@ -100,36 +71,24 @@ PARTITION BY EventDate
|
|||||||
ORDER BY (CounterID, EventDate, intHash32(ClientID), WatchID)
|
ORDER BY (CounterID, EventDate, intHash32(ClientID), WatchID)
|
||||||
SAMPLE BY intHash32(ClientID);
|
SAMPLE BY intHash32(ClientID);
|
||||||
|
|
||||||
-- Лицо слоя: пишем и читаем через него. Ключ шардирования —
|
-- Запись и диагностика через Distributed; актуальные события — в ods.event_v.
|
||||||
-- cityHash64(ClientID), а не сырой ClientID: структурированный числовой
|
-- cityHash64(ClientID) распределяет посетителей равномернее, чем остаток
|
||||||
-- идентификатор перекашивает остаток по модулю числа шардов, хеш — нет.
|
-- самого структурированного идентификатора. События посетителя лежат вместе.
|
||||||
-- События одной куки при этом остаются на одном шарде, и сессионизация со
|
-- STG распределен по хешу raw, поэтому сообщение и разобранное событие могут
|
||||||
-- склейкой идентичностей ниже по течению живут локально.
|
-- лежать на разных шардах. Общие счетчики сравнивают через _dist.
|
||||||
--
|
|
||||||
-- У STG ключ другой (хеш строки), поэтому сырая строка и разобранное из неё
|
|
||||||
-- событие почти всегда лежат на разных шардах. Пошардовые счётчики слоёв
|
|
||||||
-- сходиться не должны — сверять слои можно только через _dist.
|
|
||||||
CREATE TABLE IF NOT EXISTS ods.event_dist ON CLUSTER clickstream_cluster
|
CREATE TABLE IF NOT EXISTS ods.event_dist ON CLUSTER clickstream_cluster
|
||||||
AS ods.event_rep
|
AS ods.event_rep
|
||||||
ENGINE = Distributed('clickstream_cluster', 'ods', 'event_rep', cityHash64(ClientID));
|
ENGINE = Distributed('clickstream_cluster', 'ods', 'event_rep', cityHash64(ClientID));
|
||||||
|
|
||||||
-- Локальная таблица ошибок разбора.
|
-- Ошибки разбора: исходный текст, класс и сведения о доставке.
|
||||||
|
-- У сообщения может не быть разобранного ClientID, поэтому шардирование —
|
||||||
|
-- по cityHash64(raw), партиция — по реальному дню загрузки.
|
||||||
|
-- ORDER BY группирует ошибки по классу и координатам Kafka.
|
||||||
--
|
--
|
||||||
-- Сырой текст сообщения, метаданные доставки и класс брака. Разобранных полей
|
-- Замены версий нет: надежного ключа события у брака может не быть.
|
||||||
-- у брака нет по определению: строка сюда попала как раз потому, что не
|
-- TTL сохраняет ошибки месяц, дольше трехсуточного срока STG.
|
||||||
-- разобралась.
|
|
||||||
--
|
|
||||||
-- Движок — обычный ReplicatedMergeTree, без замены версий: у брака нет ключа
|
|
||||||
-- сущности, схлопывать его не по чему.
|
|
||||||
--
|
|
||||||
-- Ключи собственные. Шардирование — cityHash64(raw): ClientID у неразобранной
|
|
||||||
-- строки взять неоткуда. ORDER BY (error_class, kafka_partition,
|
|
||||||
-- kafka_offset): такую таблицу смотрят от класса брака, а внутри класса — по
|
|
||||||
-- координатам доставки. Нарезка по дню загрузки, как у STG: модельного дня
|
|
||||||
-- у брака тоже нет.
|
|
||||||
--
|
|
||||||
-- Срок жизни — месяц, вдесятеро дольше сырых данных. Истеки брак вместе с ними —
|
|
||||||
-- к моменту разбирательства не осталось бы ни того, ни другого.
|
|
||||||
CREATE TABLE IF NOT EXISTS ods.event_errors_rep ON CLUSTER clickstream_cluster
|
CREATE TABLE IF NOT EXISTS ods.event_errors_rep ON CLUSTER clickstream_cluster
|
||||||
(
|
(
|
||||||
raw String,
|
raw String,
|
||||||
|
|||||||
+26
-84
@@ -1,8 +1,6 @@
|
|||||||
-- ODS: каноническое чтение актуальных событий.
|
-- Актуальные события: FINAL выбирает последнюю доставку по _load_ts,
|
||||||
--
|
-- не дожидаясь фоновых слияний. Поля и типы остаются как в ods.event_dist.
|
||||||
-- Представление сохраняет поля и зерно ods.event_dist, но скрывает FINAL от
|
|
||||||
-- потребителя. Поэтому физическая таблица остаётся местом диагностики
|
|
||||||
-- доставленных версий, а точное чтение получает одно имя.
|
|
||||||
CREATE VIEW IF NOT EXISTS ods.event_v ON CLUSTER clickstream_cluster
|
CREATE VIEW IF NOT EXISTS ods.event_v ON CLUSTER clickstream_cluster
|
||||||
AS
|
AS
|
||||||
SELECT *
|
SELECT *
|
||||||
@@ -25,69 +23,24 @@ AS
|
|||||||
SELECT *
|
SELECT *
|
||||||
FROM ods.order_dist FINAL;
|
FROM ods.order_dist FINAL;
|
||||||
|
|
||||||
-- ODS: разбор STG в событие и в таблицу ошибок.
|
-- Разбор STG в события и ошибки. Два представления используют одинаковое
|
||||||
|
-- условие годности; второе берет его отрицание. Условие дает только 0 или 1:
|
||||||
|
-- NULL не выбрала бы ни одна ветвь. Функции разбора не вызывают исключений
|
||||||
|
-- на некорректном raw, чтобы одно сообщение не прервало прием.
|
||||||
--
|
--
|
||||||
-- Матвью две, и вместе они обязаны делить поток без зазора и без нахлёста:
|
-- Оба представления срабатывают на вставку в stg.hits_raw_dist,
|
||||||
-- строка, подошедшая обеим, задвоится, а не подошедшая ни одной — исчезнет
|
-- до распределения по шардам. Порядок SELECT совпадает с целевой таблицей.
|
||||||
-- молча. Держится это формой: годность считает один и тот же предикат из трёх
|
|
||||||
-- частей, а вторая матвью берёт его буквальное отрицание.
|
|
||||||
--
|
--
|
||||||
-- Отсюда два запрета на выражения предиката, и оба серьёзные.
|
-- Прием проверяет объект JSON, точный набор ключей и разбор пяти полей:
|
||||||
|
-- WatchID, VisitID, ClientID, EventDate, UTCEventTime. Остальные поля
|
||||||
|
-- извлекаются по формату без отдельной проверки. Граница — ADR 0005.
|
||||||
|
-- arraySort с обеих сторон позволяет сравнить ключи независимо от порядка.
|
||||||
--
|
--
|
||||||
-- Первый: ни одна его часть не возвращает NULL. Трёхзначная логика дала бы
|
-- UTCEventTime приходит строкой вида "2026-06-01T12:34:56Z". Ее разбирает
|
||||||
-- строку, которую не берёт ни «условие», ни «NOT условие», — и потерялась бы
|
-- parseDateTimeOrNull с явной маской: JSONExtract в DateTime такую форму
|
||||||
-- она бесшумно. Поэтому сравнения здесь дают 0 или 1, а разбор в Nullable
|
-- не принимает, а BestEffort допускает неполные даты. Пояс UTC указан
|
||||||
-- заканчивается IS NOT NULL.
|
-- отдельно, потому что Z в маске проверяется как буква. EventDate приходит
|
||||||
--
|
-- как "2026-06-01" и разбирается через JSONExtract.
|
||||||
-- Второй: ни одна его часть не бросает исключений. Упавшая матвью роняет
|
|
||||||
-- вставку, офсеты не коммитятся, и потребление топика встаёт до починки —
|
|
||||||
-- это единственное, на чём сейчас держится правило репозитория «грязные
|
|
||||||
-- записи не валят пайплайн». Поэтому приведение Nullable к необнуляемому типу
|
|
||||||
-- (assumeNotNull) стоит только в списке колонок, за предикатом, который NULL
|
|
||||||
-- уже отсёк, а в самом предикате живут только функции семейства
|
|
||||||
-- JSONExtract/*OrNull. Довод целиком — ADR 0005.
|
|
||||||
--
|
|
||||||
-- Источник у обеих — stg.hits_raw_dist, лицо слоя, а не локальная таблица:
|
|
||||||
-- потребитель цепляется к лицу, а матвью с источником-Distributed срабатывает
|
|
||||||
-- на вставку именно в распределённую таблицу, до раскладки по шардам.
|
|
||||||
--
|
|
||||||
-- Порядок колонок в SELECT совпадает с порядком в целевой таблице.
|
|
||||||
|
|
||||||
-- Годное событие: строка, прошедшая строгий приём.
|
|
||||||
--
|
|
||||||
-- Строгий приём — это три вопроса: объект ли это JSON, тот ли набор ключей,
|
|
||||||
-- разбираются ли в свой тип пять опорных колонок. Почему именно так — почему
|
|
||||||
-- объект, а не валидность; почему сверка ключей заменяет сорок семь проверок
|
|
||||||
-- на присутствие; почему опорных пять, а не сорок семь — ADR 0005, «Решение».
|
|
||||||
--
|
|
||||||
-- arraySort ниже стоит с обеих сторон, и убирать его нельзя: без обёртки сорок
|
|
||||||
-- семь CamelCase-имён пришлось бы держать ровно в байтовом порядке, а сбой
|
|
||||||
-- порядка увёл бы в брак вообще всё, и молча (ADR 0005).
|
|
||||||
--
|
|
||||||
-- Метка времени — единственная из пяти, кого разбирает не JSONExtract, и это
|
|
||||||
-- измеренная необходимость, а не вкус. На проводе UTCEventTime уезжает в
|
|
||||||
-- ISO-8601 с суффиксом зоны — «2026-06-01T12:34:56Z» (спека генератора,
|
|
||||||
-- раздел 4), а JSONExtract с типом DateTime такую строку не берёт и отдаёт
|
|
||||||
-- NULL. Оставь его здесь — и в брак уехали бы все события до единого.
|
|
||||||
--
|
|
||||||
-- Формат назван буквально, а не отдан parseDateTimeBestEffort, и вот почему.
|
|
||||||
-- Best-effort понимает десяток записей и на непонятной не краснеет, а
|
|
||||||
-- достраивает недостающее: обрезанное «20:00:21» он превращает в первое
|
|
||||||
-- января текущего года. Такая строка прошла бы строгий приём с тихо неверным
|
|
||||||
-- временем — ровно с той порчей, ради которой класс key_field_unparsed и
|
|
||||||
-- заведён. Источник у топика один и шлёт одну запись, так что широта здесь не
|
|
||||||
-- нужна вовсе, а стоит она отключённой проверкой. Замеры — ADR 0005,
|
|
||||||
-- «Что проверено».
|
|
||||||
--
|
|
||||||
-- Третьим аргументом назван пояс — 'UTC'. Суффикс Z маска сверяет как букву и
|
|
||||||
-- выбрасывает, зоны из строки не берёт вовсе, поэтому без имени функция читала
|
|
||||||
-- бы показания часов по поясу сессии, а тот по умолчанию серверный. Тип
|
|
||||||
-- колонки этого не чинит: разбор отдаёт готовое число, и колонка кладёт его
|
|
||||||
-- как есть — пояс приёмника решал бы судьбу строки, а не числа.
|
|
||||||
-- Правило и замер — docs/architecture/storage.md, «Часовые пояса».
|
|
||||||
--
|
|
||||||
-- EventDate в такой подпорке не нуждается: дата уезжает как «2026-06-01», и
|
|
||||||
-- JSONExtract её берёт.
|
|
||||||
|
|
||||||
CREATE MATERIALIZED VIEW IF NOT EXISTS ods.event_mv ON CLUSTER clickstream_cluster
|
CREATE MATERIALIZED VIEW IF NOT EXISTS ods.event_mv ON CLUSTER clickstream_cluster
|
||||||
TO ods.event_dist
|
TO ods.event_dist
|
||||||
@@ -154,10 +107,8 @@ SELECT
|
|||||||
JSONExtract(raw, 'GoalsReached', 'Array(UInt32)') AS GoalsReached,
|
JSONExtract(raw, 'GoalsReached', 'Array(UInt32)') AS GoalsReached,
|
||||||
JSONExtract(raw, 'ParsedParamsKey1', 'Array(String)') AS ParsedParamsKey1,
|
JSONExtract(raw, 'ParsedParamsKey1', 'Array(String)') AS ParsedParamsKey1,
|
||||||
JSONExtract(raw, 'purchaseID', 'Array(String)') AS purchaseID,
|
JSONExtract(raw, 'purchaseID', 'Array(String)') AS purchaseID,
|
||||||
-- Выгрузка несет JSON-числа типа Float64, но деньги хранятся в Decimal.
|
-- purchaseRevenue приходит числом рублей и разбирается сразу в Decimal.
|
||||||
-- JSONExtract разбирает их сразу, без промежуточного двоичного числа:
|
-- Прямой разбор сверяли через Context7 и на ClickHouse 26.3 (#155).
|
||||||
-- синтаксис подтвержден документацией ClickHouse через Context7 и
|
|
||||||
-- запросом к стендовому ClickHouse 26.3 при исполнении #155.
|
|
||||||
JSONExtract(raw, 'purchaseRevenue', 'Array(Decimal(18, 2))') AS purchaseRevenue,
|
JSONExtract(raw, 'purchaseRevenue', 'Array(Decimal(18, 2))') AS purchaseRevenue,
|
||||||
JSONExtract(raw, 'purchaseCurrency', 'Array(String)') AS purchaseCurrency,
|
JSONExtract(raw, 'purchaseCurrency', 'Array(String)') AS purchaseCurrency,
|
||||||
JSONExtract(raw, 'purchaseCoupon', 'Array(String)') AS purchaseCoupon,
|
JSONExtract(raw, 'purchaseCoupon', 'Array(String)') AS purchaseCoupon,
|
||||||
@@ -168,25 +119,16 @@ SELECT
|
|||||||
JSONExtract(raw, 'productQuantity', 'Array(UInt64)') AS productQuantity,
|
JSONExtract(raw, 'productQuantity', 'Array(UInt64)') AS productQuantity,
|
||||||
JSONExtract(raw, 'productEventType', 'Array(String)') AS productEventType,
|
JSONExtract(raw, 'productEventType', 'Array(String)') AS productEventType,
|
||||||
JSONExtract(raw, 'ecommerce', 'String') AS ecommerce,
|
JSONExtract(raw, 'ecommerce', 'String') AS ecommerce,
|
||||||
-- Метка загрузки переносится из STG как есть, а не ставится заново:
|
-- _load_ts остается временем исходной доставки и версией события.
|
||||||
-- она отвечает на вопрос «когда строка приехала в хранилище». Поставь
|
|
||||||
-- здесь now64(3) — и колонка версии молча ответила бы на другой вопрос.
|
|
||||||
_load_ts
|
_load_ts
|
||||||
FROM stg.hits_raw_dist
|
FROM stg.hits_raw_dist
|
||||||
WHERE is_object AND keys_match AND key_fields_parsed;
|
WHERE is_object AND keys_match AND key_fields_parsed;
|
||||||
|
|
||||||
-- Брак: буквальное отрицание того же предиката.
|
-- Ошибки: отрицание того же условия годности.
|
||||||
--
|
-- Класс определяется первым нарушенным условием. Например, JSON-строка
|
||||||
-- Классы брака пересекаются, поэтому проверяются по порядку, а в error_class
|
-- не проходит и проверку объекта, и проверку ключей, но получает not_an_object.
|
||||||
-- пишется первый совпавший. Скаляр проваливает и проверку на объект, и сверку
|
-- Условие повторено рядом с запросом, без отдельной функции ClickHouse.
|
||||||
-- ключей — JSONExtractKeys от него даёт пустой массив (измерено на стенде
|
|
||||||
-- 7 августа 2026 года), — и без объявленного порядка попал бы то в один
|
|
||||||
-- класс, то в другой.
|
|
||||||
--
|
|
||||||
-- Предикат повторён здесь дословно, и это выбор, а не безвыходность: назвать
|
|
||||||
-- его один раз на две матвью позволил бы CREATE FUNCTION. Отвергнуто — условие
|
|
||||||
-- разбора ушло бы за имя, в отдельный объект со своей жизнью, и слой перестал
|
|
||||||
-- бы читаться по своему же DDL.
|
|
||||||
CREATE MATERIALIZED VIEW IF NOT EXISTS ods.event_errors_mv ON CLUSTER clickstream_cluster
|
CREATE MATERIALIZED VIEW IF NOT EXISTS ods.event_errors_mv ON CLUSTER clickstream_cluster
|
||||||
TO ods.event_errors_dist
|
TO ods.event_errors_dist
|
||||||
AS
|
AS
|
||||||
|
|||||||
+11
-21
@@ -1,26 +1,16 @@
|
|||||||
-- STG: матвью приёма — из чтеца в таблицу STG.
|
-- Прием событий из Kafka в STG.
|
||||||
|
-- Подключение этого представления запускает чтение топика. Поэтому файл 40
|
||||||
|
-- применяется после таблиц и представлений разбора из файлов 20 и 30:
|
||||||
|
-- иначе часть сообщений сохранится в STG, но не попадет в ODS.
|
||||||
--
|
--
|
||||||
-- Номер 40, а не 11, и пропуск в нумерации намеренный. Файлы 20-ods-tables.sql
|
-- Запись через stg.hits_raw_dist распределяет сообщения по ключу таблицы.
|
||||||
-- и 30-ods-views.sql приносит #43, и матвью приёма обязана создаваться после
|
-- Она фоновая; возможную потерю при сбое описывает раздел «Приём»
|
||||||
-- матвью разбора: Kafka-движок начинает читать топик ровно тогда, когда к нему
|
-- в docs/architecture/storage.md.
|
||||||
-- привязывают первую матвью. Создай её раньше разбора — и всё, что доедет в
|
|
||||||
-- зазоре, ляжет в STG и не попадёт в ODS никуда, ни в событие, ни в ошибки.
|
|
||||||
-- На пустом топике зазор безвреден, поэтому первый прогон о нём не скажет:
|
|
||||||
-- проснётся он, когда тома ClickHouse снесены, а данные Kafka целы, то есть на
|
|
||||||
-- обычной отладке. Нумерацию здесь не «приводить в порядок».
|
|
||||||
--
|
--
|
||||||
-- Пишем в stg.hits_raw_dist, а не в локальную таблицу: раскладку по шардам
|
-- Сведения о доставке вычисляются здесь, на ноде чтения Kafka.
|
||||||
-- обязан определять ключ шардирования, а не то, какая нода случайно читала
|
-- DEFAULT hostName() только в локальной таблице назвал бы шард-получатель.
|
||||||
-- топик. Вставка при этом фоновая — окно потери принято осознанно, довод
|
-- Порядок SELECT совпадает с порядком колонок цели.
|
||||||
-- целиком в docs/architecture/storage.md, раздел «Приём».
|
|
||||||
--
|
|
||||||
-- Служебные колонки заполняются выражением здесь, а не DEFAULT в таблице.
|
|
||||||
-- Для consumer_host это обязательно: проверено на стенде 5 августа 2026 года —
|
|
||||||
-- DEFAULT hostName() вычисляется на шарде-получателе и назвал бы не ту ноду,
|
|
||||||
-- которая читала топик. hostName() в SELECT снимается на вставляющей ноде,
|
|
||||||
-- то есть отвечает ровно на нужный вопрос.
|
|
||||||
--
|
|
||||||
-- Порядок колонок в SELECT совпадает с порядком в целевой таблице.
|
|
||||||
CREATE MATERIALIZED VIEW IF NOT EXISTS stg.hits_raw_mv ON CLUSTER clickstream_cluster
|
CREATE MATERIALIZED VIEW IF NOT EXISTS stg.hits_raw_mv ON CLUSTER clickstream_cluster
|
||||||
TO stg.hits_raw_dist
|
TO stg.hits_raw_dist
|
||||||
AS
|
AS
|
||||||
|
|||||||
+19
-42
@@ -1,21 +1,9 @@
|
|||||||
-- DDS: сборка дней заказа в таблицу-двойник перед заменой партиций.
|
-- Сборка выбранных дней в донор перед заменой партиций DDS.
|
||||||
|
-- Донор очищается в начале: иначе новые строки добавились бы к прежним.
|
||||||
|
-- После сборки он остается для диагностики до следующего запуска.
|
||||||
--
|
--
|
||||||
-- Двойник очищается целиком в начале сборки, а не в конце: собранное остаётся
|
-- DROP PARTITION ALL доступен роли etl через ALTER; отдельного права
|
||||||
-- лежать до следующего запуска, и разбор происшествия видит ровно то, чем
|
-- TRUNCATE у нее нет (ADR 0007). Весь отрезок дней собирается одной вставкой.
|
||||||
-- подменили день. Очистка обязана быть первой ещё и по делу — вставка поверх
|
|
||||||
-- прежних строк удвоила бы день в доноре, а REPLACE PARTITION перенёс бы
|
|
||||||
-- удвоение в цель.
|
|
||||||
--
|
|
||||||
-- Дни собираются одной вставкой, а не по дню за раз: источник читается один
|
|
||||||
-- раз на весь отрезок. Кто раскладывает строки по шардам — вставка через
|
|
||||||
-- Distributed, ключ cityHash64(order_id) — разобрано в DDL двойника.
|
|
||||||
--
|
|
||||||
-- Чистит двойник DROP PARTITION ALL, а не TRUNCATE: последнее — отдельное
|
|
||||||
-- право, роли etl оно не выдано, а снятие всех партиций
|
|
||||||
-- входит в ALTER, который у неё есть (ADR 0007). Замерено: TRUNCATE отвечает
|
|
||||||
-- ролью etl кодом 497, DROP PARTITION ALL проходит. Расширять права ради
|
|
||||||
-- операции, для которой уже есть разрешённая форма, значило бы платить
|
|
||||||
-- доступом за синоним.
|
|
||||||
|
|
||||||
ALTER TABLE dds.order_stage_rep ON CLUSTER clickstream_cluster DROP PARTITION ALL;
|
ALTER TABLE dds.order_stage_rep ON CLUSTER clickstream_cluster DROP PARTITION ALL;
|
||||||
|
|
||||||
@@ -37,18 +25,13 @@ INSERT INTO dds.order_stage_dist
|
|||||||
_load_id,
|
_load_id,
|
||||||
_load_ts
|
_load_ts
|
||||||
)
|
)
|
||||||
-- Позиции разбираются здесь один раз, на границе ODS → DDS. Цена на проводе —
|
-- Разбор позиций из JSON: цена в строке сразу превращается в Decimal.
|
||||||
-- строка с двумя знаками, и JSONExtract читает её прямо в Decimal: замерено на
|
-- ODS проверил только тип массива items. Неверное содержимое здесь может
|
||||||
-- живом слепке, промежуточный toDecimal64 не нужен.
|
-- дать значения по умолчанию: qty вида "oops" и пропущенная цена дают 0,
|
||||||
--
|
-- скаляр вместо объекта выпадает из массива. Наблюдения —
|
||||||
-- Разбор по типу молчалив, и знать об этом надо заранее. Строгий приём ODS
|
-- docs/architecture/storage.md, «Что проверено».
|
||||||
-- проверил только, что items — массив, внутрь он не смотрел; здесь тоже никто
|
-- Проверку суммы позиций участник добавляет в docs/labs/5-1-order-checks.md.
|
||||||
-- не смотрит. Замерено: qty вида "oops" даёт 0, пропущенная цена даёт 0,
|
|
||||||
-- скаляр вместо объекта выпадает из массива целиком. На каноническом слепке
|
|
||||||
-- генератора такого не бывает, и сумма «цена × количество» сходится с
|
|
||||||
-- items_total у всех заказов, но сходится она по свойству источника, а не
|
|
||||||
-- потому, что её здесь кто-то проверяет. Постоянное место такой проверки —
|
|
||||||
-- контур DQ этапа 4; пока она делается руками при приёмке.
|
|
||||||
WITH JSONExtract(
|
WITH JSONExtract(
|
||||||
items, 'Array(Tuple(sku String, qty UInt32, price Decimal(18, 2)))'
|
items, 'Array(Tuple(sku String, qty UInt32, price Decimal(18, 2)))'
|
||||||
) AS lines
|
) AS lines
|
||||||
@@ -56,8 +39,7 @@ SELECT
|
|||||||
order_id,
|
order_id,
|
||||||
user_id,
|
user_id,
|
||||||
status,
|
status,
|
||||||
-- День заказа и оба времени переезжают в пояс счётчика: DDS обслуживает
|
-- День и время в Europe/Samara: витрины используют готовые колонки.
|
||||||
-- человека с дашбордом, и пояс называется здесь в последний раз.
|
|
||||||
toDate(toTimeZone(created_at, 'Europe/Samara')),
|
toDate(toTimeZone(created_at, 'Europe/Samara')),
|
||||||
toTimeZone(created_at, 'Europe/Samara'),
|
toTimeZone(created_at, 'Europe/Samara'),
|
||||||
toTimeZone(updated_at, 'Europe/Samara'),
|
toTimeZone(updated_at, 'Europe/Samara'),
|
||||||
@@ -68,22 +50,17 @@ SELECT
|
|||||||
arrayMap(line -> line.sku, lines),
|
arrayMap(line -> line.sku, lines),
|
||||||
arrayMap(line -> line.qty, lines),
|
arrayMap(line -> line.qty, lines),
|
||||||
arrayMap(line -> line.price, lines),
|
arrayMap(line -> line.price, lines),
|
||||||
-- Координаты запуска, собравшего день: у всех строк дня они одни.
|
-- Метки этой сборки DDS, общие для всех вставляемых строк.
|
||||||
{load_id:String},
|
{load_id:String},
|
||||||
now64(3, 'UTC')
|
now64(3, 'UTC')
|
||||||
FROM ods.order_v
|
FROM ods.order_v
|
||||||
WHERE toDate(toTimeZone(created_at, 'Europe/Samara'))
|
WHERE toDate(toTimeZone(created_at, 'Europe/Samara'))
|
||||||
BETWEEN {first_day:Date} AND {last_day:Date}
|
BETWEEN {first_day:Date} AND {last_day:Date}
|
||||||
-- Вторая граница не меняет отбор, а открывает источнику отсечение
|
-- Дополнительный фильтр сокращает чтение партиций ODS по UTC.
|
||||||
-- партиций: ODS нарезан по toDate(created_at) в UTC, и условие на
|
-- Модельные сутки Europe/Samara пересекают две даты UTC, поэтому
|
||||||
-- вычисленный день заказа для этого ключа непрозрачно — сервер прочёл бы
|
-- левая граница берется на день раньше. Точный отбор — условием выше.
|
||||||
-- все партиции. Сутки пояса счётчика ложатся ровно на две даты UTC,
|
|
||||||
-- поэтому день слева берётся с запасом.
|
|
||||||
AND toDate(created_at) BETWEEN {first_day:Date} - 1 AND {last_day:Date}
|
AND toDate(created_at) BETWEEN {first_day:Date} - 1 AND {last_day:Date}
|
||||||
-- Тот же ноль, что в приёме заказов: заводская двойка исполнила бы вставку
|
-- Ждем доставки на шарды и распределяем строки по ключу цели
|
||||||
-- локально на каждом шарде, минуя ключ шардирования цели, и строки унаследовали
|
-- (docs/architecture/storage.md, «Раскладка по шардам»).
|
||||||
-- бы раскладку источника. Ключи ODS и DDS здесь совпадают, и раскладка вышла бы
|
|
||||||
-- верной — но по совпадению, а не по контракту (docs/architecture/storage.md,
|
|
||||||
-- «Раскладка по шардам», капкан #137).
|
|
||||||
SETTINGS distributed_foreground_insert = 1,
|
SETTINGS distributed_foreground_insert = 1,
|
||||||
parallel_distributed_insert_select = 0;
|
parallel_distributed_insert_select = 0;
|
||||||
+12
-24
@@ -1,28 +1,16 @@
|
|||||||
-- DDS: подмена одного дня заказов собранной партицией двойника.
|
-- Замена одного дня DDS готовой партицией донора.
|
||||||
|
-- Повтор при неизменном источнике сохраняет деловые данные без дублей.
|
||||||
--
|
--
|
||||||
-- Это и есть идемпотентность слоя. Повтор прогона безопасен, а «дыхание»
|
-- ON CLUSTER выполняет замену на обеих нодах по локальным таблицам.
|
||||||
-- статусов внутри окна выражается сменой всей партиции, а не правкой строк.
|
-- Донор и цель имеют одинаковую структуру и ключ шардирования
|
||||||
|
-- (sql/ddl/45-dds-tables.sql); REPLACE PARTITION строки не перераспределяет.
|
||||||
|
-- Замена атомарна внутри шарда. Между заменами на разных шардах читатель
|
||||||
|
-- может увидеть часть прежних и часть новых данных. После сбоя этот период
|
||||||
|
-- длится до успешного повтора. Пустая партиция донора удаляет день из цели.
|
||||||
--
|
--
|
||||||
-- Замена идёт по локальным таблицам, шард за шардом: ON CLUSTER рассылает её
|
-- День подставляет Jinja: параметры ClickHouse в ALTER ... PARTITION
|
||||||
-- на обе ноды, а по Distributed то же самое не работает — операции с
|
-- не поддерживаются в закрепленной версии 26.3. Значение получено
|
||||||
-- партициями отвечают кодом 48 (docs/architecture/storage.md, «Что
|
-- из результата order_scope.sql; замер — docs/architecture/storage.md.
|
||||||
-- проверено»). Правильность пошардовой замены держится на том, что двойник
|
|
||||||
-- шардирован тем же ключом, что цель, — см. sql/ddl/45-dds-tables.sql.
|
|
||||||
--
|
|
||||||
-- Отсюда граница обещания: атомарность здесь пошардовая. Внутри шарда подмена
|
|
||||||
-- мгновенна — читатель видит либо прежний день целиком, либо новый; но две
|
|
||||||
-- локальные замены независимы, и в зазоре между ними чтение через Distributed
|
|
||||||
-- соберёт ответ из нового дня одного шарда и прежнего дня другого. Зазор
|
|
||||||
-- короткий, а отказ одной из двух замен растягивает его до повторного прогона.
|
|
||||||
--
|
|
||||||
-- Пустая партиция двойника — законный случай, а не сбой: день, в котором
|
|
||||||
-- заказов нет, снимает день из цели, и слой перестаёт показывать то, чего в
|
|
||||||
-- источнике больше нет.
|
|
||||||
--
|
|
||||||
-- День подставляет Jinja, а не параметр ClickHouse: в ALTER … PARTITION
|
|
||||||
-- сервер параметров не подставляет — замерено, отвечает синтаксической
|
|
||||||
-- ошибкой. Общее правило репозитория «параметры передаются отдельно, а не
|
|
||||||
-- строкой» здесь не нарушено по существу: значение пришло из ответа самого
|
|
||||||
-- ClickHouse на запрос объёма, а не снаружи стенда.
|
|
||||||
ALTER TABLE dds.order_rep ON CLUSTER clickstream_cluster
|
ALTER TABLE dds.order_rep ON CLUSTER clickstream_cluster
|
||||||
REPLACE PARTITION '{{ params.day }}' FROM dds.order_stage_rep;
|
REPLACE PARTITION '{{ params.day }}' FROM dds.order_stage_rep;
|
||||||
+16
-37
@@ -1,37 +1,17 @@
|
|||||||
-- DDS: объём пересборки заказов — с какого дня по какой собирает этот запуск.
|
-- Дни пересборки заказов: first_day и last_day, обе границы включены.
|
||||||
|
-- Текущее окно — дни оформления заказов из последнего snapshot_date.
|
||||||
|
-- Если в DDS нет дня, который есть в ODS, отрезок расширяется до первого
|
||||||
|
-- такого дня. Это восстанавливает пропуски даже за пределами текущего окна.
|
||||||
--
|
--
|
||||||
-- Параметров у дага нет: объём он выводит сам из состояния слоёв
|
-- Дата слепка у актуальной версии показывает последнее наблюдение заказа
|
||||||
-- (docs/architecture/etl.md, «Объём прогона»). Ответ — две даты, границы
|
-- (ADR 0014). Поэтому окно берется из данных, без копирования константы
|
||||||
-- непрерывного отрезка; список дней между ними разворачивает даг. Пустой
|
-- генератора. FINAL здесь не нужен: created_at одинаков у всех версий,
|
||||||
-- источник даёт пустой ответ, и даг на нём останавливается с внятной ошибкой
|
-- а строки последнего слепка сохраняются при слияниях.
|
||||||
-- вместо зелёного прогона, собравшего ничего.
|
|
||||||
--
|
--
|
||||||
-- Файл кончается без точки с запятой: даг читает его целиком и шлёт одним
|
-- Пустой ODS не дает строки результата; даг завершится ошибкой.
|
||||||
-- запросом, дописывая формат ответа, — точка с запятой сделала бы из этого
|
-- Точка с запятой в конце не нужна: клиент дописывает FORMAT ответа.
|
||||||
-- мультизапрос, который ClickHouse не принимает.
|
-- Порядок пересборки — docs/architecture/etl.md, «Объём прогона».
|
||||||
--
|
|
||||||
-- Объём складывается из двух частей. Первая — дышащее окно: дни, которые нёс
|
|
||||||
-- последний слепок, то есть те, чьё состояние источник ещё может изменить.
|
|
||||||
-- Вторая — хвост от первого дня, которого в слое нет: первая сборка застаёт
|
|
||||||
-- слой пустым и собирает весь стартовый мир, а после разгона мира пачкой слой
|
|
||||||
-- догоняет уехавший вперёд источник.
|
|
||||||
--
|
|
||||||
-- **Хвост считается от первого недостающего дня, а не от последнего
|
|
||||||
-- собранного.** Разница не косметическая: замены партиций идут отдельными
|
|
||||||
-- задачами и падают порознь, поэтому упавший день оставляет дыру, пока
|
|
||||||
-- соседние дни собираются дальше. Считай мы прогресс по максимуму собранного
|
|
||||||
-- дня, дыра позади окна не заросла бы никогда, а прогоны продолжали бы
|
|
||||||
-- зеленеть над щербатым слоем. Цена честного ответа — проход по дню заказа
|
|
||||||
-- в ODS.
|
|
||||||
--
|
|
||||||
-- Окно читается из ярлыка слепка, а не вычисляется вычитанием окна
|
|
||||||
-- изменяемости: у выжившей строки snapshot_date — последний слепок, который
|
|
||||||
-- нёс этот заказ (ADR 0014). Поэтому оба края окна знает сам источник, а
|
|
||||||
-- копии его константы на стороне стенда больше нет.
|
|
||||||
--
|
|
||||||
-- FINAL не нужен ни здесь, ни ниже: время рождения заказа у всех его версий
|
|
||||||
-- одно, а строки последнего слепка несут максимум колонки версии — мерж
|
|
||||||
-- оставляет как раз их.
|
|
||||||
WITH
|
WITH
|
||||||
(SELECT max(snapshot_date) FROM ods.order_dist) AS last_snapshot,
|
(SELECT max(snapshot_date) FROM ods.order_dist) AS last_snapshot,
|
||||||
(
|
(
|
||||||
@@ -44,10 +24,9 @@ WITH
|
|||||||
FROM ods.order_dist
|
FROM ods.order_dist
|
||||||
WHERE snapshot_date = last_snapshot
|
WHERE snapshot_date = last_snapshot
|
||||||
) AS window_end,
|
) AS window_end,
|
||||||
-- GLOBAL обязателен: обе таблицы распределённые, и обычный NOT IN сверял
|
-- GLOBAL NOT IN сравнивает дни со всем DDS. Локальное сравнение могло бы
|
||||||
-- бы дни источника с локальной частью слоя на каждом шарде — день, чьи
|
-- счесть день пропущенным, если его заказы находятся на соседнем шарде.
|
||||||
-- заказы легли на соседний шард, показался бы недостающим.
|
-- minOrNull возвращает NULL, когда пропущенных дней нет.
|
||||||
-- minOrNull отличает «дыр нет» от даты по умолчанию.
|
|
||||||
(
|
(
|
||||||
SELECT minOrNull(day)
|
SELECT minOrNull(day)
|
||||||
FROM (
|
FROM (
|
||||||
@@ -57,7 +36,7 @@ WITH
|
|||||||
WHERE day GLOBAL NOT IN (SELECT order_date FROM dds.order_dist)
|
WHERE day GLOBAL NOT IN (SELECT order_date FROM dds.order_dist)
|
||||||
) AS first_missing_day
|
) AS first_missing_day
|
||||||
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
|
||||||
WHERE (SELECT count() FROM ods.order_dist) > 0
|
WHERE (SELECT count() FROM ods.order_dist) > 0
|
||||||
+24
-42
@@ -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;
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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
|
||||||
+27
-46
@@ -1,24 +1,15 @@
|
|||||||
-- Разбор одного среза заказов: сначала годные версии, затем брак.
|
-- Разбор сохраненной порции заказов: сначала годные версии, затем ошибки.
|
||||||
|
-- Макрос Jinja подставляет одно условие годности в обе вставки.
|
||||||
|
-- Вторая вставка берет его отрицание, чтобы каждая строка получила результат.
|
||||||
|
-- Условие должно давать только 0 или 1 и не вызывать исключений на неверном raw.
|
||||||
--
|
--
|
||||||
-- Обе вставки должны делить один предикат. Локальный макрос Jinja
|
-- Проверяются одиннадцать полей верхнего уровня. Внутри items поля не проверяются;
|
||||||
-- разворачивает его в каждое выражение до того, как оператор разделит файл по
|
-- позиции разбирает DDS, а равенства сумм проверяются отдельно.
|
||||||
-- точкам с запятой. Так контракт остаётся рядом с запросами и не становится
|
-- Точные правила — docs/architecture/orders/ingestion.md, «Граница строгого приёма».
|
||||||
-- третьим SQL-файлом, который нельзя исполнить самостоятельно.
|
|
||||||
--
|
|
||||||
-- Строгий приём проверяет форму провода и ничего сверх неё. Ключей одиннадцать,
|
|
||||||
-- и проверяются все: десять скалярных значений прямо образуют типизированную
|
|
||||||
-- строку заказа. Внутрь items приём не смотрит — содержимое позиций, переходы
|
|
||||||
-- статуса и равенства сумм остаются ниже этой границы
|
|
||||||
-- (docs/architecture/orders/ingestion.md, «Граница строгого приёма»).
|
|
||||||
--
|
|
||||||
-- У общего предиката два обязательных свойства. Он всегда даёт 0 или 1, не
|
|
||||||
-- NULL: иначе строку не возьмут ни условие, ни его отрицание. И он не бросает
|
|
||||||
-- исключений на произвольном raw: одна грязная строка не должна ронять весь
|
|
||||||
-- переход, ради этого и существует таблица брака.
|
|
||||||
|
|
||||||
{% macro order_wire_contract() %}
|
{% macro order_wire_contract() %}
|
||||||
WITH
|
WITH
|
||||||
-- Каноническое время провода: UTC, ровно три знака долей секунды.
|
-- Время источника: UTC, ровно три знака долей секунды.
|
||||||
'yyyy-MM-dd\'T\'HH:mm:ss.SSS\'Z\'' AS ts_mask,
|
'yyyy-MM-dd\'T\'HH:mm:ss.SSS\'Z\'' AS ts_mask,
|
||||||
JSONType(raw) = 'Object' AS is_object,
|
JSONType(raw) = 'Object' AS is_object,
|
||||||
arraySort(JSONExtractKeys(raw)) = arraySort([
|
arraySort(JSONExtractKeys(raw)) = arraySort([
|
||||||
@@ -28,15 +19,14 @@ WITH
|
|||||||
]) AS keys_match,
|
]) AS keys_match,
|
||||||
JSONType(raw, 'order_id') = 'String'
|
JSONType(raw, 'order_id') = 'String'
|
||||||
AND JSONType(raw, 'status') = 'String'
|
AND JSONType(raw, 'status') = 'String'
|
||||||
-- Целое у JSONType зовётся двумя именами, и нужны оба: с одним
|
-- JSONType различает Int64 и UInt64; принимаются оба, если значение
|
||||||
-- Int64 законный идентификатор за 2^63 уехал бы в брак.
|
-- разбирается в UInt64, в том числе идентификаторы больше 2^63−1.
|
||||||
AND JSONType(raw, 'user_id') IN ('Int64', 'UInt64')
|
AND JSONType(raw, 'user_id') IN ('Int64', 'UInt64')
|
||||||
AND JSONExtract(raw, 'user_id', 'Nullable(UInt64)') IS NOT NULL
|
AND JSONExtract(raw, 'user_id', 'Nullable(UInt64)') IS NOT NULL
|
||||||
AND JSONType(raw, 'items') = 'Array'
|
AND JSONType(raw, 'items') = 'Array'
|
||||||
-- Форму держит регулярка, реальность — разбор, и порознь они дырявы:
|
-- Регулярное выражение проверяет точную форму, включая ведущие нули.
|
||||||
-- маска берёт «2026-6-3T…» без ведущих нулей, а регулярка пропускает
|
-- Разбор проверяет само время: например, отвергает 30 февраля.
|
||||||
-- 30 февраля. Обе половины измерены — docs/architecture/storage.md,
|
-- Замеры функций — docs/architecture/storage.md, «Что проверено».
|
||||||
-- «Что проверено».
|
|
||||||
AND arrayAll(k ->
|
AND arrayAll(k ->
|
||||||
JSONType(raw, k) = 'String'
|
JSONType(raw, k) = 'String'
|
||||||
AND match(JSONExtractString(raw, k),
|
AND match(JSONExtractString(raw, k),
|
||||||
@@ -44,9 +34,8 @@ WITH
|
|||||||
AND parseDateTime64InJodaSyntaxOrNull(
|
AND parseDateTime64InJodaSyntaxOrNull(
|
||||||
JSONExtractString(raw, k), ts_mask, 'UTC') IS NOT NULL,
|
JSONExtractString(raw, k), ts_mask, 'UTC') IS NOT NULL,
|
||||||
['created_at', 'updated_at'])
|
['created_at', 'updated_at'])
|
||||||
-- Деньги — строка с ровно двумя знаками после точки. Регулярка держит
|
-- Сумма в рублях: строка с двумя знаками после точки, без минуса.
|
||||||
-- форму, разбор — вместимость: у строки из двадцати девяток форма
|
-- После проверки формы проверяется возможность разбора в Decimal.
|
||||||
-- хороша, а в Decimal(18, 2) она не влезает.
|
|
||||||
AND arrayAll(k ->
|
AND arrayAll(k ->
|
||||||
JSONType(raw, k) = 'String'
|
JSONType(raw, k) = 'String'
|
||||||
AND match(JSONExtractString(raw, k), '^\\d+\\.\\d{2}$')
|
AND match(JSONExtractString(raw, k), '^\\d+\\.\\d{2}$')
|
||||||
@@ -60,11 +49,9 @@ WITH
|
|||||||
is_object AND keys_match AND fields_valid AS row_is_valid
|
is_object AND keys_match AND fields_valid AS row_is_valid
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
-- Годные версии заказа. Срез читается по _load_id — тому же, что проставил
|
-- Годные строки сохраненного среза по _load_id запуска приема.
|
||||||
-- забор: разбор идёт по неизменной порции, а не по «всему, что появилось».
|
-- WHERE исключает строки с NULL в разобранных полях; assumeNotNull
|
||||||
--
|
-- приводит оставшиеся значения к типам целевой таблицы.
|
||||||
-- Разобранные значения берёт assumeNotNull: обнуляемый разбор стоит за
|
|
||||||
-- предикатом, который NULL уже отсёк, и приведение здесь не может упасть.
|
|
||||||
|
|
||||||
INSERT INTO ods.order_dist
|
INSERT INTO ods.order_dist
|
||||||
{{ order_wire_contract() }}
|
{{ order_wire_contract() }}
|
||||||
@@ -81,30 +68,25 @@ SELECT
|
|||||||
assumeNotNull(toDecimal64OrNull(JSONExtractString(raw, 'discount'), 2)) AS discount,
|
assumeNotNull(toDecimal64OrNull(JSONExtractString(raw, 'discount'), 2)) AS discount,
|
||||||
assumeNotNull(toDecimal64OrNull(JSONExtractString(raw, 'delivery'), 2)) AS delivery,
|
assumeNotNull(toDecimal64OrNull(JSONExtractString(raw, 'delivery'), 2)) AS delivery,
|
||||||
assumeNotNull(toDecimal64OrNull(JSONExtractString(raw, 'total'), 2)) AS total,
|
assumeNotNull(toDecimal64OrNull(JSONExtractString(raw, 'total'), 2)) AS total,
|
||||||
-- items кладётся сырым фрагментом JSON, а не разобранной структурой.
|
-- items сохраняется строкой JSON для разбора позиций в DDS.
|
||||||
JSONExtractRaw(raw, 'items') AS items,
|
JSONExtractRaw(raw, 'items') AS items,
|
||||||
toDate(assumeNotNull(parseDateTimeInJodaSyntaxOrNull(
|
toDate(assumeNotNull(parseDateTimeInJodaSyntaxOrNull(
|
||||||
JSONExtractString(raw, 'snapshot_date'),
|
JSONExtractString(raw, 'snapshot_date'),
|
||||||
'yyyy-MM-dd', 'UTC'))) AS snapshot_date,
|
'yyyy-MM-dd', 'UTC'))) AS snapshot_date,
|
||||||
-- Метки запуска и прибытия переносятся как есть. Поставь здесь now64(3) —
|
-- _load_ts остается временем приема в STG; _load_id связывает с исходной порцией.
|
||||||
-- и _load_ts молча ответила бы на другой вопрос: «когда разобрали».
|
|
||||||
_load_id,
|
_load_id,
|
||||||
_load_ts
|
_load_ts
|
||||||
FROM stg.orders_raw_dist
|
FROM stg.orders_raw_dist
|
||||||
WHERE _load_id = {load_id:String} AND row_is_valid
|
WHERE _load_id = {load_id:String} AND row_is_valid
|
||||||
-- Заводская двойка parallel_distributed_insert_select исполняет вставку из
|
-- Ноль заставляет распределить результат по ключу цели. При значении 2
|
||||||
-- Distributed в Distributed локально на каждом шарде, минуя ключ шардирования
|
-- вставка может сохранить раскладку STG, где ключ — raw, вместо order_id.
|
||||||
-- цели, — так заказы и расползлись по обоим шардам. Ноль отдаёт раскладку
|
-- Подробнее — docs/architecture/storage.md, «Раскладка по шардам».
|
||||||
-- инициатору, по ключу цели. Капкан целиком — docs/architecture/storage.md,
|
|
||||||
-- «Раскладка по шардам».
|
|
||||||
SETTINGS distributed_foreground_insert = 1,
|
SETTINGS distributed_foreground_insert = 1,
|
||||||
parallel_distributed_insert_select = 0;
|
parallel_distributed_insert_select = 0;
|
||||||
|
|
||||||
-- Брак: тот же срез и буквальное отрицание того же предиката.
|
-- Ошибки: тот же срез и отрицание условия годности.
|
||||||
--
|
-- Класс определяется первым нарушенным условием: например, JSON-строка
|
||||||
-- Классы перекрываются, поэтому проверяются по порядку, а в error_class идёт
|
-- не проходит и проверку объекта, и проверку ключей, но получает not_an_object.
|
||||||
-- первый совпавший: скаляр проваливает и проверку на объект, и сверку ключей —
|
|
||||||
-- без объявленного порядка он попал бы то в один класс, то в другой.
|
|
||||||
|
|
||||||
INSERT INTO ods.order_errors_dist
|
INSERT INTO ods.order_errors_dist
|
||||||
{{ order_wire_contract() }}
|
{{ order_wire_contract() }}
|
||||||
@@ -124,7 +106,6 @@ SELECT
|
|||||||
_load_ts
|
_load_ts
|
||||||
FROM stg.orders_raw_dist
|
FROM stg.orders_raw_dist
|
||||||
WHERE _load_id = {load_id:String} AND NOT row_is_valid
|
WHERE _load_id = {load_id:String} AND NOT row_is_valid
|
||||||
-- Тот же ноль: ключ брака сегодня совпадает с ключом STG, и локальная
|
-- Та же настройка: распределяем строки по ключу целевой таблицы ошибок.
|
||||||
-- запись легла бы верно — но по совпадению, а не по контракту.
|
|
||||||
SETTINGS distributed_foreground_insert = 1,
|
SETTINGS distributed_foreground_insert = 1,
|
||||||
parallel_distributed_insert_select = 0;
|
parallel_distributed_insert_select = 0;
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
-- Забор порции слепка заказов из Kafka в STG.
|
-- Порция сообщений заказов из Kafka в STG, без разбора JSON.
|
||||||
|
-- Один SELECT читает одну порцию; непрочитанное остается до следующего запуска.
|
||||||
|
-- Полнота слепка не проверяется. Если после чтения вставка упадет, подтвержденные
|
||||||
|
-- сообщения уже не перечитаются (docs/architecture/orders/ingestion.md).
|
||||||
--
|
--
|
||||||
-- Забор — один прямой SELECT, без цикла до пустоты: одна порция ClickHouse
|
-- stream_like_engine_allow_direct_select разрешает прямое чтение Kafka;
|
||||||
-- берёт десятки тысяч сообщений, а слепок дня — порядка полутора тысяч строк.
|
-- kafka_commit_on_select включен в sql/ddl/10-stg-tables.sql.
|
||||||
-- Короткая порция оставит хвост до следующего прогона, а отказ после чтения
|
-- distributed_foreground_insert = 1 ждет записи на шарды до завершения задачи.
|
||||||
-- унесёт прочитанное с собой: офсеты коммитятся в момент чтения. Граница
|
|
||||||
-- целиком — ADR 0008, «Следствия».
|
|
||||||
--
|
|
||||||
-- stream_like_engine_allow_direct_select разрешает читать чтеца запросом; вторая
|
|
||||||
-- половина пары объявлена на самой таблице (sql/ddl/10-stg-tables.sql).
|
|
||||||
-- distributed_foreground_insert = 1 — конвенция ETL-вставок стенда: задача не
|
|
||||||
-- должна зеленеть раньше, чем строки легли на шарды.
|
|
||||||
|
|
||||||
INSERT INTO stg.orders_raw_dist
|
INSERT INTO stg.orders_raw_dist
|
||||||
SELECT
|
SELECT
|
||||||
|
|||||||
Reference in new issue
Block a user