Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
086007fbd4 | ||
|
|
5f317ba8d2 | ||
|
|
84dc718f62 | ||
|
|
5a3e3f92d9 | ||
|
|
f3373f1e8f |
No files matched your search
+247
-201
@@ -1,229 +1,275 @@
|
||||
# Кликстрим-платформа (стенд v2)
|
||||
|
||||
Словарь понятий проекта: одни и те же слова для одних и тех же вещей —
|
||||
у владельца, кода, документов и агентов. Только язык, никаких решений.
|
||||
Словарь понятий, которые встречаются в учебных заданиях, документации и коде
|
||||
стенда.
|
||||
|
||||
## Язык
|
||||
|
||||
**Хранилище**:
|
||||
Аналитическая база стенда — кластер ClickHouse со слоями STG/ODS/DDS/DM.
|
||||
Принимающая сторона границы «трекер | хранилище»: нормализует имена и стили
|
||||
источников, строится по их документации.
|
||||
_Избегать_: склад, склад данных
|
||||
- [Люди и события](#люди-и-события)
|
||||
- [Хранилище и форматы данных](#хранилище-и-форматы-данных)
|
||||
- [Заказы](#заказы)
|
||||
- [Модельное время и запуск стенда](#модельное-время-и-запуск-стенда)
|
||||
- [Устройство генератора](#устройство-генератора) — для чтения его кода
|
||||
и технических справочников.
|
||||
|
||||
**Сырые данные**:
|
||||
Слой STG хранилища и его содержимое: сообщение топика, положенное строкой,
|
||||
как пришло, без разбора. В прозе — сырые данные и сырой слой; про
|
||||
конкретную таблицу или базу — STG.
|
||||
_Избегать_: сырьё
|
||||
|
||||
**Состав мира**:
|
||||
Постоянная часть мира генератора — популяция посетителей, их привычки,
|
||||
двухкуковые пары. По дням его выдаёт план состава.
|
||||
_Избегать_: состояние мира
|
||||
|
||||
**План состава**:
|
||||
Способ спросить состав мира: функция зерна, выдающая его по дням —
|
||||
когорту новых кук, их возвраты, назначенные заказы двухкуковых пар.
|
||||
|
||||
**Подпоток**:
|
||||
Ветвь дерева случайности генератора: своё зерно у состава мира, у каждого
|
||||
дня и у каждого компонента дня. Подпоток задан позицией в дереве, а не
|
||||
порядком вычислений.
|
||||
|
||||
**Конфигурация мира**:
|
||||
Модуль чистых данных со всеми числами мира: приток, профили возвратов, доли
|
||||
покупателей, D0. Правка модуля — смена мира. Модуль-близнец контракта схемы:
|
||||
там колонки, здесь числа.
|
||||
|
||||
**Когорта дня**:
|
||||
Люди, впервые пришедшие в мир в один день, со всеми их куками и днями
|
||||
активности. Единица плана состава: когорта — функция зерна и номера дня.
|
||||
### Люди и события
|
||||
|
||||
**Человек**:
|
||||
Устойчивая личность в составе мира, которой принадлежат одна или две куки.
|
||||
Не поле источника: кликстрим видит куку, а заказ — пользователя магазина.
|
||||
Участник модели магазина, который может заходить с одного или двух
|
||||
устройств. Генератор знает, какие идентификаторы посетителя ему принадлежат,
|
||||
но в событиях сайта этой связи нет.
|
||||
|
||||
**Посетитель**:
|
||||
Кука, наблюдаемая в кликстриме и обозначенная `ClientID`; единица
|
||||
`uniq(ClientID)`. Один человек может быть представлен несколькими посетителями.
|
||||
_Избегать_: «посетитель» про человека после склейки
|
||||
Браузер на устройстве, который счетчик сайта узнает по куке — сохраненному
|
||||
в браузере идентификатору. В кликстриме он обозначается `ClientID`:
|
||||
`uniq(ClientID)` считает посетителей, и у одного человека их может быть несколько.
|
||||
_Избегать_: «посетитель» про человека после объединения его идентификаторов.
|
||||
|
||||
**Пользователь магазина**:
|
||||
Представление человека со стороны заказов; в записи заказа обозначается
|
||||
`user_id`. Через заказы связывает разные куки человека, в кликстрим напрямую
|
||||
не попадает.
|
||||
|
||||
**Приток**:
|
||||
Появление новых кук на всём протяжении оси модельного времени; единица —
|
||||
кука (`ClientID`). Из-за притока накопленная аудитория растёт с
|
||||
горизонтом и не совпадает с дневной.
|
||||
|
||||
**Хвост возвратов**:
|
||||
Окно активности, отсчитанное от первого дня человека и общее на обе
|
||||
его куки; дольше окна кука не возвращается.
|
||||
|
||||
**Предыстория**:
|
||||
Когорты плана с первым днём активности до D0; событий не порождают.
|
||||
|
||||
**День активности**:
|
||||
День, в который кука хоть раз появилась в мире: день её рождения или день
|
||||
возврата. Единица плана состава: за день он у куки один, а визитов внутри
|
||||
него бывает несколько.
|
||||
_Избегать_: визит (визит — про сессию)
|
||||
Человек, известный серверной части магазина под идентификатором `user_id`.
|
||||
Он указан в заказах и позволяет связать разные `ClientID` одного человека;
|
||||
в кликстриме поля `user_id` нет. Например, покупки с телефона и ноутбука
|
||||
могут иметь разные `ClientID`, но один `user_id` в заказах.
|
||||
|
||||
**Визит**:
|
||||
Подряд идущие события одной куки без пауз длиннее 30 минут, не пересекающие
|
||||
границу суток. Поле `VisitID` — эталон для лабы сессий. «Сессия» — то же
|
||||
понятие словами аналитики: в данных и в коде оно зовётся визитом, но имена
|
||||
вроде «лаба сессий» и «сборка сессий» остаются.
|
||||
Последовательность событий одного посетителя без пауз длиннее 30 минут
|
||||
и без перехода через полночь по часовому поясу счетчика. Сессия означает
|
||||
то же самое; поле `VisitID` служит эталоном в задании по нарезке сессий.
|
||||
|
||||
**День активности**:
|
||||
День, в который посетитель появился на сайте хотя бы раз: впервые или
|
||||
вернувшись. За один день активности у него может быть несколько визитов.
|
||||
_Избегать_: визит в значении целого дня активности.
|
||||
|
||||
**Паспорт куки**:
|
||||
Устройство и город, приписанные куке на всю жизнь: кука — это браузер на
|
||||
устройстве. Держит их план состава, разворачивает в поля события
|
||||
день-функция; у двухкуковой пары город один на две куки, устройства разные.
|
||||
|
||||
**Покупатель**:
|
||||
Человек, которого план состава пометил склонным покупать. Метка значима:
|
||||
помеченный доходит до заказа заметно чаще прочих, но и непомеченный иногда
|
||||
покупает. Из покупателей отбираются двухкуковые пары.
|
||||
_Избегать_: «покупатель» про того, кто купил в конкретный день — это визит
|
||||
с заказом.
|
||||
|
||||
**День-функция**:
|
||||
Функция (зерно, номер дня), выдающая упорядоченный поток событий этих
|
||||
модельных суток. Состояния между днями нет: день D не зависит от того,
|
||||
прожиты ли дни до него.
|
||||
Устройство и город, закрепленные за посетителем во все дни его активности.
|
||||
Генератор записывает их в поля событий. У двух посетителей одного человека
|
||||
город совпадает, а устройства различаются.
|
||||
|
||||
**Торговое событие**:
|
||||
Событие корзины или покупки — отдельная строка потока, а не просмотр
|
||||
страницы. Садится на ту страницу, где случилось: корзина — на карточку
|
||||
товара, покупка — на страницу подтверждения заказа.
|
||||
Запись о добавлении товара в корзину или о покупке: `add_to_cart` или
|
||||
`purchase`. Это отдельная строка кликстрима со страницей, где произошло
|
||||
действие: карточкой товара для корзины или страницей подтверждения для покупки.
|
||||
|
||||
### Хранилище и форматы данных
|
||||
|
||||
**Хранилище**:
|
||||
Аналитическая база стенда в ClickHouse. Данные проходят слои STG, ODS, DDS
|
||||
и DM: от исходных сообщений до витрин для отчетов. Хранилище принимает
|
||||
данные по документации источников и приводит их к своей модели и именам.
|
||||
_Избегать_: склад, склад данных.
|
||||
|
||||
**Сырые данные**:
|
||||
Исходные сообщения Kafka, сохраненные в слое STG без разбора: одно сообщение
|
||||
занимает одну строку таблицы. Слой STG также называют сырым слоем.
|
||||
_Избегать_: сырьё.
|
||||
|
||||
**Справочник**:
|
||||
Данные, которые хранилище не производит, а получает готовыми со стороны:
|
||||
содержимым владеет источник вне хранилища, слои только читают. Живут в базе
|
||||
`dic`. Первый и пока единственный — каталог товаров.
|
||||
_Избегать_: словарь — этим словом в проекте зовут и глоссарий понятий, и
|
||||
объект `DICTIONARY` ClickHouse, одну из форм, в которой справочник доступен.
|
||||
Готовые сведения из внешнего источника, которыми хранилище дополняет
|
||||
свои данные. На стенде они находятся в базе `dic`; пока это только каталог
|
||||
товаров. Объект `DICTIONARY` ClickHouse — одна из форм доступа к справочнику.
|
||||
_Избегать_: словарь в значении справочных данных: это слово уже обозначает
|
||||
`DICTIONARY` и словарь понятий.
|
||||
|
||||
**Каталог товаров**:
|
||||
`data/catalog/products.csv` — общий файл генератора и словаря
|
||||
ClickHouse. Форма файла решена, длина — нет: строки дописываются.
|
||||
|
||||
**Уровень спроса**:
|
||||
Свойство товара в каталоге: как часто открытую карточку кладут в корзину.
|
||||
Три значения — магнит, обычный, залёживается. Постоянная часть мира, а не
|
||||
поведение дня: он и делает конверсию «просмотр → корзина» по товарам и
|
||||
брендам различимой.
|
||||
|
||||
**Ось модельного времени**:
|
||||
Собственный календарь мира генератора. Дни считаются от фиксированного
|
||||
первого дня D0; реальный календарь в модели не участвует. Между прогонами
|
||||
живут только зерно и позиция на оси.
|
||||
|
||||
**Стартовый мир**:
|
||||
Первые восемь дней оси (понедельник по понедельник), которыми стенд
|
||||
наполняется при каждом `make up`. Он же эталонный мир, всегда один и тот же:
|
||||
на нем принимаются следующие этапы.
|
||||
_Избегать_: зерновой мир
|
||||
|
||||
**Паспорт мира**:
|
||||
`data/world-inventory.json` — единственное, что о мире хранится в git: шапка
|
||||
(зерно, версия генератора, хеш каталога), строка на каждый день событий
|
||||
(число и хеш байтов) и строка на каждый отправленный слепок (хеш байтов).
|
||||
Счётчик сверх этого заводится только с названным читателем. Сам мир не
|
||||
хранится, а пересчитывается; паспорт отвечает на один вопрос: тот ли это мир.
|
||||
_Избегать_: опись мира, манифест, мини-манифест
|
||||
|
||||
**Пошаговый режим**:
|
||||
Базовый способ движения по оси модельного времени: «прожить следующий
|
||||
день» — явное действие.
|
||||
|
||||
**Живой день**:
|
||||
Проигрывание текущего модельного дня в реальном времени с ускорением;
|
||||
включается по требованию — постоянным фоном идёт, только пока включён
|
||||
выключатель `world_live`.
|
||||
|
||||
**Пакетный режим**:
|
||||
Проигрывание готового дня пачкой, без темпа: заливка снимка при старте
|
||||
стенда, пересборки и проверки.
|
||||
|
||||
**Даги модельного времени**:
|
||||
Даги Airflow, которыми двигают ось модельного времени: шаговые даги играют
|
||||
день — пачкой или в темпе, — а выключатель держит стенд живущим, пока включён.
|
||||
Коротко — даги времени.
|
||||
_Избегать_: пульт мира, управляющие даги
|
||||
|
||||
**Шаговый даг**:
|
||||
Даг модельного времени, который играет день и ставит позицию на оси:
|
||||
`world_next_day` — пачкой, `world_live_day` — в темпе. Запускается руками,
|
||||
расписания и паузы не имеет; автоматику поверх него даёт выключатель.
|
||||
_Избегать_: работник
|
||||
|
||||
**Граница суток**:
|
||||
Единственный структурный шов модели: сессии режутся по ней, слепок заказов
|
||||
снимается на ней, день проживается только целиком.
|
||||
|
||||
**Контракт схемы**:
|
||||
Python-модуль с описателями колонок события — собственность генератора.
|
||||
Из него выводятся генератор, валидация и документация формата; хранилище
|
||||
строится по документации, не по модулю.
|
||||
Список товаров с артикулами, названиями, категориями, брендами, ценами
|
||||
и уровнями спроса. Файл `data/catalog/products.csv` общий для генератора
|
||||
и справочника ClickHouse; товары можно добавлять строками в этом формате.
|
||||
|
||||
**Нормализованное имя**:
|
||||
Имя колонки источника, приведённое к нашему стилю (snake_case). Живёт в
|
||||
контракте схемы и в описании выгрузки. Не то же, что имя атрибута в модели
|
||||
данных: слой DDS складывает модель и называет атрибуты по ней.
|
||||
Имя поля источника, записанное в стиле `snake_case`: например,
|
||||
`ClientID` становится `client_id`. Оно указано в описании выгрузки и
|
||||
контракте схемы. Слой DDS выбирает имена по своей модели данных, поэтому
|
||||
они не обязаны повторять нормализованные имена источника.
|
||||
|
||||
**Описание выгрузки**:
|
||||
Публичная документация формата события: таблица колонок, собранная из
|
||||
контракта схемы. По ней пишется сторона хранилища — как в бою по документации
|
||||
источника. Правится только контракт, документ пересобирается.
|
||||
_Избегать_: описание схемы, документация контракта
|
||||
|
||||
**Канонический сериализатор**:
|
||||
Единственная граница, где запись генератора превращается в байты. Фиксированный
|
||||
порядок ключей и строк — основа побайтовой воспроизводимости.
|
||||
|
||||
**Проигрыватель**:
|
||||
Компонент доставки готового потока дня в приёмник. Два режима: пакетный
|
||||
(пачкой, без темпа) и живой день.
|
||||
|
||||
**Слепок**:
|
||||
Полная выгрузка заказов окна изменяемости, снятая бэкендом на границе суток:
|
||||
состояние заказов на этот момент, а не поток их изменений. Опоздавший заказ
|
||||
может отсутствовать в ранних слепках; после первого появления ездит до конца
|
||||
своего окна.
|
||||
|
||||
**Дата слепка**:
|
||||
Дата завершившегося модельного дня, состояние которого снято на исходящей
|
||||
границе суток. Одна для всех записей выгрузки; не дата заказа и не время
|
||||
отправки сообщения.
|
||||
|
||||
**Время аудита источника**:
|
||||
Время создания или последнего изменения строки заказа по часам базы источника.
|
||||
В записи слепка это `created_at` и `updated_at`; бизнес-время покупки живёт в
|
||||
событии `purchase` отдельно.
|
||||
|
||||
**Судьба заказа**:
|
||||
Исход, моменты изменений, вычеркнутая позиция и опоздание заказа — всё
|
||||
решается бросками при его рождении и укладывается в окно изменяемости целиком.
|
||||
Слепок любого дня — чтение готовой судьбы, а не накопление состояния.
|
||||
|
||||
**Окно изменяемости**:
|
||||
Сколько модельных дней заказ ещё может измениться и потому продолжает ездить
|
||||
в слепках. Константа мира: семь дней. За окном заказ замёрз, выручка дня
|
||||
перестала «дышать».
|
||||
|
||||
**Пакетный забор**:
|
||||
Способ приёма топика, при котором данные тянет запрос по команде, а не
|
||||
матвью непрерывно. Так стенд принимает заказы: у слепка есть начало и конец,
|
||||
и забирать его уместно тогда, когда он собран целиком. Противоположность —
|
||||
потоковый приём событий.
|
||||
_Избегать_: путать с приёмником и пакетным режимом — те про сторону
|
||||
генератора, про отправку; забор — про сторону хранилища, про чтение.
|
||||
Документ, который объясняет поля и формат событий источника:
|
||||
[`docs/formats/clickstream-event.md`](docs/formats/clickstream-event.md).
|
||||
По нему строится прием данных в хранилище. Документ собирается из
|
||||
описаний колонок генератора: правят их, затем пересобирают документ.
|
||||
_Избегать_: описание схемы, документация контракта.
|
||||
|
||||
**Техническая проверка**:
|
||||
Проверка, чей оракул — внутренний эталон стенда (паспорт мира), которого у
|
||||
настоящего магазина нет. За учебный урок 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 или файл. В пакетном режиме он отправляет их без пауз,
|
||||
в режиме живого дня — с ускорением по времени событий.
|
||||
+24
-32
@@ -1,18 +1,17 @@
|
||||
"""Трансформации слоя DDS: сборка моделей.
|
||||
"""Трансформации DDS: заказы, сессии и карта идентичностей.
|
||||
|
||||
Заказ и сессия собираются заменой дневных партиций. Карта идентичностей
|
||||
пересчитывается целиком после заказа, потому что читает его текущую модель.
|
||||
Запросы рассчитывают заказы и сессии за выбранные дни и записывают результат
|
||||
в промежуточные таблицы. Затем готовые дневные разделы этих таблиц заменяют
|
||||
разделы основных таблиц. Пока идет расчет, читатели видят прежние данные.
|
||||
|
||||
Расписания у дага нет и параметров тоже: объём каждого прогона он выводит сам
|
||||
из состояния слоёв и позиции мира, поэтому один и тот же запуск одинаково
|
||||
верен из триггера дневного конвейера, с кнопки менти и после падения
|
||||
(docs/architecture/etl.md, «Объём прогона»).
|
||||
Карта идентичностей пересчитывается целиком после заказов. Новые строки
|
||||
добавляются с меткой времени сборки; представление с FINAL выбирает последнюю
|
||||
версию каждой строки.
|
||||
|
||||
Заказ и сессия гасят повтор заменой партиции целиком: день собирается в
|
||||
таблицу-двойник и подменяет собой партицию цели. Карта пишет полный набор пар
|
||||
новой версией, а точное чтение даёт FINAL. Замена партиции разобрана в
|
||||
sql/dds/order_replace.sql; точное чтение карты описано в
|
||||
sql/ddl/50-dds-views.sql.
|
||||
Даг запускается без расписания и параметров. Даты он выбирает по состоянию
|
||||
таблиц и позиции мира. Новый запуск заново определяет диапазон: наличие даты
|
||||
не подтверждает полноту дня и не гарантирует повтор его сборки после сбоя.
|
||||
Подробности — docs/architecture/etl.md, «Объём прогона» и «Идемпотентность».
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -257,16 +256,11 @@ def _days(first_day: datetime.date, last_day: datetime.date) -> list[str]:
|
||||
|
||||
@task
|
||||
def replacements(scope: dict[str, object]) -> list[dict[str, object]]:
|
||||
"""Разложить дни в аргументы размноженной задачи замены.
|
||||
"""Подготовить параметры отдельной задачи replace для каждого дня.
|
||||
|
||||
Шаг существует ради ограничения Airflow: размножать задачу можно
|
||||
только по целому возврату другой задачи — ни по ключу словаря, ни по
|
||||
карте поверх него. Отсюда отдельный возврат под один список.
|
||||
|
||||
День едет в запрос подстановкой Jinja, а не параметром ClickHouse:
|
||||
в ALTER … PARTITION сервер параметр не подставляет — замерено,
|
||||
отвечает синтаксической ошибкой. Значение при этом не внешнее, его
|
||||
вернул сам ClickHouse запросом объёма.
|
||||
expand_kwargs получает список словарей и создает по одному экземпляру
|
||||
задачи на каждый словарь. Дата подставляется в SQL шаблоном Jinja:
|
||||
параметр ClickHouse в ALTER ... PARTITION здесь не поддерживается.
|
||||
"""
|
||||
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,
|
||||
start_date=START_DATE,
|
||||
is_paused_upon_creation=False,
|
||||
# Два прогона разом собирали бы один и тот же двойник: второй затёр бы
|
||||
# чужую сборку между вставкой и заменой партиций.
|
||||
# Одновременные запуски используют одни промежуточные таблицы.
|
||||
# Второй мог бы очистить результат первого до замены партиций.
|
||||
max_active_runs=1,
|
||||
template_searchpath=str(SQL_ROOT),
|
||||
tags=["dds"],
|
||||
@@ -298,7 +292,7 @@ def dds_transform():
|
||||
|
||||
@task_group(group_id="order")
|
||||
def order():
|
||||
"""Заказ: дышащее окно плюс дни, которых в слое ещё нет."""
|
||||
"""Заказ: изменяемые дни последнего слепка и пропущенные дни."""
|
||||
|
||||
@task
|
||||
def scope() -> dict[str, object]:
|
||||
@@ -306,8 +300,7 @@ def dds_transform():
|
||||
query = (SQL_ROOT / "dds" / "order_scope.sql").read_text(encoding="utf-8")
|
||||
hook = ClickHouseHook(clickhouse_conn_id=CLICKHOUSE_CONNECTION)
|
||||
row = hook.get_first(query)
|
||||
# Пустой ответ означает пустой источник: собирать нечего, и
|
||||
# зеленеть на этом нельзя — прогон встаёт с внятной причиной.
|
||||
# Без заказов ODS расчет невозможен: задача сообщает об ошибке.
|
||||
if row is None:
|
||||
raise AirflowException(
|
||||
"в ods.order нет ни одного заказа: собирать нечего; "
|
||||
@@ -342,9 +335,9 @@ def dds_transform():
|
||||
do_xcom_push=False,
|
||||
)
|
||||
|
||||
# Замена идёт по партиции за раз — другой формы у операции нет. День
|
||||
# становится отдельной задачей: в интерфейсе видно, сколько дней несёт
|
||||
# прогон, а повтор после отказа стоит одного дня, а не всей пересборки.
|
||||
# На каждый день создается отдельная задача замены партиции.
|
||||
# Неудачную замену можно повторить, пока промежуточная таблица
|
||||
# сохраняет рассчитанные строки этого запуска.
|
||||
replace = SQLExecuteQueryOperator.partial(
|
||||
task_id="replace",
|
||||
conn_id=CLICKHOUSE_CONNECTION,
|
||||
@@ -370,9 +363,8 @@ def dds_transform():
|
||||
"проверьте, дошёл ли поток до хранилища"
|
||||
)
|
||||
first_day, last_day = row
|
||||
# Пропуск, а не отказ: слой полон по последний прожитый день, а
|
||||
# сегодняшний ещё идёт. Это обычное состояние повторного прогона,
|
||||
# и в интерфейсе оно видно пропущенной группой.
|
||||
# Для каждого прожитого дня уже есть строки сессий.
|
||||
# Новых дней для расчета нет: группа задач будет пропущена.
|
||||
if first_day is None:
|
||||
raise AirflowSkipException(
|
||||
f"сессии собраны по {last_day} включительно, "
|
||||
|
||||
+12
-8
@@ -1,9 +1,12 @@
|
||||
"""Трансформации слоя DM: сборка стендовых витрин.
|
||||
|
||||
Выручка наследует ритм заказов. Сверка соединяет окно заказов с прожитым
|
||||
хвостом событий. Обе заменяют дневные партиции. Трафик наследует
|
||||
ретроспективность карты идентичностей и пересчитывает всю историю новой
|
||||
версией. Объем каждого прогона выводится из состояния слоев.
|
||||
Даты для расчета выручки берутся из последнего слепка заказов и списка
|
||||
пропущенных дней. Сверка покупок с заказами учитывает также прожитые дни
|
||||
событий и дни, где заказ еще не найден. Запросы записывают результаты
|
||||
в промежуточные таблицы, затем заменяют дневные разделы основных таблиц.
|
||||
|
||||
Трафик пересчитывается за всю историю: новая связь посетителя с пользователем
|
||||
магазина меняет число известных пользователей и за прошлые дни.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -62,10 +65,11 @@ def _days(first_day: datetime.date, last_day: datetime.date) -> list[str]:
|
||||
|
||||
@task
|
||||
def replacements(scope: dict[str, object]) -> list[dict[str, object]]:
|
||||
"""Разложить дни в аргументы размноженной задачи замены.
|
||||
"""Подготовить параметры отдельной задачи replace для каждого дня.
|
||||
|
||||
Отдельный словарь нужен каждому экземпляру ``expand_kwargs``; тот же
|
||||
прием разобран в ``dds_transform.py`` на замене партиций заказов.
|
||||
expand_kwargs получает список словарей и создает по одному экземпляру
|
||||
задачи на каждый словарь. Дата подставляется в SQL шаблоном Jinja:
|
||||
параметр ClickHouse в ALTER ... PARTITION здесь не поддерживается.
|
||||
"""
|
||||
return [{"params": {"day": day}} for day in scope["days"]]
|
||||
|
||||
@@ -141,7 +145,7 @@ def dm_transform():
|
||||
|
||||
@task_group(group_id="purchase_vs_orders")
|
||||
def purchase_vs_orders():
|
||||
"""Сверка: окно заказов, прожитый хвост и предварительные дни."""
|
||||
"""Сверка: изменяемые заказы, прожитые покупки и дни ожидания заказа."""
|
||||
|
||||
@task
|
||||
def scope() -> dict[str, object]:
|
||||
|
||||
@@ -1,111 +1,118 @@
|
||||
# Модель сессии в DDS
|
||||
|
||||
Учебный результат: менти строит расчётную сущность, которой нет в потоке, —
|
||||
режет события на сессии по правилу визита, чеканит идентификатор и сверяет
|
||||
свою нарезку с эталоном. Стенд здесь играет роль сессионизатора: в бою её
|
||||
играет Метрика, которая режет визиты у себя и с октября 2025 отдаёт готовый
|
||||
`VisitID` прямо в хитах. Поэтому стенд считает сессии сам, а `VisitID`
|
||||
генератора использует только как эталон самопроверки — иначе сборка сессий
|
||||
выродилась бы в `GROUP BY` по чужому полю и центральный урок
|
||||
слоя пропал бы. Без этого абзаца действия стенда выглядят странно: «зачем
|
||||
считать то, что уже дано колонкой».
|
||||
Сессия объединяет события одного визита. На стенде вы учитесь определять
|
||||
ее границы по событиям, вычислять устойчивый идентификатор и сверять
|
||||
результат с эталоном. Поэтому сборка читает `dds.event_v`, где нет готового
|
||||
`VisitID`. Этот идентификатор источника остается в ODS для проверки.
|
||||
|
||||
## Зерно
|
||||
|
||||
Одна строка — один визит ([CONTEXT.md](../../../CONTEXT.md)): подряд идущие
|
||||
события одной куки без пауз длиннее 30 минут, не пересекающие границу суток.
|
||||
В нарезку входят все три типа событий — торговые садятся на свои страницы
|
||||
и держат сессию наравне с просмотрами. События внутри сессии
|
||||
упорядочиваются по метке времени события (`UTCEventTime` — опорная колонка
|
||||
приёма).
|
||||
Зерно описывает, чему соответствует одна строка: здесь это один
|
||||
[визит](../../../CONTEXT.md#люди-и-события). В него входят подряд идущие
|
||||
события одного `client_id` без пауз длиннее 30 минут и без перехода через
|
||||
модельную полночь. Пауза ровно в 30 минут новую сессию не открывает.
|
||||
Учитываются все три типа событий: `pageview`, `add_to_cart`, `purchase`.
|
||||
|
||||
Граница суток проходит по модельному дню счётчика, и он у события уже есть:
|
||||
`EventDate` — свойство содержимого. Раз сессия сутки не пересекает, у всех
|
||||
её событий `EventDate` один; он и становится **днём сессии** — колонкой
|
||||
партиции, без пересчёта.
|
||||
День сессии `session_date` берется из `event_date` событий DDS — это
|
||||
`EventDate` источника, дата по часовому поясу счетчика. У всех событий
|
||||
одной сессии она одинакова. Пересчитывать ее из времени UTC не нужно.
|
||||
|
||||
SQL рассчитывает границы визитов двумя оконными функциями: первая отмечает
|
||||
начало нового визита, вторая по этим отметкам присваивает событиям номер
|
||||
визита. Обе используют порядок `(event_time, event_id)`: при равном времени
|
||||
события идут по идентификатору. Иначе два расчета могли бы по-разному
|
||||
упорядочить события и отнести отметку начала к разным строкам.
|
||||
|
||||
## Идентификатор
|
||||
|
||||
Идентификатор сессии в природе не существует: сессия — расчётная сущность,
|
||||
и id ей чеканит тот, кто сессионизирует. `VisitID` — такой же суррогат,
|
||||
отчеканенный генератором в роли Метрики; наш — `session_id`,
|
||||
детерминированный хеш от пары «кука + начало сессии» (`cityHash64`).
|
||||
Детерминированность обязательна: партиция дня пересобирается заменой, и
|
||||
повторный прогон обязан отчеканить те же id — счётчик или UUID это правило
|
||||
ломают.
|
||||
`session_id` вычисляется как `cityHash64(client_id, min(event_time))`.
|
||||
При повторной сборке тех же событий посетитель и начало визита остаются
|
||||
прежними, поэтому идентификатор не меняется. Счетчик или случайный UUID
|
||||
выдали бы другие значения.
|
||||
|
||||
`session_id` — не ключ сортировки и не ключ шардирования, а колонка-ручка:
|
||||
в соединениях он не участвует — события его не несут, мост к заказам живёт
|
||||
на событиях. Читатели ручки — лаба сессий (сопоставить свою нарезку со
|
||||
стендовой и назвать расхождение одним числом) и разбор DQ-расхождений.
|
||||
По `session_id` можно сопоставить сессии до и после пересборки и назвать
|
||||
конкретную строку при разборе расхождения. Он не используется для сортировки
|
||||
или шардирования. У событий этого поля нет; связь с заказами строится
|
||||
по событиям покупки.
|
||||
|
||||
## Состав строки
|
||||
|
||||
Боевой образец — таблица визитов Метрики, парная к нашему формату хита; по
|
||||
ней состав и собран:
|
||||
- `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`, начало и конец сессии,
|
||||
длительность, день сессии. Времена — в поясе счётчика по правилу слоя;
|
||||
- **счётчики**: событий всего, просмотров, корзин, покупок;
|
||||
- **страницы**: входная и выходная — как `StartURL`/`EndURL` визита; полного
|
||||
списка страниц нет и у Метрики, топы считаются по событиям;
|
||||
- **атрибуция**: UTM-метки первого события сессии — так визит атрибутирует
|
||||
Метрика. В v1 витрина брала последнее касание — заменено осознанно;
|
||||
- **паспорт куки**: категория устройства (расшифрованная, язык бизнес-модели)
|
||||
и город — у куки они постоянны, берутся с первого события;
|
||||
- **служебные**: `_load_id` и `_load_ts` собравшего запуска — правило слоя
|
||||
([модель заказа](order.md), «Служебные колонки»).
|
||||
В готовом SQL страницы выбираются через `argMin` и `argMax`, а UTM,
|
||||
устройство и город — через `argMin`, только по `event_time`. При одинаковом
|
||||
времени выбор между событиями не определен. Полный порядок окон нарезки
|
||||
сам по себе эту неоднозначность не устраняет. Это ограничение учитывается
|
||||
в [задании ступени 4](../../labs/4-1-session-pipeline.md).
|
||||
|
||||
Осознанные расхождения с образцом: **денег в сессии нет** — визит Метрики
|
||||
несёт клиентскую выручку, у нас деньги считает только бэкенд (спека, раздел
|
||||
7), а клиентская сумма живёт в сверке; **целей нет** — их место заняли
|
||||
счётчики торговых событий.
|
||||
В сессии нет суммы выручки: ее считают по заказам магазина. Клиентская
|
||||
сумма покупки используется в сверке с заказами и в учебной UTM-витрине.
|
||||
|
||||
## Движок и наполнение
|
||||
|
||||
`dds.session_rep` — `ReplicatedMergeTree` без колонки версии, по образцу
|
||||
заказа: партиция — день сессии, ключ сортировки — кука и начало сессии.
|
||||
`dds.session_dist` шардирует по `cityHash64(client_id)`, тем же выражением,
|
||||
что события, — это ко-локация с `dds.identity_map_rep` ([дока
|
||||
хранилища](../storage.md), «Раскладка по шардам»).
|
||||
Совпадение ключа с событиями не случайно: все события куки лежат на одном
|
||||
шарде, поэтому сессионизация локальна по природе — капкан смены ключа при
|
||||
вставке, который дока хранилища обещает сборкам DDS, у сессий не стреляет.
|
||||
`dds.session_rep` хранит готовые сессии в `ReplicatedMergeTree` без колонки
|
||||
версии. При чтении не нужно заново выполнять оконный расчет. Партиция —
|
||||
раздел таблицы за один `session_date`; ключ сортировки — посетитель
|
||||
и начало сессии. Читайте результат через `dds.session_v`: представление
|
||||
над `dds.session_dist` явно перечисляет деловые и служебные колонки.
|
||||
|
||||
Сборка — дневной батч: хранилище выше ODS по смыслу батчевое, раз второй
|
||||
источник приходит раз в сутки. Собирается день, который мир уже прожил, и
|
||||
собирается один раз и целиком; какой день прожит, слой спрашивает у позиции
|
||||
мира ([дневной конвейер](../etl.md), «Объём прогона»). Идемпотентность держит
|
||||
замена партиции. В отличие от заказов, где батч пересобирает до K «дышащих»
|
||||
дней, у сессии дышащего окна нет: прожитый день самодостаточен (правило
|
||||
спеки, раздел 6). Второй раз он соберётся только
|
||||
вынужденно — когда упавшая замена оставила позади дыру и хвост от неё накрыл
|
||||
соседние дни. Дней в одном прогоне при этом бывает несколько — столько,
|
||||
сколько мир успел прожить без слоя. Внутри живого дня
|
||||
модель сессии про сегодня честно молчит — «сегодня» смотрят через
|
||||
`dds.event_v`; сессия и по смыслу не финальна, пока день не прожит.
|
||||
Источник сборки — `dds.event_v`. События, сессии и карта идентичностей
|
||||
распределены по одному ключу `cityHash64(client_id)`: данные посетителя
|
||||
находятся на одном шарде. Роли объектов и правило записи через `_dist`
|
||||
описаны в [справочнике хранилища](../storage.md#раскладка-по-шардам).
|
||||
|
||||
Публичная поверхность — `dds.session_v`: обычное представление с явным
|
||||
списком деловых и собственных служебных колонок над `dds.session_dist`.
|
||||
Роли объектов и доступ следуют общей [конвенции](../storage.md), «Имена
|
||||
объектов».
|
||||
Три SQL-файла выполняются последовательно в группе `session` дага
|
||||
`dds_transform`:
|
||||
|
||||
Источник сборки — `dds.event_v` ([контракт события](event.md)): сессии
|
||||
нужны от него ключи нарезки, тип события, страница, UTM и паспорт куки.
|
||||
Механика запуска — где собирается партиция и как подменяется на кластере —
|
||||
не входит: это устройство [дневного конвейера](../etl.md).
|
||||
1. `session_scope.sql` находит первую дату событий, за которую еще нет
|
||||
сессий. Последний день для расчета — первый день событий ODS
|
||||
плюс `world_position` минус один; `world_position` — номер следующего
|
||||
дня для генерации. Так в расчет попадают только полностью
|
||||
прожитые дни. При пустом 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-проверка с зерном «кука × день»: число сессий нашей нарезки
|
||||
равно `uniqExact(VisitID)` по тем же событиям. Счёт по Distributed ловит и
|
||||
склеенную, и разрезанную сессию — мимо него проходит лишь взаимная
|
||||
компенсация ошибок внутри одной куки за день; эталон в модель не тащим —
|
||||
строка сессии остаётся нашей, а не генераторной. Поштучное сопоставление
|
||||
границ — материал лабы, где менти разбирает найденное расхождение руками. Дом проверки —
|
||||
контур DQ этапа 4. Сверке нужен `VisitID`, а сборке сессий — нет; контракт
|
||||
`dds.event_v` его скрывает, и за эталоном проверка идёт в `ods.event_v`
|
||||
([контракт события](event.md)).
|
||||
Проверка `sessions_vs_reference` в `dq_check` сравнивает для каждого
|
||||
посетителя и дня число строк `dds.session_v` с `uniqExact(VisitID)`
|
||||
из `ods.event_v`. Эталон читается из ODS, потому что `dds.event_v`
|
||||
скрывает `VisitID`.
|
||||
|
||||
Нулевое число расхождений подтверждает только число сессий каждого
|
||||
посетителя за день. Например, ошибочное объединение двух визитов
|
||||
и разделение другого на два сохранят их общее число. Поэтому в заданиях
|
||||
вы дополнительно сопоставляете границы сессий по исходным событиям и проверяете страницы,
|
||||
UTM, устройство и город.
|
||||
|
||||
## Читатели
|
||||
|
||||
|
||||
+168
-141
@@ -1,179 +1,202 @@
|
||||
# Слой DM: витрины
|
||||
|
||||
Как модель DDS превращается в витрины для потребителя BI: какую форму
|
||||
принимает витрина, кто какие витрины строит и как слой проверяется. Состав
|
||||
колонок каждой витрины задаёт раздел 7 мастер-спеки; здесь — форма,
|
||||
наполнение и контроль.
|
||||
Витрины готовят данные DDS для отчетов и дашбордов. Здесь описаны форма
|
||||
хранения, правила обновления и проверки. Состав колонок задан в
|
||||
[спецификации](../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)).
|
||||
Мы все равно оставили материализованные агрегаты ради практики: без них в
|
||||
программе не было бы загрузки и пересборки витрин. Представления дают другой
|
||||
случай: преобразование строк на прежнем зерне.
|
||||
Готовые таблицы нужны в программе для практики загрузки, обновления
|
||||
и включения витрин в конвейер.
|
||||
|
||||
## Что готово в стенде, а что остаётся заданием
|
||||
|
||||
Стенд привозит готовыми витрины трёх сюжетов дашборда этапа 8 и
|
||||
инфраструктуру контроля:
|
||||
Готовы три витрины дашборда и сводка качества:
|
||||
|
||||
- `revenue_daily` — выручка по дням и категориям; образец материализованной
|
||||
витрины, на него ссылаются учебные задания;
|
||||
- `purchase_vs_orders` — сверка A+C с классами расхождений; зерно — заказ,
|
||||
шардирование сохраняет ко-локацию заказов по `cityHash64(order_id)`;
|
||||
- `daily_traffic` — пара «посетители» / «известные пользователи», обогащение
|
||||
[картой идентичностей](dds/identity.md);
|
||||
- `dq_summary` — сводка проверок качества (ниже).
|
||||
- `revenue_daily` — одна строка на день заказа и категорию товара;
|
||||
образец дневной пересборки для учебных заданий.
|
||||
- `purchase_vs_orders` — одна строка на заказ: сопоставление покупки
|
||||
из кликстрима с заказом магазина и класс расхождения.
|
||||
- `daily_traffic` — одна строка на день: число посетителей и известных
|
||||
пользователей с учетом [карты идентичностей](dds/identity.md).
|
||||
- `dq_summary` — одна строка на день данных и проверку качества.
|
||||
|
||||
Остальные витрины спеки нужно построить в учебных заданиях по готовому ТЗ и
|
||||
образцу. Разминка — представление `events_enriched_v` (зерно события), затем
|
||||
агрегаты `top_pages_daily`, `session_overview` и `dq_errors_daily`, последней —
|
||||
самая содержательная `utm_effectiveness` с уроком `declared_*`. В финале
|
||||
каждого задания с агрегатом его задачу нужно добавить в `dm_transform`. До
|
||||
этого конвейер о витрине не знает, после — пересобирает её вместе со
|
||||
стендовыми. Представление из разминки добавлять некуда: его не нужно
|
||||
пересобирать. Тексты заданий лежат в [`docs/labs/`](../labs/README.md).
|
||||
В [заданиях ступени 1](../labs/README.md) вы строите `events_enriched_v`,
|
||||
`top_pages_daily`, `session_overview`, `dq_errors_daily` и `utm_effectiveness`.
|
||||
Для каждой физической витрины нужно добавить сборку в `dm_transform`.
|
||||
Представление `events_enriched_v` отдельной задачи не требует.
|
||||
|
||||
Дашборд этапа 8 читает только готовые витрины стенда, поэтому его приёмка не
|
||||
зависит от домашних заданий. Отдельный дашборд по учебным витринам можно
|
||||
собрать в [факультативном задании](../labs/optional/superset-dashboard.md),
|
||||
доступном после ступени 2. Устройство стендового дашборда описано в
|
||||
[справочнике Superset](superset.md).
|
||||
Стендовый дашборд читает готовые витрины и работает до выполнения заданий.
|
||||
Дашборд по своим витринам можно собрать в
|
||||
[факультативе](../labs/optional/superset-dashboard.md) после ступени 2.
|
||||
Устройство стендового дашборда — в [справочнике Superset](superset.md).
|
||||
|
||||
## Ритм пересборки
|
||||
|
||||
День витрины пересобирается, пока может измениться его источник, —
|
||||
поэтому ритмов три, по числу источников:
|
||||
Дни для пересчета выбирают по тому, как меняется источник:
|
||||
|
||||
- **от событий и сессий** — день замерзает сразу; пересборка догоняет
|
||||
хвост несобранных дней заменой дневных партиций;
|
||||
- **от заказов** — день дышит, пока входит в последний слепок; пересборка
|
||||
заменяет партиции этого отрезка, дни за ним закрыты по построению;
|
||||
- **от карты идентичностей** — не замерзает никогда: склейка
|
||||
ретроспективна, покупка сегодня делает куку известной во всех её прошлых
|
||||
днях, а карта пересчитывается целиком.
|
||||
- События и сессии прожитого дня на стенде больше не меняются. Витрины
|
||||
по ним рассчитывают показатели от первого пропущенного дня до последнего
|
||||
прожитого и заменяют дневные партиции.
|
||||
- Заказы могут меняться в течение семи модельных дней. Последний слепок ODS
|
||||
задает диапазон дат для пересборки; пропущенные дни могут расширить его.
|
||||
- Новая связь в карте идентичностей позволяет определить пользователя
|
||||
по его прежним визитам: один `client_id` мог встречаться и до покупки,
|
||||
которая связала его с `user_id`.
|
||||
Поэтому `daily_traffic`, как и карта, пересчитывается целиком.
|
||||
|
||||
Поэтому `daily_traffic` наследует у своего источника и ритм, и механику:
|
||||
полный пересчёт всех дней каждый прогон в `ReplacingMergeTree(_load_ts)`,
|
||||
как у самой карты. Дневных партиций у неё нет: партиция — единица замены,
|
||||
а эта витрина заменяется только целиком. На стенде такая работа остается
|
||||
быстрой; витрина читает сессии и карту через их публичные договоры.
|
||||
Пересчитывать точечно только затронутые дни не из чего: для этого надо
|
||||
знать, какие связки новые, а полный пересчёт карты этот след затирает —
|
||||
восстановление следа оставлено лабе повышенной сложности (issue #149).
|
||||
`daily_traffic` каждый раз добавляет всю историю новой версией
|
||||
в `ReplacingMergeTree(_load_ts)`. Дневных партиций у нее нет, а текущие
|
||||
строки выбирает представление через `FINAL`.
|
||||
|
||||
У `purchase_vs_orders` два источника и одна непрерывная пересборка. Заказная
|
||||
сторона задает изменяемое окно последним принятым слепком ODS, событийная —
|
||||
последний прожитый день с `purchase`. Левая граница возвращает первую дыру и
|
||||
самый ранний день с `awaiting_order`: предварительная строка уже есть в
|
||||
витрине, поэтому поиск отсутствующего дня ее не заметит. Правая граница
|
||||
доходит до более позднего из двух хвостов. Так один прогон догоняет пакетный
|
||||
разгон длиннее K и не оставляет старое ожидание замороженным.
|
||||
У `purchase_vs_orders` два источника. Начало диапазона — самая ранняя
|
||||
из трех дат: первая дата оформления заказа в последнем слепке, первый
|
||||
пропущенный день и первый день с `awaiting_order` — покупкой, для которой
|
||||
заказ пока не поступил. День ожидания проверяется отдельно: строки за него
|
||||
уже есть в витрине, поэтому он не считается пропущенным.
|
||||
Конец диапазона — более поздняя из последней даты заказов в слепке и последнего
|
||||
прожитого дня с покупкой. Один запуск пересобирает весь отрезок,
|
||||
даже если с предыдущего запуска прошло больше семи модельных дней.
|
||||
|
||||
Для учебной сводки брака действует отдельное правило: дни выбираются
|
||||
по времени загрузки, а прежние итоги сохраняются после удаления
|
||||
источника по TTL. Оно разобрано в
|
||||
[задании `dq_errors_daily`](../labs/1-4-dq-errors-daily.md).
|
||||
|
||||
## Витрины стенда
|
||||
|
||||
Публичные представления перечисляют деловые колонки и координаты запуска
|
||||
`_load_id`, `_load_ts` явно. Новая колонка физической таблицы сама в договор не
|
||||
попадет.
|
||||
Представления `_v` явно перечисляют поля для отчетов, а также `_load_id`
|
||||
и `_load_ts` — идентификатор запуска и время записи. Новая колонка
|
||||
физической таблицы появится в представлении только после изменения его SQL.
|
||||
|
||||
### `dm.revenue_daily_v`
|
||||
|
||||
Зерно: день заказа × категория товара. Витрина читает только оплаченные
|
||||
заказы из `dds.order_v`, синхронно разворачивает три массива позиций и узнает
|
||||
категорию артикула через `dic.products`. Один заказ с несколькими позициями
|
||||
категории считается в `orders` один раз. `units` складывает количество единиц,
|
||||
`revenue` складывает цену позиции × количество. Скидка, доставка и итог заказа
|
||||
между категориями не делятся. `aov` равен `revenue / orders` в том же срезе и
|
||||
хранит два знака после запятой.
|
||||
Одна строка содержит итоги оплаченных заказов за день по категории товара.
|
||||
Запрос читает `dds.order_v` и разворачивает массивы артикулов, количеств
|
||||
и цен в строки позиций заказа. Артикул, количество и цена с одним индексом
|
||||
становятся одной строкой. Категория артикула берется из `dic.products`.
|
||||
|
||||
Первый запуск строит всю доступную историю. Следующие берут границы дней из
|
||||
последнего слепка ODS: только источник знает, какие даты еще может изменить.
|
||||
Пропущенный оплаченный день расширяет левую границу, чтобы частичный отказ
|
||||
не оставил дыру после следующего прогона. Агрегат при этом читает DDS; ODS
|
||||
отвечает только на вопрос об объеме.
|
||||
`orders` считает разные заказы: несколько позиций одной категории
|
||||
в заказе не увеличивают этот счетчик. `units` — сумма количества товаров,
|
||||
`revenue` — сумма произведений цены на количество. Скидка и доставка
|
||||
в нее не входят. `aov` равен `revenue / orders` для того же дня и категории
|
||||
с округлением до двух знаков после запятой.
|
||||
|
||||
`dm.revenue_daily_rep` нарезана по `report_date`; день собирается в
|
||||
таблице-доноре и заменяется через `REPLACE PARTITION`. Цель и донор шардируются
|
||||
по `cityHash64(product_category)`. Кроме дня, у витрины только категория, а
|
||||
шесть категорий стартового мира близки по объему. Более сложный составной ключ
|
||||
распределил бы эту маленькую таблицу без нового урока.
|
||||
Первый запуск рассчитывает выручку за всю доступную историю. Следующие
|
||||
берут диапазон дат оформления заказов из последнего слепка ODS.
|
||||
Если в 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`
|
||||
|
||||
Зерно — заказ. Покупки из `dds.event_v` сначала сворачиваются по
|
||||
`purchase_id[1]`: контракт источника обещает один номер и одну объявленную
|
||||
сумму, а отдельный счет событий оставляет класс `duplicate_event`
|
||||
наблюдаемым. Заказы приходят из `dds.order_v`. Обе стороны записаны
|
||||
подзапросами, затем соединены `FULL OUTER GLOBAL JOIN`: голая левая
|
||||
Distributed-таблица вернула бы каждый непарный заказ по разу с каждого шарда
|
||||
Одна строка сопоставляет события покупки с заказом магазина. События
|
||||
из `dds.event_v` группируются по номеру заказа `purchase_id[1]`:
|
||||
по формату источника у покупки один номер и одна заявленная сумма.
|
||||
Отдельный счет событий позволяет обнаружить повтор покупки
|
||||
и присвоить класс `duplicate_event`. Заказы берутся из `dds.order_v`.
|
||||
|
||||
Полное соединение `FULL OUTER GLOBAL JOIN` сохраняет и покупки без заказа,
|
||||
и заказы без покупки. Обе стороны записаны подзапросами: при прямом чтении
|
||||
левой Distributed-таблицы каждый непарный заказ повторился бы по числу шардов
|
||||
([исследование](../research/2026-08-22-distributed-join.md)).
|
||||
|
||||
Непарная сторона остается `NULL` благодаря `join_use_nulls = 1`. При
|
||||
`join_use_nulls = 0` ClickHouse заполнил бы ее нулем или пустой строкой и скрыл
|
||||
отсутствие источника. Клиентская `declared_revenue` и серверная `items_total`
|
||||
сравниваются как `Decimal(18, 2)`. Классы проверяются по порядку:
|
||||
Если для строки нет пары, поля отсутствующей стороны получают `NULL`
|
||||
благодаря `join_use_nulls = 1`. При `join_use_nulls = 0` они заполнились бы
|
||||
нулями или пустыми строками, которые сложнее отличить от данных источника.
|
||||
Клиентская `declared_revenue` и серверная `items_total` сравниваются
|
||||
как `Decimal(18, 2)`. Классы проверяются по порядку:
|
||||
`awaiting_order` для покупки без заказа, затем `cancelled`, `lost_event`,
|
||||
`duplicate_event`, `amount_delta`, `match`. Поэтому отмененный заказ без
|
||||
события остается отмененным, а не потерянным.
|
||||
|
||||
Витрина нарезана по `order_day`, цель и донор шардируются по
|
||||
`cityHash64(order_id)`. Диапазон собирается в доноре одной вставкой и
|
||||
заменяется по дням. Первичная сборка охватывает всю историю обоих источников;
|
||||
следующие прогоны идут по двустороннему ритму, описанному выше.
|
||||
Партиции витрины разделены по `order_day`. Основная и промежуточная таблицы
|
||||
распределяют строки по `cityHash64(order_id)`. Запрос рассчитывает весь
|
||||
выбранный диапазон и одной вставкой записывает строки в промежуточную
|
||||
таблицу. Затем ее дневные разделы заменяют разделы основной таблицы.
|
||||
Первичная сборка охватывает всю историю обоих источников;
|
||||
следующие прогоны выбирают дни по правилам обоих источников, описанным выше.
|
||||
|
||||
### `dm.daily_traffic_v`
|
||||
|
||||
В строке один день. `visitors` считает разные `client_id` из
|
||||
`dds.session_v`; `known_users` считает разные `user_id` после левого соединения
|
||||
с полной текущей `dds.identity_map_v`. Анонимная сессия остается в витрине.
|
||||
Несколько кук одного человека увеличивают `visitors`, но дают одного
|
||||
`known_user`, поэтому `known_users <= visitors`.
|
||||
Несколько кук, связанных с одним пользователем магазина, учитываются
|
||||
в `visitors` по отдельности, а в `known_users` дают одного пользователя.
|
||||
Поэтому `known_users <= visitors`.
|
||||
|
||||
Сессии и карта лежат по `client_id`, но публичные `_v` скрывают
|
||||
`Distributed`-таблицы от локальной подстановки. На ClickHouse 26.3
|
||||
`EXPLAIN PLAN` показал чтение обоих шардов на инициаторе: настройка
|
||||
`distributed_product_mode = 'local'` локального соединения не дала. Поэтому
|
||||
витрина использует явный `GLOBAL LEFT JOIN` через публичные договоры, а не
|
||||
подменяет их физическими таблицами. Это дороже по сети, зато граница слоев
|
||||
остается явной. Новая связь куки меняет прошлые дни, поэтому каждый запуск
|
||||
пишет полную историю новой версией в `ReplacingMergeTree(_load_ts)`, а
|
||||
публичное представление выбирает текущие строки через `FINAL`.
|
||||
Сессии и карта распределены по `client_id`. Однако на ClickHouse 26.3
|
||||
чтение через публичные `_v` с `distributed_product_mode = 'local'`
|
||||
не дало локального соединения. Поэтому в запросе явно указан
|
||||
`GLOBAL LEFT JOIN`. Он сохраняет сессии без известного пользователя;
|
||||
в `known_users` учитываются только ненулевые `user_id`.
|
||||
Обновление всей истории новой версией описано в разделе
|
||||
[«Ритм пересборки»](#ритм-пересборки).
|
||||
|
||||
## `dm.dq_summary`
|
||||
|
||||
Зерно публичного `dm.dq_summary_v` — «день данных × атомарная проверка».
|
||||
Одна строка `dm.dq_summary_v` — день данных и одна проверка.
|
||||
Строка отвечает, каково качество дня сейчас; историю запусков хранит Airflow.
|
||||
Партиция совпадает с днём данных и заменяется через `REPLACE PARTITION`.
|
||||
|
||||
Деловые колонки сводки:
|
||||
|
||||
- `data_date` — проверяемый день;
|
||||
- `check_name` — устойчивое имя инварианта;
|
||||
- `check_name` — имя проверяемого правила;
|
||||
- `reference_rows` — число деловых ключей на эталонной стороне;
|
||||
- `actual_rows` — число деловых ключей в проверяемом объекте;
|
||||
- `failed_rows` — число отсутствующих или различающихся ключей;
|
||||
- `status` — `pass` при нуле нарушений, иначе `fail`.
|
||||
|
||||
Служебные `_load_id` и `_load_ts` называют собравший запуск. Представление
|
||||
перечисляет колонки явно. Порогов и промежуточного состояния нет.
|
||||
В `_load_id` и `_load_ts` записаны идентификатор запуска и время расчета
|
||||
сводки. Даже одно расхождение дает `fail`.
|
||||
|
||||
## Звено DQ конвейера
|
||||
|
||||
Даг `dq_check` стоит в `etl_pipeline` после `dm_transform` и проверяет пять
|
||||
атомарных договоров:
|
||||
правил:
|
||||
|
||||
- `sessions_vs_reference`: ключ — `client_id` внутри дня, эталон —
|
||||
`uniqExact(VisitID)` из `ods.event_v`, объект — число строк
|
||||
@@ -192,43 +215,47 @@ Distributed-таблица вернула бы каждый непарный з
|
||||
сравниваются с прямыми агрегатами `dds.session_v` и
|
||||
`dds.identity_map_v`.
|
||||
|
||||
Область каждой проверки выводится из объединения сторон. Полностью потерянный
|
||||
день или ключ поэтому не исчезает из результата. Разные ошибки тоже не могут
|
||||
скомпенсировать друг друга общим итогом: `failed_rows` считается после
|
||||
сопоставления деловых ключей.
|
||||
Каждая проверка берет дни и ключи из обоих сравниваемых наборов данных.
|
||||
Поэтому потерянная строка остается видна на другой стороне сравнения.
|
||||
Расхождения считаются по ключам: завышение одного показателя не скроет
|
||||
занижение другого в общей сумме за день.
|
||||
|
||||
`classes_vs_inventory` — техническая растяжка пути от генератора до DM. Она
|
||||
подтверждает, что до витрины доехал записанный в паспорте мир, но не проверяет
|
||||
сам генератор: у настоящего магазина такого оракула нет.
|
||||
`classes_vs_inventory` сравнивает результат с паспортом мира. Это техническая
|
||||
проверка доставки и обработки эталонных данных стенда. Она не проверяет
|
||||
сам генератор; у настоящего магазина такого эталона нет.
|
||||
|
||||
Четыре SQL-сверки считают дневные итоги в ClickHouse и возвращают Airflow только
|
||||
сводку и малый срез диагностики. Техническая сверка с паспортом читает небольшой
|
||||
файл JSON в Python. Все пять сборок пишут в общий донор. После их успеха даг
|
||||
заменяет проверенные дневные партиции. Затем именованные задачи читают
|
||||
опубликованную сводку и падают при `failed_rows > 0`. Расхождение успевает
|
||||
сохраниться до красного завершения, а ошибка сборки не затирает прежний
|
||||
результат. Красная задача печатает не больше двадцати нарушенных ключей за
|
||||
проверку со значениями сторон. Для полного списка итоговую агрегацию SQL-сверки
|
||||
сводку и несколько примеров расхождений. Техническая сверка с паспортом читает небольшой
|
||||
файл JSON в Python. Результаты всех пяти проверок записываются в общую
|
||||
промежуточную таблицу. Когда расчеты завершились, ее дневные разделы
|
||||
заменяют разделы `dm.dq_summary_rep`. Затем отдельные задачи читают
|
||||
сохраненную сводку и завершаются ошибкой при `failed_rows > 0`.
|
||||
Так расхождение сохраняется в таблице до отказа задачи. Если сам расчет
|
||||
не выполнен, прежняя сводка остается на месте. Задача с расхождениями
|
||||
выводит в журнал не больше двадцати ключей со значениями обеих сторон. Для полного списка итоговую агрегацию SQL-сверки
|
||||
заменяют на `SELECT * FROM comparison WHERE failed ORDER BY data_date,
|
||||
business_key`.
|
||||
|
||||
Если после починки день исчез с обеих сторон, его больше нет и в новом доноре.
|
||||
Такой день удаляется из сводки отдельно: старая красная строка не должна
|
||||
превращаться в историю попыток.
|
||||
Если день исчез из обоих сравниваемых наборов, новый расчет не создаст
|
||||
для него строк. Даг отдельно удаляет прежний результат за этот день
|
||||
из сводки.
|
||||
|
||||
## Даг самопроверки
|
||||
|
||||
Домашние задания проверяет `homework_check` — отдельный даг без расписания,
|
||||
который запускают вручную. В нём есть задача для каждого из
|
||||
[пяти заданий по витринам](../labs/README.md).
|
||||
Задача красная, пока витрины нет или счёт не сходится, и зелёная, когда
|
||||
построено верно; сообщение об ошибке говорит, что не так и куда смотреть. Проверки — инварианты против источников витрины,
|
||||
золотые константы не прибиваются. В `dq_summary` даг не пишет: он инструмент
|
||||
лабы, а не история качества конвейера.
|
||||
`homework_check` запускается вручную и содержит задачу для каждого из
|
||||
[пяти заданий по витринам](../labs/README.md). Если витрины нет или значения
|
||||
расходятся с источником, задача завершается ошибкой. Журнал показывает
|
||||
причину и примеры несовпадений. Успех означает, что проверяемые значения
|
||||
совпали; остальные требования задания вы проверяете сами.
|
||||
|
||||
Приём проверен на соседнем учебном стенде Airflow + Greenplum:
|
||||
валидационный даг с группами проверок по слоям давал обратную связь по каждому
|
||||
шагу задания, не трогая рабочий конвейер.
|
||||
Ожидаемые значения рассчитываются по исходным данным текущего стенда.
|
||||
Для четырех заданий это события или сессии DDS, для сводки брака —
|
||||
таблицы ошибок ODS. Сравнение по деловому ключу учитывает отсутствующие
|
||||
строки с обеих сторон. Результаты не пишутся в `dq_summary`:
|
||||
самопроверка учебных витрин отделена от качества готового конвейера.
|
||||
|
||||
Готовые SQL-проверки открывайте после собственного решения: они содержат
|
||||
расчет существенной части задания.
|
||||
|
||||
## Отклонённые варианты
|
||||
|
||||
|
||||
+107
-82
@@ -1,28 +1,23 @@
|
||||
# Дневной конвейер трансформаций
|
||||
|
||||
Как принятые источники превращаются в модель DDS и витрины DM: кто запускает
|
||||
трансформации, когда и в каком объёме. Правила самих сущностей — зерно,
|
||||
движки, что именно пересобирается — живут в доках слоя
|
||||
([карта модели DDS](dds/README.md)); здесь — только конвейер.
|
||||
|
||||
Дневная сборка вперёд появляется на этапе 4 и остаётся основным режимом
|
||||
конвейера. На этапе 5 к ней добавляется партиционная переобработка: запуск
|
||||
произвольного прошлого дня по команде.
|
||||
Даг `etl_pipeline` собирает модель DDS, обновляет витрины DM и проверяет
|
||||
результат. Здесь описаны запуск, выбор дней и запись результата в таблицы.
|
||||
Состав моделей — в [справочниках DDS](dds/README.md) и [DM](dm.md).
|
||||
|
||||
## Форма
|
||||
|
||||
Дневной конвейер на стенде один — даг `etl_pipeline`. Своей работы
|
||||
у него нет: он дёргает слойные даги последовательно и ждёт каждый —
|
||||
`dds_transform`, затем `dm_transform` и `dq_check`. Состав проверок описан в
|
||||
[доке слоя DM](dm.md).
|
||||
`etl_pipeline` последовательно запускает `dds_transform`, `dm_transform`
|
||||
и `dq_check`. Каждый следующий даг начинается после успеха предыдущего.
|
||||
Для этого `TriggerDagRunOperator` ждет завершения запущенного им дага.
|
||||
Если тот завершился ошибкой, задача ожидания в конвейере тоже завершится
|
||||
ошибкой.
|
||||
Поведение проверено через 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_live_day` дёргает конвейер с ожиданием — последним
|
||||
шагом, после установки позиции. Порядок несёт смысл: день сыгран — факт,
|
||||
упавшая трансформация его не отменяет, поэтому позиция ставится раньше; но
|
||||
зеленеть, не узнав, разложился ли день, шаговый даг не должен ([ADR
|
||||
0003](../adr/0003-dag-run-state.md)). Такой отказ чинится перезапуском
|
||||
`etl_pipeline` рукой — перезапуску ничего не нужно помнить, см. «Объём
|
||||
прогона».
|
||||
- **Разгон пачкой.** `world_next_day` конвейер не дёргает: даги модельного
|
||||
времени управляют генератором и только им. После разгона менти запускает
|
||||
конвейер сам — и видит, как слои догоняют уехавший вперёд мир.
|
||||
- **Рождение мира.** `world_initialize` дёргает конвейер последним шагом:
|
||||
`make up` отдаёт стенд с собранными слоями, а не пустые витрины. Уроки
|
||||
ручного запуска начинаются со следующих дней.
|
||||
- `world_live_day` запускает конвейер после успешной генерации, приема
|
||||
заказов и обновления `world_position`. Если трансформация упала, день
|
||||
уже прожит и позиция не откатывается. После исправления причины
|
||||
запустите `etl_pipeline` вручную.
|
||||
- `world_next_day` отправляет дни пачкой и ждет приема заказов, но конвейер
|
||||
не запускает. После него запустите `etl_pipeline` сами.
|
||||
- `world_initialize` запускает конвейер последним шагом: при успешном
|
||||
первом `make up` начальные данные уже собраны в DDS и DM.
|
||||
|
||||
## Объём прогона
|
||||
|
||||
Параметров у конвейера нет: объём он выводит сам из `world_position` и
|
||||
состояния слоёв. Сессии догоняют хвост — от первого несобранного дня по
|
||||
последний прожитый; заказы пересобирают дышащее окно и тот же хвост; карта
|
||||
идентичностей пересчитывается целиком; правила витрин — у
|
||||
[слоя DM](dm.md).
|
||||
Поэтому один и тот же запуск одинаково верен из триггера, с кнопки менти и
|
||||
после падения, а разгону на семь дней хватает одного прогона.
|
||||
Объем прогона — дни, которые обрабатывает один запуск. Даты передавать
|
||||
не нужно: даги определяют их по состоянию таблиц и `world_position`.
|
||||
Например, после генерации семи дней без ETL один запуск рассчитает сессии
|
||||
за все семь дней.
|
||||
|
||||
Окно заказов задаёт последний слепок, наблюдаемый в ODS. У каждого заказа
|
||||
`snapshot_date` равен последнему слепку, в котором источник его показал.
|
||||
Трансформация выбирает строки с максимальной `snapshot_date` и берёт среди них
|
||||
минимальный и максимальный `order_date`. Это границы дней, состояние которых
|
||||
источник ещё может изменить
|
||||
([ADR 0014](../adr/0014-order-version-snapshot-date.md)). Кроме живого окна,
|
||||
трансформация закрывает пропуски: находит самый ранний день, представленный в
|
||||
ODS и ещё не собранный в DDS, затем при необходимости расширяет до него левую
|
||||
границу.
|
||||
Для расчета сессий выбираются дни от первого, за который в DDS еще нет
|
||||
сессий, до последнего прожитого.
|
||||
`world_position` хранит номер следующего дня для генерации, поэтому
|
||||
последний прожитый день равен `min(EventDate) + world_position - 1`.
|
||||
Начало календаря берется из ODS; события ранних дней там не удаляются по TTL.
|
||||
Один `max(EventDate)` не годится: события за день есть и во время его
|
||||
генерации, когда последние визиты еще не закончились.
|
||||
Точный запрос — [`session_scope.sql`](../../sql/dds/session_scope.sql).
|
||||
|
||||
Позиция мира заказам не отвечает: она говорит, какой день прожит, а не какой
|
||||
доехал. Прогон дня D везёт слепок дня D − 1, да и ставится позиция раньше
|
||||
трансформации, так что после неудачного приёма обгоняет хранилище. Слой обязан
|
||||
считать объём по тому, что у него есть, а не по тому, что мир о себе заявляет.
|
||||
Для заказов диапазон задает последний принятый слепок ODS. Среди строк
|
||||
с максимальной `snapshot_date` выбираются самая ранняя и самая поздняя
|
||||
даты оформления заказа. Состояние заказов за эти дни еще может измениться.
|
||||
Если в ODS есть заказы за более ранний день, а в DDS за него нет строк,
|
||||
начало диапазона сдвигается до этого дня. Запрос заново рассчитывает
|
||||
заказы за все дни выбранного отрезка.
|
||||
|
||||
Для сессий границу задаёт `world_position`: она отличает полностью прожитый
|
||||
день от ещё идущего. Сами события этой границы не показывают: и посреди дня D,
|
||||
и сразу после его конца их позднейший день равен D. Если собрать сессии посреди
|
||||
дня, визит, начатый в 23:50, останется обрезанным: завершённый день слой
|
||||
повторно не собирает. Поэтому последний прожитый день равен первому дню
|
||||
событий плюс позиция минус один
|
||||
([`sql/dds/session_scope.sql`](../../sql/dds/session_scope.sql)).
|
||||
Оговорка про «доехал» остаётся в силе и здесь: за хвост потока отвечает не
|
||||
позиция, а «Готовность источника».
|
||||
Позиция мира для выбора дней заказов не подходит: с событиями дня D
|
||||
приходит слепок за D−1. Объем рассчитывается по принятым строкам ODS;
|
||||
их наличие еще не доказывает полноту слепка.
|
||||
|
||||
Карта идентичностей пересчитывается целиком. Витрины выбирают дни
|
||||
по своим источникам — см. [ритм пересборки DM](dm.md#ритм-пересборки).
|
||||
Если за день сохранилась хотя бы одна строка, запрос не считает его
|
||||
пропущенным. Полноту данных проверяют отдельно.
|
||||
|
||||
## Готовность источника
|
||||
|
||||
Первый шаг `dds_transform` проходит два барьера за общий срок в 300 секунд.
|
||||
Сначала клиент `confluent-kafka` сверяет подтвержденные смещения группы
|
||||
`clickstream_hits` с верхними смещениями обоих разделов `hits`. Группа, оба
|
||||
раздела и все смещения должны существовать. Суммарное отставание должно стать
|
||||
нулевым. Каждый опрос пишет в журнал оба смещения и отставание по разделу.
|
||||
Перед сборкой DDS задача `wait_for_source` последовательно ждет двух
|
||||
условий:
|
||||
|
||||
Только после этого шаг проверяет обе ноды ClickHouse через
|
||||
`clusterAllReplicas`: у `ods.event_dist` в `system.distribution_queue` не
|
||||
должно остаться файлов. Каждый опрос пишет в журнал ноды, число файлов и
|
||||
`last_exception` очереди. Оба барьера опрашиваются раз в три секунды и делят
|
||||
один крайний срок. После Kafka срок не отсчитывается заново.
|
||||
1. Группа `clickstream_hits` дочитала оба раздела топика `hits`.
|
||||
Подтвержденные смещения сравниваются с верхними смещениями Kafka.
|
||||
Группа и смещения должны существовать, суммарное отставание — равняться нулю.
|
||||
2. В `system.distribution_queue` у `ods.event_dist` не осталось файлов
|
||||
доставки на шарды. Запрос через `clusterAllReplicas` читает обе ноды.
|
||||
|
||||
Зелёный приём заказов о событиях не говорит ничего — топики разные и
|
||||
потребители разные. Одного нулевого отставания Kafka тоже мало: чтец мог
|
||||
подтвердить сообщение раньше, чем Distributed-таблица доставила файл на шард.
|
||||
Два барьера закрывают обе части пути до того, как сессии выберут свой объем.
|
||||
Опросы идут с паузой в три секунды; на обе стадии задан общий срок
|
||||
300 секунд. Журнал показывает смещения и отставание Kafka, затем число
|
||||
файлов и `last_exception` очереди на каждой ноде. Если готовность не
|
||||
подтверждена за срок ожидания, задача завершается ошибкой.
|
||||
|
||||
Одного нулевого отставания Kafka мало: сообщение может быть подтверждено,
|
||||
пока Distributed-таблица еще доставляет его на шард. Успех `orders_ingest`
|
||||
тоже не заменяет эти проверки: у заказов другой топик и потребитель.
|
||||
|
||||
Эти условия проверяют доставку на момент опроса. Они не останавливают
|
||||
поступление новых событий и не сверяют содержимое таблиц. Поэтому
|
||||
`world_position` отдельно ограничивает сборку сессий прожитыми днями,
|
||||
а качество результата проверяет `dq_check`.
|
||||
|
||||
## Идемпотентность
|
||||
|
||||
Партиция не заменяется простой вставкой. День собирается в таблицу-двойник —
|
||||
вставка через Distributed раскладывает строки по шардам, — затем
|
||||
`REPLACE PARTITION` на кластере атомарно подменяет кусок каждого шарда. Эта
|
||||
механика — часть самих трансформаций и строится этапом 4: без неё дневной
|
||||
прогон не повторяем. Сущности, пересчитываемые целиком, идемпотентны и без
|
||||
замены: полная перезапись в `ReplacingMergeTree` — так живут карта
|
||||
идентичностей и витрина `daily_traffic` ([слой DM](dm.md)).
|
||||
Идемпотентность означает, что повторная обработка тех же исходных данных
|
||||
не добавляет копии деловых строк. Служебные `_load_id` и `_load_ts`
|
||||
при новой сборке меняются.
|
||||
|
||||
Результат дневного расчета сначала записывается в промежуточную таблицу.
|
||||
Она нужна, чтобы читатели продолжали видеть прежние данные, пока новые
|
||||
еще рассчитываются. Например, для сессий это `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`. Старые версии
|
||||
могут физически оставаться до фонового слияния; повторяемость относится
|
||||
к результату чтения, а не к числу строк на диске.
|
||||
|
||||
## Чего конвейер о себе не знает
|
||||
|
||||
|
||||
@@ -30,115 +30,96 @@ keeper, Kafka, каркас сервисов. Этап 2 собран: в `sql/d
|
||||
|
||||
## Имена объектов
|
||||
|
||||
Имя объекта заканчивается тем, что это за объект:
|
||||
Суффикс показывает назначение объекта:
|
||||
|
||||
| Суффикс | Что это |
|
||||
|---|---|
|
||||
| `_rep` | локальная таблица шарда, движок семейства `Replicated*` |
|
||||
| `_dist` | `Distributed` поверх одноимённой локальной |
|
||||
| `_kafka` | таблица на движке `Kafka` |
|
||||
| `_file` | таблица на движке `File` — чтец файла |
|
||||
| `_mv` | материализованное представление |
|
||||
| `_v` | обычное представление, публичная поверхность чтения |
|
||||
| `_rep` | локальная таблица: хранит данные своего шарда, движок семейства `Replicated*` |
|
||||
| `_dist` | таблица `Distributed`: читает данные шардов и распределяет запись между ними |
|
||||
| `_kafka` | таблица на движке `Kafka`: читает сообщения топика |
|
||||
| `_file` | таблица на движке `File`: читает файл |
|
||||
| `_mv` | материализованное представление: обрабатывает вставляемые данные |
|
||||
| `_v` | обычное представление: выполняет сохраненный запрос при чтении |
|
||||
|
||||
Суффикс носит каждый физический объект. Голого имени у таблицы не существует:
|
||||
запрос к `stg.hits_raw` даёт громкую ошибку «нет такой таблицы» — а под голым
|
||||
именем в документах и разговоре понимается сущность, у которой этих объектов
|
||||
несколько. Единственное исключение — словари: у них воплощение одно, шардировать
|
||||
нечего, и суффикс ничего не различал бы. В зоне `dic` обе формы стоят рядом и
|
||||
правило видно целиком: `dic.products_file` — чтец файла, `dic.products` — сам
|
||||
словарь.
|
||||
Для запросов к моделям DDS и витринам DM используйте `_v`. Представления
|
||||
явно перечисляют колонки, поэтому добавление поля в физическую таблицу
|
||||
не меняет набор полей для читателя. У `dds.event_v` нет отдельной таблицы
|
||||
DDS: представление читает события ODS.
|
||||
|
||||
Распространённая конвенция, где голое имя означает локальную таблицу, а
|
||||
распределённая получает суффикс `_all`, ошибается иначе: забытый суффикс тихо
|
||||
возвращает данные одного шарда. Правило спеки «проверки и контрольные суммы —
|
||||
только по `Distributed`» такую тишину переживает плохо.
|
||||
В документации имя без суффикса, например `stg.hits_raw`, обозначает
|
||||
сущность с несколькими объектами. Таблицы с таким именем нет: в запросе
|
||||
нужно указать суффикс. Исключение — словари ClickHouse: `dic.products`
|
||||
обращается к каталогу товаров, а `dic.products_file` читает его файл.
|
||||
|
||||
Второе свойство — в списке по алфавиту объекты группируются по сущности, а не по
|
||||
технологии: все четыре объекта топика `hits` стоят рядом, потому что различаются
|
||||
хвостом, а не началом имени. Речь про дерево в клиенте и про `ORDER BY name`:
|
||||
порядок выдачи `SHOW TABLES` документация не оговаривает.
|
||||
В STG имя берется из топика: `hits_raw`, `orders_raw`. В типизированных
|
||||
слоях оно называет сущность в единственном числе: `event`, `order`,
|
||||
`session`. Объекты одной сущности различаются суффиксом и стоят рядом
|
||||
при сортировке имен по алфавиту.
|
||||
|
||||
Начало имени — сущность, и берётся она в разных слоях из разных мест. В STG имя
|
||||
приходит от транспорта: слой хранит то, что доехало по топику, и зовётся именем
|
||||
топика — `hits_raw` от `hits`, `orders_raw` от `orders`. В типизированных слоях
|
||||
имя приходит от предметной области и стоит в единственном числе: `event`,
|
||||
`order`, `session`. Граница между «как привезли» и «что это такое»
|
||||
проходит по STG, и имена её показывают.
|
||||
|
||||
В DDS публичный договор сущности заканчивается на `_v`. Потребительские запросы
|
||||
и витрины называют только представление; `_rep` и `_dist` служат загрузке,
|
||||
диагностике и обслуживанию. Представление перечисляет колонки явно, даже если
|
||||
его запрос — простая проекция над `_dist`: договор не выдаёт устройство
|
||||
кластера и случайно добавленные в физическую таблицу колонки. У события
|
||||
физической копии в DDS нет, но правило то же: `dds.event_v` — его публичный
|
||||
договор.
|
||||
|
||||
Право доступа здесь намеренно шире договора. Роль `bi_reader` может читать
|
||||
любые объекты ODS, DDS и DM, но физические объекты от этого не становятся
|
||||
публичной поверхностью. Конвенцию держат явный список колонок, документация и
|
||||
ревью, а не запрет на физические таблицы.
|
||||
|
||||
Широкое право принято в [ADR 0007](../adr/0007-clickhouse-access.md): узкая
|
||||
граница поверх `Distributed` потребовала бы отдельной механики. Механизм
|
||||
ClickHouse и отклонённые варианты записаны в
|
||||
`_rep` и `_dist` нужны для загрузки, диагностики и обслуживания. Роль
|
||||
`bi_reader` может читать их в ODS, DDS и DM, но для обычного анализа
|
||||
принято использовать `_v`. Это соглашение о чтении, а не ограничение прав;
|
||||
причина широкого доступа описана в
|
||||
[ADR 0007](../adr/0007-clickhouse-access.md) и
|
||||
[исследовании](../research/2026-08-25-clickhouse-distributed-view-security.md).
|
||||
|
||||
## Раскладка по шардам
|
||||
|
||||
Пишем только в `_dist`. Локальные таблицы остаются для чтения и обслуживания —
|
||||
операций с партициями, ручной переобработки. Правило не про удобство: при записи
|
||||
через распределённую таблицу раскладку определяет ключ шардирования, то есть
|
||||
свойство данных, а при записи в локальную — то, какая нода случайно выполняла
|
||||
код. Отсюда урок стенда: какая нода читала топик, меняется между прогонами
|
||||
(видно в колонке `consumer_host`), а куда легли данные — нет.
|
||||
Шард хранит часть строк. При записи через `_dist` место строки выбирает
|
||||
ключ шардирования. При прямой записи в `_rep` она остается на той ноде,
|
||||
где выполнена вставка. Поэтому данные записывают через `_dist`,
|
||||
а операции с партициями выполняют на локальных `_rep`.
|
||||
|
||||
Ключи шардирования: `cityHash64(ClientID)` у событий, `cityHash64(order_id)` у
|
||||
заказов, `cityHash64` сырой строки у STG и у таблицы ошибок. У первых двух хеш
|
||||
выбран против перекоса: структурированный числовой идентификатор распределяется
|
||||
по остатку от деления неравномерно. У сырого слоя выбора нет — строку иначе не
|
||||
разложишь; там хеш даёт другое свойство, одинаковые сообщения ложатся на один
|
||||
шард.
|
||||
У событий ключ — `cityHash64(ClientID)`, у заказов —
|
||||
`cityHash64(order_id)`, у STG и таблиц ошибок — хеш сырой строки.
|
||||
Хеш распределяет числовые идентификаторы по шардам и направляет одинаковые
|
||||
сообщения на один шард. У сырой строки и разобранного события разные
|
||||
ключи: они могут оказаться на разных шардах. Счетчики STG и ODS сравнивают
|
||||
по всему кластеру через `_dist`, а не отдельно по каждой ноде.
|
||||
|
||||
Ключи у слоёв разные, и это имеет наблюдаемое следствие: сырая строка и
|
||||
разобранное из неё событие почти всегда оказываются на разных шардах.
|
||||
Пошардовые счётчики STG и ODS поэтому не сходятся и сходиться не должны —
|
||||
сверять слои можно только через `_dist`.
|
||||
При `INSERT INTO цель_dist SELECT … FROM источник_dist` важна настройка
|
||||
`parallel_distributed_insert_select`. При значении `2` и одном кластере
|
||||
вставка может выполняться локально на шардах, сохраняя раскладку источника
|
||||
вместо ключа цели. Если ключи различаются, укажите
|
||||
`parallel_distributed_insert_select = 0`: нода, принявшая запрос,
|
||||
соберет результат и распределит его по ключу цели. Так сделано в
|
||||
[`sql/ods/order_parse.sql`](../../sql/ods/order_parse.sql) и при сборке
|
||||
выручки. Неверную раскладку проверяют по `hostName()` и ожидаемому шарду
|
||||
строки; общий счет по `_dist` ее не показывает.
|
||||
|
||||
Разные ключи у слоёв ставят капкан на вставке `INSERT INTO цель_dist SELECT …
|
||||
FROM источник_dist`. Заводское значение `parallel_distributed_insert_select` —
|
||||
2: если обе таблицы на одном кластере, вставка исполняется локально на каждом
|
||||
шарде, минуя ключ шардирования цели. Строки наследуют раскладку источника, и
|
||||
ошибки при этом нет — расхождение видно только пересчётом ключей по
|
||||
`hostName()`. Так половина заказов и легла на оба шарда, где
|
||||
`ReplacingMergeTree` через границу шарда дубли не схлопывает (замер — «Что
|
||||
проверено», #137). Лекарство — явное `parallel_distributed_insert_select = 0`
|
||||
в `SETTINGS` такой вставки: раскладку делает инициатор, по ключу цели
|
||||
(`sql/ods/order_parse.sql`). Капкан ждёт и сборки DDS: они тоже будут читать
|
||||
`_dist` и писать в `_dist` с другим ключом.
|
||||
События, `dds.session_dist` и `dds.identity_map_dist` распределены
|
||||
по `cityHash64(client_id)` (в ODS поле называется `ClientID`). Данные
|
||||
одного посетителя лежат вместе — это ко-локация. Заказы DDS и их сверка
|
||||
сохраняют раскладку по `cityHash64(order_id)`. Обычное соединение допускается
|
||||
по ключу ко-локации; для остальных соединений применяется `GLOBAL`.
|
||||
Само совпадение ключей еще не гарантирует локального выполнения запроса
|
||||
через представления — пример есть в [описании `daily_traffic`](dm.md#dmdaily_traffic_v).
|
||||
|
||||
Ключи ко-локации названы заранее, потому что на них стоит политика соединений из
|
||||
раздела 6 спеки: обычное соединение разрешено только по ключу ко-локации, всё
|
||||
прочее — через `GLOBAL`. Значит `dds.session_dist` и `dds.identity_map_dist`
|
||||
шардируются по `cityHash64(client_id)`. Объекты DDS с зерном заказа сохраняют
|
||||
ко-локацию по `cityHash64(order_id)`; ключи остальных частей модели и витрин
|
||||
появляются вместе с ними.
|
||||
Для дневной пересборки DDS и DM результат сначала записывают
|
||||
в промежуточную таблицу. Основная таблица в это время остается доступна
|
||||
для чтения с прежними данными. Например, заказы записываются через
|
||||
`dds.order_stage_dist` в `dds.order_stage_rep`, затем ее дневные разделы
|
||||
заменяют разделы основной `dds.order_rep`.
|
||||
|
||||
Замена партиций не используется для `ods.order`: версии прошлых дней
|
||||
доливаются, а прямое чтение Kafka не задаёт границы полного слепка ([ADR
|
||||
0010](../adr/0010-order-versions-in-ods.md)). Если партиционная пересборка
|
||||
понадобится будущим объектам DDS или DM, партиция-донор должна быть заранее
|
||||
разложена по шардам по тому же ключу. Форму донора следует решать вместе с таким
|
||||
объектом, а не переносить на ODS заранее.
|
||||
|
||||
Первым такой донор понадобился заказу DDS, и правило на нём видно целиком:
|
||||
`dds.order_stage_rep` объявлен через `AS dds.order_rep` и шардируется тем же
|
||||
`cityHash64(order_id)`. Совпадение структуры требует сама операция, а совпадение
|
||||
ключа — устройство замены: она идёт по локальным таблицам, шард за шардом, и
|
||||
строка, попавшая в доноре не на свой шард, при подмене пропала бы с одного шарда
|
||||
и задвоилась на другом. Доводы целиком — в
|
||||
У промежуточной и основной таблиц должны совпадать структура, ключи
|
||||
партиции и сортировки, первичный ключ и политика хранения. Поэтому
|
||||
`dds.order_stage_rep` объявлена через `AS dds.order_rep` с совпадающими
|
||||
ключами. Требования сверены через Context7 с
|
||||
[документацией ClickHouse](https://clickhouse.com/docs/sql-reference/statements/alter/partition#replace-partition)
|
||||
для схемы на ClickHouse 26.3. DDL показан в
|
||||
[`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)).
|
||||
|
||||
## Служебные колонки
|
||||
|
||||
Собственные колонки не повторяют имён виртуальных. Виртуальные даёт движок:
|
||||
|
||||
+152
-154
@@ -149,15 +149,14 @@
|
||||
<script>
|
||||
const MAP = {
|
||||
"title": "Карта слоев хранилища",
|
||||
"lead": "Проследите, как один смысл меняет имя и поверхность между слоями.",
|
||||
"lead": "Проследите путь событий и заказов от Kafka до отчетов.",
|
||||
"chapter_url": "https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/handbook/storage-layers.md#user-content-хранилище-и-его-слои",
|
||||
"intro": [
|
||||
"Эта карта показывает путь событий и заказов от Kafka до витрин DM.",
|
||||
"Щелкните по узлу, чтобы увидеть его назначение, устройство и ключи.",
|
||||
"Щелкните по стрелке, чтобы узнать, кто и каким запросом переносит данные.",
|
||||
"Когда вы выбираете узел, карта подсвечивает все пути к нему и от него.",
|
||||
"Переключатель потока приглушает другую часть карты, но не меняет раскладку.",
|
||||
"Сначала прочитайте главу «Хранилище и его слои», затем вернитесь к карте и рассмотрите объекты."
|
||||
"Выберите объект, чтобы узнать, что он хранит, как его читать и откуда в него поступают данные.",
|
||||
"Выберите стрелку, чтобы посмотреть способ обработки и открыть SQL-запрос.",
|
||||
"Для выбранного объекта подсвечиваются все пути, по которым данные поступают в него и используются дальше.",
|
||||
"Кнопки «События» и «Заказы» выделяют нужный поток. Остальные объекты остаются на месте, но становятся бледнее.",
|
||||
"Если вы впервые знакомитесь со слоями хранилища, начните с главы по ссылке ниже."
|
||||
],
|
||||
"streams": {
|
||||
"all": "Все",
|
||||
@@ -166,39 +165,39 @@
|
||||
},
|
||||
"dictionary": {
|
||||
"title": "Словарь",
|
||||
"caption": "сбоку цепочки: справочники"
|
||||
"caption": "справочные данные"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"id": "kafka",
|
||||
"title": "Kafka",
|
||||
"caption": "откуда приходит"
|
||||
"caption": "сообщения источников"
|
||||
},
|
||||
{
|
||||
"id": "stg",
|
||||
"title": "STG",
|
||||
"caption": "как поступило"
|
||||
"caption": "исходные сообщения"
|
||||
},
|
||||
{
|
||||
"id": "ods",
|
||||
"title": "ODS",
|
||||
"caption": "язык источника"
|
||||
"caption": "данные разобраны по полям"
|
||||
},
|
||||
{
|
||||
"id": "dds",
|
||||
"title": "DDS",
|
||||
"caption": "язык бизнеса"
|
||||
"caption": "события, заказы и сессии"
|
||||
},
|
||||
{
|
||||
"id": "dm",
|
||||
"title": "DM",
|
||||
"caption": "данные для дашбордов"
|
||||
"caption": "витрины для отчетов"
|
||||
}
|
||||
],
|
||||
"mechanisms": [
|
||||
{
|
||||
"id": "materialized_view",
|
||||
"label": "матвью",
|
||||
"label": "материализованное представление",
|
||||
"line": "solid",
|
||||
"rhythm": "непрерывно, по мере прихода сообщений"
|
||||
},
|
||||
@@ -206,13 +205,13 @@
|
||||
"id": "airflow_dag",
|
||||
"label": "даг Airflow",
|
||||
"line": "dashed",
|
||||
"rhythm": "пачкой: раз в модельные сутки (`orders_ingest`) или на каждом прогоне конвейера"
|
||||
"rhythm": "пакетно: раз в модельные сутки (`orders_ingest`) или при каждом запуске конвейера"
|
||||
},
|
||||
{
|
||||
"id": "view",
|
||||
"label": "представление",
|
||||
"line": "dotted",
|
||||
"rhythm": "без копии, данные читаются на месте"
|
||||
"rhythm": "при запросе к представлению, без копирования данных"
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
@@ -225,9 +224,9 @@
|
||||
"events"
|
||||
],
|
||||
"what": "Топик Kafka с событиями кликстрима. Каждое сообщение содержит одно событие.",
|
||||
"read": "Топик напрямую не читают: таблица `stg.hits_raw_kafka` забирает сообщения из топика. Для данных событий используйте `stg.hits_raw_dist`.",
|
||||
"read": "Для просмотра загруженных сообщений используйте `stg.hits_raw_dist`. Таблица `stg.hits_raw_kafka` служит для приема из топика.",
|
||||
"fill": [
|
||||
"Источник событий пишет сообщения в топик в темпе выбранного режима мира."
|
||||
"Генератор отправляет события без пауз в пакетном режиме или постепенно в живом режиме."
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
@@ -248,10 +247,10 @@
|
||||
"streams": [
|
||||
"orders"
|
||||
],
|
||||
"what": "Топик Kafka со слепками заказов магазина. После модельного дня приходит снимок изменяемого окна.",
|
||||
"read": "Топик напрямую не читают: таблица `stg.orders_raw_kafka` забирает сообщения из топика. Для заказов используйте `stg.orders_raw_dist`.",
|
||||
"what": "Топик Kafka с дневными выгрузками заказов — слепками. Каждый слепок содержит состояние заказов за последние семь модельных дней.",
|
||||
"read": "Для просмотра загруженных сообщений используйте `stg.orders_raw_dist`. Таблица `stg.orders_raw_kafka` служит для приема из топика.",
|
||||
"fill": [
|
||||
"Источник заказов публикует слепок пачкой после каждого модельного дня."
|
||||
"После генерации событий дня D источник отправляет слепок заказов за предыдущий день D−1."
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
@@ -272,17 +271,17 @@
|
||||
"streams": [
|
||||
"events"
|
||||
],
|
||||
"what": "Сырая строка события и метаданные доставки. STG сохраняет сообщение без разбора.",
|
||||
"what": "Исходное сообщение события и сведения о его доставке из Kafka. Содержимое сохраняется целой строкой, без разбора на поля.",
|
||||
"read": "Читайте `_dist`: у STG нет представления `_v`, а распределенная таблица показывает оба шарда.",
|
||||
"parts": [
|
||||
"`stg.hits_raw_kafka` — чтец топика `hits`.",
|
||||
"`stg.hits_raw_kafka` — таблица для чтения сообщений топика `hits`.",
|
||||
"`stg.hits_raw_rep` — реплицируемая таблица на шарде.",
|
||||
"`stg.hits_raw_dist` — распределенная поверхность записи и чтения.",
|
||||
"`stg.hits_raw_mv` — матвью приема сообщений из чтеца."
|
||||
"`stg.hits_raw_dist` — распределенная таблица для записи и чтения.",
|
||||
"`stg.hits_raw_mv` — материализованное представление, которое записывает сообщения из Kafka в STG."
|
||||
],
|
||||
"fill": [
|
||||
"`stg.hits_raw_mv` пишет непрерывно, по мере прихода сообщений.",
|
||||
"Сырые строки живут трое суток реального времени плюс хвост до суток."
|
||||
"Строки хранятся не менее трех суток от загрузки. Удаление целыми частями таблицы добавляет до суток к этому сроку; оно выполняется при очередном слиянии по TTL."
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
@@ -294,7 +293,7 @@
|
||||
"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"
|
||||
}
|
||||
]
|
||||
@@ -307,16 +306,16 @@
|
||||
"streams": [
|
||||
"orders"
|
||||
],
|
||||
"what": "Сырые строки слепка заказов, метаданные доставки и идентификатор пачки `_load_id`.",
|
||||
"what": "Исходные строки выгрузки заказов, сведения о доставке и идентификатор загруженной партии `_load_id`.",
|
||||
"read": "Читайте `_dist`: у STG нет представления `_v`, а распределенная таблица показывает оба шарда.",
|
||||
"parts": [
|
||||
"`stg.orders_raw_kafka` — чтец топика `orders` на первой ноде.",
|
||||
"`stg.orders_raw_kafka` — таблица для чтения топика `orders` на первом узле.",
|
||||
"`stg.orders_raw_rep` — реплицируемая таблица на шарде.",
|
||||
"`stg.orders_raw_dist` — распределенная поверхность записи и чтения."
|
||||
"`stg.orders_raw_dist` — распределенная таблица для записи и чтения."
|
||||
],
|
||||
"fill": [
|
||||
"Задача `pull_batch` дага `orders_ingest` забирает слепок пачкой раз в модельные сутки.",
|
||||
"Сырые строки живут трое суток реального времени плюс хвост до суток."
|
||||
"Задача `pull_batch` дага `orders_ingest` загружает слепок раз в модельные сутки.",
|
||||
"Строки хранятся не менее трех суток от загрузки. Удаление целыми частями таблицы добавляет до суток к этому сроку; оно выполняется при очередном слиянии по TTL."
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
@@ -337,13 +336,13 @@
|
||||
"streams": [
|
||||
"events"
|
||||
],
|
||||
"what": "Типизированное широкое событие с именами полей источника. Запрос представления с `FINAL` возвращает актуальную версию события.",
|
||||
"read": "Читайте `ods.event_v`: запрос представления с `FINAL` возвращает актуальные строки вместо физических версий.",
|
||||
"what": "События, разобранные на колонки с типами данных. Имена полей сохранены как в источнике.",
|
||||
"read": "Читайте `ods.event_v`: представление использует `FINAL` и возвращает последнюю версию каждого события.",
|
||||
"parts": [
|
||||
"`ods.event_rep` — реплицируемая таблица версий события на шарде.",
|
||||
"`ods.event_dist` — распределенная поверхность записи и диагностики.",
|
||||
"`ods.event_dist` — распределенная таблица записи и диагностики.",
|
||||
"`ods.event_v` — представление актуального состояния с `FINAL`.",
|
||||
"`ods.event_mv` — матвью разбора годных строк STG."
|
||||
"`ods.event_mv` — материализованное представление для разбора сообщений STG без ошибок."
|
||||
],
|
||||
"keys": [
|
||||
"`WatchID` — тот же идентификатор, что `event_id` в DDS.",
|
||||
@@ -377,16 +376,16 @@
|
||||
"streams": [
|
||||
"events"
|
||||
],
|
||||
"what": "Сырые события, которые не прошли строгий прием, с классом ошибки и метаданными доставки.",
|
||||
"what": "Сообщения событий, которые не удалось разобрать по правилам ODS. Сохранены исходная строка, класс ошибки и сведения о доставке.",
|
||||
"read": "Читайте `_dist`: представления `_v` нет, а распределенная таблица показывает ошибки с обоих шардов.",
|
||||
"parts": [
|
||||
"`ods.event_errors_rep` — реплицируемая таблица ошибок на шарде.",
|
||||
"`ods.event_errors_dist` — распределенная поверхность чтения и записи.",
|
||||
"`ods.event_errors_mv` — матвью, которое отбирает негодные строки STG."
|
||||
"`ods.event_errors_dist` — распределенная таблица чтения и записи.",
|
||||
"`ods.event_errors_mv` — материализованное представление для отбора сообщений STG с ошибками."
|
||||
],
|
||||
"fill": [
|
||||
"`ods.event_errors_mv` пишет непрерывно, по мере записи в STG.",
|
||||
"Ошибки живут один месяц и не останавливают прием годных событий."
|
||||
"Ошибки хранятся один месяц. Остальные события продолжают загружаться."
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
@@ -398,7 +397,7 @@
|
||||
"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"
|
||||
}
|
||||
]
|
||||
@@ -411,11 +410,11 @@
|
||||
"streams": [
|
||||
"orders"
|
||||
],
|
||||
"what": "Текущая версия заказа на языке источника. Позиции пока остаются сырым фрагментом JSON.",
|
||||
"what": "Последняя загруженная версия заказа с именами полей из источника. Состав заказа пока хранится как фрагмент JSON.",
|
||||
"read": "Читайте `ods.order_v`: запрос представления с `FINAL` возвращает последнюю версию по `snapshot_date`.",
|
||||
"parts": [
|
||||
"`ods.order_rep` — реплицируемая таблица версий заказа на шарде.",
|
||||
"`ods.order_dist` — распределенная поверхность записи и диагностики.",
|
||||
"`ods.order_dist` — распределенная таблица записи и диагностики.",
|
||||
"`ods.order_v` — представление текущего состояния с `FINAL`."
|
||||
],
|
||||
"keys": [
|
||||
@@ -423,7 +422,7 @@
|
||||
"`user_id` — пользователь магазина, который переходит в заказ DDS."
|
||||
],
|
||||
"fill": [
|
||||
"Задача `parse_batch` дага `orders_ingest` разбирает пачку раз в модельные сутки.",
|
||||
"Задача `parse_batch` дага `orders_ingest` разбирает загруженный слепок раз в модельные сутки.",
|
||||
"`ReplacingMergeTree(snapshot_date)` оставляет версию из последнего принятого слепка."
|
||||
],
|
||||
"links": [
|
||||
@@ -449,15 +448,15 @@
|
||||
"streams": [
|
||||
"orders"
|
||||
],
|
||||
"what": "Сырые строки слепка, которые не прошли строгий прием, с классом ошибки и `_load_id`.",
|
||||
"what": "Строки выгрузки заказов, которые не удалось разобрать по правилам ODS. Сохранены исходная строка, класс ошибки и `_load_id`.",
|
||||
"read": "Читайте `_dist`: представления `_v` нет, а распределенная таблица показывает ошибки с обоих шардов.",
|
||||
"parts": [
|
||||
"`ods.order_errors_rep` — реплицируемая таблица ошибок на шарде.",
|
||||
"`ods.order_errors_dist` — распределенная поверхность чтения и записи."
|
||||
"`ods.order_errors_dist` — распределенная таблица чтения и записи."
|
||||
],
|
||||
"fill": [
|
||||
"Задача `parse_batch` дага `orders_ingest` пишет ошибки из той же пачки, что и годные заказы.",
|
||||
"Ошибки живут один месяц и не останавливают прием годных заказов."
|
||||
"Задача `parse_batch` дага `orders_ingest` сохраняет строки с ошибками при разборе той же выгрузки, из которой загружаются заказы.",
|
||||
"Ошибки хранятся один месяц. Остальные заказы продолжают загружаться."
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
@@ -478,8 +477,8 @@
|
||||
"streams": [
|
||||
"events"
|
||||
],
|
||||
"what": "Публичное представление события на языке бизнеса. Его запрос переименовывает поля и расшифровывает категорию устройства.",
|
||||
"read": "Читайте `dds.event_v`: запрос представления возвращает явный список колонок DDS.",
|
||||
"what": "События с названиями полей, принятыми в DDS. Например, `WatchID` переименован в `event_id`, а числовой код устройства заменен названием категории.",
|
||||
"read": "Читайте `dds.event_v`: представление возвращает явно перечисленные колонки DDS.",
|
||||
"parts": [
|
||||
"`dds.event_v` — только представление над `ods.event_v`; своей таблицы в DDS нет."
|
||||
],
|
||||
@@ -490,7 +489,7 @@
|
||||
"`purchase_id[1]` — тот же заказ, что `order_id` в DDS и DM."
|
||||
],
|
||||
"fill": [
|
||||
"Данные для `dds.event_v` читаются из актуальных событий ODS без копии и отдельного расписания."
|
||||
"При запросе к `dds.event_v` данные читаются из актуальных событий ODS. Отдельная загрузка в DDS не нужна."
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
@@ -511,22 +510,22 @@
|
||||
"streams": [
|
||||
"events"
|
||||
],
|
||||
"what": "Сессия, которую запрос дага `dds_transform` собирает из последовательных событий одной куки. В источнике готовой сессии нет.",
|
||||
"read": "Читайте `dds.session_v`: запрос представления возвращает деловые и служебные колонки.",
|
||||
"what": "Сессии, рассчитанные дагом `dds_transform` по событиям посетителя. Новая сессия начинается после паузы дольше 30 минут или при смене модельной даты.",
|
||||
"read": "Читайте `dds.session_v`: здесь есть начало, конец, длительность сессии, показатели событий и служебные поля загрузки.",
|
||||
"parts": [
|
||||
"`dds.session_rep` — реплицируемая таблица сессий на шарде.",
|
||||
"`dds.session_dist` — распределенная поверхность загрузки и диагностики.",
|
||||
"`dds.session_stage_rep` — донор дневных партиций на шарде.",
|
||||
"`dds.session_stage_dist` — распределенная поверхность донора.",
|
||||
"`dds.session_v` — публичное представление."
|
||||
"`dds.session_dist` — распределенная таблица загрузки и диагностики.",
|
||||
"`dds.session_stage_rep` — промежуточная таблица для пересчета данных по дням на шарде.",
|
||||
"`dds.session_stage_dist` — распределенная таблица для записи и чтения промежуточных данных.",
|
||||
"`dds.session_v` — представление для чтения готовых данных."
|
||||
],
|
||||
"keys": [
|
||||
"`client_id` — тот же посетитель, что в событии DDS и карте идентичностей.",
|
||||
"`session_date` — день, который в `dm.daily_traffic_v` называется `report_date`."
|
||||
],
|
||||
"fill": [
|
||||
"Даг `dds_transform` пересобирает данные прожитых дней при каждом прогоне конвейера.",
|
||||
"Задачи `session.rebuild` и `session.replace` собирают день в доноре и заменяют партицию."
|
||||
"При каждом запуске `dds_transform` выбирает дни от первого пропущенного в DDS до последнего дня с завершенной генерацией.",
|
||||
"Задача `session.rebuild` рассчитывает данные в промежуточной таблице (доноре). `session.replace` заменяет ими дневные партиции основной таблицы."
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
@@ -551,14 +550,14 @@
|
||||
"streams": [
|
||||
"orders"
|
||||
],
|
||||
"what": "Заказ в текущем состоянии на языке бизнеса. Артикулы, количества и цены позиций хранятся в согласованных массивах.",
|
||||
"read": "Читайте `dds.order_v`: запрос представления возвращает данные без таблиц и донора партиций.",
|
||||
"what": "Последнее известное состояние заказа, подготовленное для анализа. Артикулы, количества и цены товаров хранятся в массивах; элементы с одинаковым индексом относятся к одной позиции заказа.",
|
||||
"read": "Читайте `dds.order_v`, чтобы получить готовые заказы из основной таблицы.",
|
||||
"parts": [
|
||||
"`dds.order_rep` — реплицируемая таблица заказов на шарде.",
|
||||
"`dds.order_dist` — распределенная поверхность загрузки и диагностики.",
|
||||
"`dds.order_stage_rep` — донор дневных партиций на шарде.",
|
||||
"`dds.order_stage_dist` — распределенная поверхность донора.",
|
||||
"`dds.order_v` — публичное представление."
|
||||
"`dds.order_dist` — распределенная таблица загрузки и диагностики.",
|
||||
"`dds.order_stage_rep` — промежуточная таблица для пересчета данных по дням на шарде.",
|
||||
"`dds.order_stage_dist` — распределенная таблица для записи и чтения промежуточных данных.",
|
||||
"`dds.order_v` — представление для чтения готовых данных."
|
||||
],
|
||||
"keys": [
|
||||
"`order_id` — тот же заказ, что в ODS, событии покупки и витрине сверки.",
|
||||
@@ -567,8 +566,8 @@
|
||||
"`order_date` — день, который в витринах называется `report_date` или `order_day`."
|
||||
],
|
||||
"fill": [
|
||||
"Даг `dds_transform` пересобирает изменяемое окно при каждом прогоне конвейера.",
|
||||
"Задачи `order.rebuild` и `order.replace` собирают дни в доноре и заменяют партиции."
|
||||
"При каждом запуске `dds_transform` пересчитывает дни заказов из последнего слепка и заполняет пропуски в DDS.",
|
||||
"Задача `order.rebuild` рассчитывает данные в промежуточной таблице (доноре). `order.replace` заменяет ими дневные партиции основной таблицы."
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
@@ -594,20 +593,20 @@
|
||||
"events",
|
||||
"orders"
|
||||
],
|
||||
"what": "Известные пары куки трекера и пользователя магазина. Связь появляется после сопоставленной покупки.",
|
||||
"read": "Читайте `dds.identity_map_v`: запрос представления с `FINAL` возвращает актуальные строки полного пересчета.",
|
||||
"what": "Связи между посетителем сайта и пользователем магазина. Они появляются, когда событие покупки сопоставлено с заказом по его номеру.",
|
||||
"read": "Читайте `dds.identity_map_v`: представление с `FINAL` возвращает последнюю версию каждой пары посетителя и пользователя.",
|
||||
"parts": [
|
||||
"`dds.identity_map_rep` — реплицируемая таблица версий пар на шарде.",
|
||||
"`dds.identity_map_dist` — распределенная поверхность записи.",
|
||||
"`dds.identity_map_v` — точное публичное представление с `FINAL`."
|
||||
"`dds.identity_map_dist` — распределенная таблица записи.",
|
||||
"`dds.identity_map_v` — представление последних версий строк с `FINAL`."
|
||||
],
|
||||
"keys": [
|
||||
"`client_id` — кука из события и сессии DDS.",
|
||||
"`client_id` — идентификатор посетителя из cookie браузера, как в событии и сессии DDS.",
|
||||
"`user_id` — пользователь магазина из заказа DDS."
|
||||
],
|
||||
"fill": [
|
||||
"Задача `identity_map` дага `dds_transform` пересчитывает все известные пары после обновления заказов.",
|
||||
"Новая версия полного набора пишется при каждом прогоне; `FINAL` скрывает физические повторы."
|
||||
"При каждом запуске записывается новая версия набора связей."
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
@@ -632,19 +631,18 @@
|
||||
"streams": [
|
||||
"events"
|
||||
],
|
||||
"what": "Дневные счетчики посетителей и известных пользователей после склейки идентичностей.",
|
||||
"read": "Читайте `dm.daily_traffic_v`: запрос представления с `FINAL` возвращает актуальные строки полного пересчета.",
|
||||
"what": "Число посетителей и известных пользователей за каждый день. Известными считаются пользователи магазина, связанные с посетителями сайта через карту идентичностей.",
|
||||
"read": "Читайте `dm.daily_traffic_v`: представление с `FINAL` возвращает последний результат расчета за каждый день.",
|
||||
"parts": [
|
||||
"`dm.daily_traffic_rep` — реплицируемая таблица версий счетчиков.",
|
||||
"`dm.daily_traffic_dist` — распределенная поверхность записи.",
|
||||
"`dm.daily_traffic_v` — точное публичное представление с `FINAL`."
|
||||
"`dm.daily_traffic_dist` — распределенная таблица записи.",
|
||||
"`dm.daily_traffic_v` — представление последних версий строк с `FINAL`."
|
||||
],
|
||||
"keys": [
|
||||
"`report_date` — тот же день, что `session_date` в сессии DDS."
|
||||
],
|
||||
"fill": [
|
||||
"Задача `daily_traffic` дага `dm_transform` при каждом прогоне пересчитывает всю историю и записывает новую версию.",
|
||||
"`FINAL` оставляет текущую строку каждого дня."
|
||||
"Задача `daily_traffic` дага `dm_transform` при каждом запуске пересчитывает всю историю и записывает новую версию."
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
@@ -669,22 +667,22 @@
|
||||
"streams": [
|
||||
"orders"
|
||||
],
|
||||
"what": "Дневная выручка, число заказов и единиц товара по категориям.",
|
||||
"read": "Читайте `dm.revenue_daily_v`: это публичное представление готовой дневной витрины.",
|
||||
"what": "Выручка, число заказов и единиц товара за день по категориям. В расчет входят оплаченные заказы.",
|
||||
"read": "Читайте `dm.revenue_daily_v`, чтобы получить готовые показатели по дням и категориям товаров.",
|
||||
"parts": [
|
||||
"`dm.revenue_daily_rep` — реплицируемая таблица витрины на шарде.",
|
||||
"`dm.revenue_daily_dist` — распределенная поверхность загрузки.",
|
||||
"`dm.revenue_daily_stage_rep` — донор дневных партиций на шарде.",
|
||||
"`dm.revenue_daily_stage_dist` — распределенная поверхность донора.",
|
||||
"`dm.revenue_daily_v` — публичное представление."
|
||||
"`dm.revenue_daily_dist` — распределенная таблица загрузки.",
|
||||
"`dm.revenue_daily_stage_rep` — промежуточная таблица для пересчета данных по дням на шарде.",
|
||||
"`dm.revenue_daily_stage_dist` — распределенная таблица для записи и чтения промежуточных данных.",
|
||||
"`dm.revenue_daily_v` — представление для чтения готовых данных."
|
||||
],
|
||||
"keys": [
|
||||
"`report_date` — тот же день, что `order_date` в заказе DDS.",
|
||||
"`product_category` — категория, найденная по `item_sku` заказа и ключу `sku` словаря."
|
||||
],
|
||||
"fill": [
|
||||
"Даг `dm_transform` пересобирает изменяемое окно заказов при каждом прогоне конвейера.",
|
||||
"Задачи `revenue_daily.rebuild` и `revenue_daily.replace` собирают дни в доноре и заменяют партиции."
|
||||
"При каждом запуске `dm_transform` пересчитывает дни заказов из последнего слепка и заполняет пропущенные дни витрины.",
|
||||
"Задача `revenue_daily.rebuild` рассчитывает данные в промежуточной таблице (доноре). `revenue_daily.replace` заменяет ими дневные партиции витрины."
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
@@ -710,22 +708,22 @@
|
||||
"events",
|
||||
"orders"
|
||||
],
|
||||
"what": "Сверка события покупки с заказом магазина. Одна строка показывает совпадение или класс расхождения.",
|
||||
"read": "Читайте `dm.purchase_vs_orders_v`: в результате запроса отсутствующей стороне соответствует `NULL`.",
|
||||
"what": "Результат сверки покупок из кликстрима с заказами магазина. Для каждого номера заказа указано, совпали ли данные двух источников и какое расхождение найдено.",
|
||||
"read": "Читайте `dm.purchase_vs_orders_v`: если покупка или заказ отсутствует, поля этой стороны содержат `NULL`.",
|
||||
"parts": [
|
||||
"`dm.purchase_vs_orders_rep` — реплицируемая таблица сверки на шарде.",
|
||||
"`dm.purchase_vs_orders_dist` — распределенная поверхность загрузки.",
|
||||
"`dm.purchase_vs_orders_stage_rep` — донор дневных партиций на шарде.",
|
||||
"`dm.purchase_vs_orders_stage_dist` — распределенная поверхность донора.",
|
||||
"`dm.purchase_vs_orders_v` — публичное представление."
|
||||
"`dm.purchase_vs_orders_dist` — распределенная таблица загрузки.",
|
||||
"`dm.purchase_vs_orders_stage_rep` — промежуточная таблица для пересчета данных по дням на шарде.",
|
||||
"`dm.purchase_vs_orders_stage_dist` — распределенная таблица для записи и чтения промежуточных данных.",
|
||||
"`dm.purchase_vs_orders_v` — представление для чтения готовых данных."
|
||||
],
|
||||
"keys": [
|
||||
"`order_id` — тот же заказ, что `purchase_id[1]` в событии и `order_id` в заказе DDS.",
|
||||
"`order_day` — общий день события покупки и заказа."
|
||||
"`order_day` — дата заказа; если заказа еще нет, берется дата события покупки."
|
||||
],
|
||||
"fill": [
|
||||
"Даг `dm_transform` пересобирает изменяемый хвост обоих источников при каждом прогоне конвейера.",
|
||||
"Задачи `purchase_vs_orders.rebuild` и `purchase_vs_orders.replace` собирают дни в доноре и заменяют партиции."
|
||||
"При каждом запуске `dm_transform` пересчитывает дни последнего слепка, добавляет новые дни покупок, заполняет пропуски и повторяет сверку дней с покупками без заказа.",
|
||||
"Задача `purchase_vs_orders.rebuild` рассчитывает данные в промежуточной таблице (доноре). `purchase_vs_orders.replace` заменяет ими дневные партиции витрины."
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
@@ -751,19 +749,19 @@
|
||||
"events",
|
||||
"orders"
|
||||
],
|
||||
"what": "Текущее состояние пяти атомарных проверок качества по дням. История запусков остается в Airflow.",
|
||||
"read": "Читайте `dm.dq_summary_v`: запрос к публичному представлению возвращает последнюю опубликованную сводку дня.",
|
||||
"what": "Последние результаты пяти проверок качества данных за каждый день. Историю запусков можно посмотреть в Airflow.",
|
||||
"read": "Читайте `dm.dq_summary_v`, чтобы увидеть результат каждой проверки за день.",
|
||||
"parts": [
|
||||
"`dm.dq_summary_rep` — реплицируемая таблица опубликованной сводки.",
|
||||
"`dm.dq_summary_dist` — распределенная поверхность чтения и записи.",
|
||||
"`dm.dq_summary_stage_rep` — общий донор дневных партиций.",
|
||||
"`dm.dq_summary_stage_dist` — распределенная поверхность донора.",
|
||||
"`dm.dq_summary_v` — публичное представление."
|
||||
"`dm.dq_summary_dist` — распределенная таблица чтения и записи.",
|
||||
"`dm.dq_summary_stage_rep` — промежуточная таблица для результатов всех проверок за день.",
|
||||
"`dm.dq_summary_stage_dist` — распределенная таблица для записи и чтения промежуточных данных.",
|
||||
"`dm.dq_summary_v` — представление для чтения готовых данных."
|
||||
],
|
||||
"fill": [
|
||||
"Даг `dq_check` при каждом прогоне сначала выполняет все пять проверок, затем заменяет готовые дневные партиции.",
|
||||
"Даг `dq_check` сначала рассчитывает результаты всех пяти проверок в промежуточной таблице, затем заменяет ими дневные партиции сводки.",
|
||||
"`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`.",
|
||||
"`purchase_vs_orders_vs_dds` — `sql/dq/purchase_vs_orders_vs_dds.sql`.",
|
||||
"`daily_traffic_vs_dds` — `sql/dq/daily_traffic_vs_dds.sql`."
|
||||
@@ -791,17 +789,17 @@
|
||||
"streams": [
|
||||
"orders"
|
||||
],
|
||||
"what": "Словарь товаров вне цепочки слоев. По артикулу доступны название, бренд, категория и цена.",
|
||||
"read": "Читайте `dic.products`: у словаря одна поверхность без суффикса.",
|
||||
"what": "Словарь товаров. По артикулу можно получить название, бренд, категорию и цену. Эти данные доступны для расчетов в разных слоях.",
|
||||
"read": "Читайте `dic.products`: к словарю обращаются напрямую, суффикса `_v` у него нет.",
|
||||
"parts": [
|
||||
"`dic.products_file` — чтец CSV-каталога из файла репозитория.",
|
||||
"`dic.products` — словарь в памяти поверх подложки."
|
||||
"`dic.products_file` — таблица, которая читает каталог товаров из CSV-файла репозитория.",
|
||||
"`dic.products` — словарь в памяти, загруженный из этой таблицы."
|
||||
],
|
||||
"keys": [
|
||||
"`sku` — тот же артикул, что `item_sku` в позиции заказа DDS."
|
||||
],
|
||||
"fill": [
|
||||
"ClickHouse перечитывает подложку каждые 60–90 секунд по настройке `LIFETIME`."
|
||||
"ClickHouse обновляет словарь из `dic.products_file` каждые 60–90 секунд согласно настройке `LIFETIME`."
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
@@ -825,8 +823,8 @@
|
||||
"events"
|
||||
],
|
||||
"rhythm": "непрерывно, по мере прихода сообщений",
|
||||
"actor": "Чтец `stg.hits_raw_kafka` и матвью `stg.hits_raw_mv`.",
|
||||
"join": "Соединения нет: сообщение кладется строкой целиком.",
|
||||
"actor": "Таблица `stg.hits_raw_kafka` и материализованное представление `stg.hits_raw_mv`.",
|
||||
"join": "Каждое сообщение Kafka записывается целой строкой, без разбора на поля.",
|
||||
"links": [
|
||||
{
|
||||
"label": "SQL",
|
||||
@@ -847,8 +845,8 @@
|
||||
"events"
|
||||
],
|
||||
"rhythm": "непрерывно, по мере прихода сообщений",
|
||||
"actor": "Матвью `ods.event_mv` разбирает годные строки.",
|
||||
"join": "Соединения нет: матвью разбирает строку в колонки.",
|
||||
"actor": "Материализованное представление `ods.event_mv` разбирает сообщения без ошибок.",
|
||||
"join": "Материализованное представление разбирает сообщение из STG на колонки с нужными типами данных.",
|
||||
"links": [
|
||||
{
|
||||
"label": "SQL",
|
||||
@@ -869,8 +867,8 @@
|
||||
"events"
|
||||
],
|
||||
"rhythm": "непрерывно, по мере прихода сообщений",
|
||||
"actor": "Матвью `ods.event_errors_mv` отбирает строки, которые не прошли строгий прием.",
|
||||
"join": "Соединения нет: негодная строка уходит целиком вместе с классом ошибки.",
|
||||
"actor": "Материализованное представление `ods.event_errors_mv` отбирает сообщения с ошибками.",
|
||||
"join": "Сообщение, которое не удалось разобрать по правилам ODS, сохраняется целиком вместе с классом ошибки.",
|
||||
"links": [
|
||||
{
|
||||
"label": "SQL",
|
||||
@@ -890,9 +888,9 @@
|
||||
"streams": [
|
||||
"orders"
|
||||
],
|
||||
"rhythm": "пачкой, раз в модельные сутки",
|
||||
"rhythm": "одной выгрузкой, раз в модельные сутки",
|
||||
"actor": "Задача `pull_batch` дага `orders_ingest` читает `stg.orders_raw_kafka`.",
|
||||
"join": "Соединения нет: сообщение слепка кладется целиком, пачка помечена `_load_id`.",
|
||||
"join": "Строки выгрузки сохраняются целиком. Общий `_load_id` позволяет найти все строки одной загрузки.",
|
||||
"links": [
|
||||
{
|
||||
"label": "SQL",
|
||||
@@ -912,9 +910,9 @@
|
||||
"streams": [
|
||||
"orders"
|
||||
],
|
||||
"rhythm": "пачкой, раз в модельные сутки",
|
||||
"actor": "Задача `parse_batch` дага `orders_ingest` пишет годные версии заказа.",
|
||||
"join": "Соединения нет: строка разбирается в колонки, версия задается `snapshot_date`.",
|
||||
"rhythm": "одной выгрузкой, раз в модельные сутки",
|
||||
"actor": "Задача `parse_batch` дага `orders_ingest` записывает версии заказов без ошибок разбора.",
|
||||
"join": "Строка разбирается на колонки заказа. Версия определяется датой слепка `snapshot_date`.",
|
||||
"links": [
|
||||
{
|
||||
"label": "SQL",
|
||||
@@ -934,9 +932,9 @@
|
||||
"streams": [
|
||||
"orders"
|
||||
],
|
||||
"rhythm": "пачкой, раз в модельные сутки",
|
||||
"actor": "Задача `parse_batch` дага `orders_ingest` пишет негодные строки той же пачки.",
|
||||
"join": "Соединения нет: негодная строка пачки уходит целиком.",
|
||||
"rhythm": "одной выгрузкой, раз в модельные сутки",
|
||||
"actor": "Задача `parse_batch` дага `orders_ingest` записывает строки с ошибками из загруженной выгрузки.",
|
||||
"join": "Строка с ошибкой сохраняется целиком вместе с классом ошибки и идентификатором загрузки.",
|
||||
"links": [
|
||||
{
|
||||
"label": "SQL",
|
||||
@@ -956,9 +954,9 @@
|
||||
"streams": [
|
||||
"events"
|
||||
],
|
||||
"rhythm": "без копии, данные читаются на месте",
|
||||
"actor": "Запрос `dds.event_v` читает данные из `ods.event_v` и переименовывает поля на языке бизнеса.",
|
||||
"join": "Соединения нет: представление переименовывает колонки той же строки.",
|
||||
"rhythm": "при запросе к представлению, без копирования данных",
|
||||
"actor": "Представление `dds.event_v` читает данные из `ods.event_v`.",
|
||||
"join": "Колонки события переименовываются по правилам DDS. Данные не копируются в отдельную таблицу.",
|
||||
"links": [
|
||||
{
|
||||
"label": "SQL",
|
||||
@@ -978,9 +976,9 @@
|
||||
"streams": [
|
||||
"events"
|
||||
],
|
||||
"rhythm": "пачкой, при каждом прогоне `dds_transform`",
|
||||
"rhythm": "при каждом запуске `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": [
|
||||
{
|
||||
"label": "SQL сборки",
|
||||
@@ -1004,9 +1002,9 @@
|
||||
"streams": [
|
||||
"orders"
|
||||
],
|
||||
"rhythm": "пачкой, при каждом прогоне `dds_transform`",
|
||||
"rhythm": "при каждом запуске `dds_transform`",
|
||||
"actor": "Задачи `order.rebuild` и `order.replace` дага `dds_transform`.",
|
||||
"join": "Соединения нет: из заказа берутся поля и позиции, `order_date` выводится из `created_at`.",
|
||||
"join": "Поля заказа приводятся к типам DDS, а позиции из JSON разбираются в массивы. Дата `order_date` рассчитывается из `created_at`.",
|
||||
"links": [
|
||||
{
|
||||
"label": "SQL сборки",
|
||||
@@ -1030,9 +1028,9 @@
|
||||
"streams": [
|
||||
"events"
|
||||
],
|
||||
"rhythm": "пачкой, при каждом прогоне `dds_transform`",
|
||||
"rhythm": "при каждом запуске `dds_transform`",
|
||||
"actor": "Задача `identity_map` дага `dds_transform` соединяет покупки с заказами.",
|
||||
"join": "Строки покупки и заказа соединяются по `purchase_id[1] = order_id`.",
|
||||
"join": "Событие покупки соединяется с заказом по `purchase_id[1] = order_id`. В результате связываются посетитель сайта и пользователь магазина.",
|
||||
"links": [
|
||||
{
|
||||
"label": "SQL",
|
||||
@@ -1052,9 +1050,9 @@
|
||||
"streams": [
|
||||
"orders"
|
||||
],
|
||||
"rhythm": "пачкой, при каждом прогоне `dds_transform`",
|
||||
"rhythm": "при каждом запуске `dds_transform`",
|
||||
"actor": "Задача `identity_map` дага `dds_transform` соединяет заказы с покупками.",
|
||||
"join": "Строки заказа и покупки соединяются по `order_id = purchase_id[1]`; берется `user_id`.",
|
||||
"join": "Заказ соединяется с событием покупки по `order_id = purchase_id[1]`. Из заказа берется `user_id` пользователя магазина.",
|
||||
"links": [
|
||||
{
|
||||
"label": "SQL",
|
||||
@@ -1074,9 +1072,9 @@
|
||||
"streams": [
|
||||
"events"
|
||||
],
|
||||
"rhythm": "пачкой, при каждом прогоне `dm_transform`",
|
||||
"rhythm": "при каждом запуске `dm_transform`",
|
||||
"actor": "Задача `daily_traffic` дага `dm_transform` считает посетителей по сессиям.",
|
||||
"join": "Соединения нет: сессии группируются по `session_date`, который становится `report_date`.",
|
||||
"join": "К сессиям присоединяется карта идентичностей по `client_id`, затем данные группируются по `session_date`. Эта дата записывается в `report_date`.",
|
||||
"links": [
|
||||
{
|
||||
"label": "SQL",
|
||||
@@ -1096,9 +1094,9 @@
|
||||
"streams": [
|
||||
"events"
|
||||
],
|
||||
"rhythm": "пачкой, при каждом прогоне `dm_transform`",
|
||||
"rhythm": "при каждом запуске `dm_transform`",
|
||||
"actor": "Задача `daily_traffic` дага `dm_transform` считает известных пользователей по `client_id`.",
|
||||
"join": "GLOBAL LEFT JOIN по `client_id`; сессии без пары остаются в витрине.",
|
||||
"join": "`GLOBAL LEFT JOIN` присоединяет карту идентичностей к сессиям по `client_id`. Сессии без известного пользователя тоже участвуют в расчете посетителей.",
|
||||
"links": [
|
||||
{
|
||||
"label": "SQL",
|
||||
@@ -1118,9 +1116,9 @@
|
||||
"streams": [
|
||||
"orders"
|
||||
],
|
||||
"rhythm": "пачкой, при каждом прогоне `dm_transform`",
|
||||
"rhythm": "при каждом запуске `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": [
|
||||
{
|
||||
"label": "SQL сборки",
|
||||
@@ -1144,9 +1142,9 @@
|
||||
"streams": [
|
||||
"orders"
|
||||
],
|
||||
"rhythm": "пачкой, при каждом прогоне `dm_transform`",
|
||||
"actor": "Даг `dm_transform` получает категорию через `dictGet` по `item_sku`; ключ словаря — `sku`.",
|
||||
"join": "Категория читается через `dictGet` по `item_sku`; ключ справочника - `sku`.",
|
||||
"rhythm": "при каждом запуске `dm_transform`",
|
||||
"actor": "Задача `revenue_daily.rebuild` дага `dm_transform`.",
|
||||
"join": "Функция `dictGet` получает категорию товара по артикулу `item_sku` из заказа. В словаре этому артикулу соответствует ключ `sku`.",
|
||||
"links": [
|
||||
{
|
||||
"label": "SQL",
|
||||
@@ -1166,9 +1164,9 @@
|
||||
"streams": [
|
||||
"events"
|
||||
],
|
||||
"rhythm": "пачкой, при каждом прогоне `dm_transform`",
|
||||
"rhythm": "при каждом запуске `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": [
|
||||
{
|
||||
"label": "SQL сборки",
|
||||
@@ -1192,9 +1190,9 @@
|
||||
"streams": [
|
||||
"orders"
|
||||
],
|
||||
"rhythm": "пачкой, при каждом прогоне `dm_transform`",
|
||||
"rhythm": "при каждом запуске `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": [
|
||||
{
|
||||
"label": "SQL сборки",
|
||||
@@ -1308,9 +1306,9 @@
|
||||
details.append(heading, meta);
|
||||
addSection("Что это", node.what);
|
||||
addSection("Как читать", node.read);
|
||||
addSection("Из чего состоит", node.parts);
|
||||
addSection("Ключи", node.keys);
|
||||
addSection("Кто наполняет и как часто", node.fill);
|
||||
addSection("Связанные таблицы и представления", node.parts);
|
||||
addSection("Как связать данные", node.keys);
|
||||
addSection("Как обновляются данные", node.fill);
|
||||
addLinks(node.links);
|
||||
}
|
||||
function renderEdgeCard(edge) {
|
||||
@@ -1318,10 +1316,10 @@
|
||||
const heading = document.createElement("h2");
|
||||
heading.textContent = `${nodeById.get(edge.from).name} → ${nodeById.get(edge.to).name}`;
|
||||
details.append(heading);
|
||||
addSection("Механизм", mechanismById.get(edge.mechanism).label);
|
||||
addSection("Ритм", edge.rhythm);
|
||||
addSection("Кто именно", edge.actor);
|
||||
addSection("Как соединяет", edge.join);
|
||||
addSection("Способ обработки", mechanismById.get(edge.mechanism).label);
|
||||
addSection("Когда выполняется", edge.rhythm);
|
||||
addSection("Что выполняет обработку", edge.actor);
|
||||
addSection("Что происходит с данными", edge.join);
|
||||
addLinks(edge.links);
|
||||
}
|
||||
function layoutMap() {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
title: Карта слоев хранилища
|
||||
lead: Проследите, как один смысл меняет имя и поверхность между слоями.
|
||||
lead: Проследите путь событий и заказов от Kafka до отчетов.
|
||||
chapter_url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/handbook/storage-layers.md#user-content-хранилище-и-его-слои
|
||||
intro:
|
||||
- Эта карта показывает путь событий и заказов от Kafka до витрин DM.
|
||||
- Щелкните по узлу, чтобы увидеть его назначение, устройство и ключи.
|
||||
- Щелкните по стрелке, чтобы узнать, кто и каким запросом переносит данные.
|
||||
- Когда вы выбираете узел, карта подсвечивает все пути к нему и от него.
|
||||
- Переключатель потока приглушает другую часть карты, но не меняет раскладку.
|
||||
- Сначала прочитайте главу «Хранилище и его слои», затем вернитесь к карте и рассмотрите объекты.
|
||||
- Выберите объект, чтобы узнать, что он хранит, как его читать и откуда в него поступают данные.
|
||||
- Выберите стрелку, чтобы посмотреть способ обработки и открыть SQL-запрос.
|
||||
- Для выбранного объекта подсвечиваются все пути, по которым данные поступают в него и используются дальше.
|
||||
- Кнопки «События» и «Заказы» выделяют нужный поток. Остальные объекты остаются на месте, но становятся бледнее.
|
||||
- Если вы впервые знакомитесь со слоями хранилища, начните с главы по ссылке ниже.
|
||||
|
||||
streams:
|
||||
all: Все
|
||||
@@ -16,38 +15,38 @@ streams:
|
||||
|
||||
dictionary:
|
||||
title: Словарь
|
||||
caption: 'сбоку цепочки: справочники'
|
||||
caption: справочные данные
|
||||
|
||||
layers:
|
||||
- id: kafka
|
||||
title: Kafka
|
||||
caption: откуда приходит
|
||||
caption: сообщения источников
|
||||
- id: stg
|
||||
title: STG
|
||||
caption: как поступило
|
||||
caption: исходные сообщения
|
||||
- id: ods
|
||||
title: ODS
|
||||
caption: язык источника
|
||||
caption: данные разобраны по полям
|
||||
- id: dds
|
||||
title: DDS
|
||||
caption: язык бизнеса
|
||||
caption: события, заказы и сессии
|
||||
- id: dm
|
||||
title: DM
|
||||
caption: данные для дашбордов
|
||||
caption: витрины для отчетов
|
||||
|
||||
mechanisms:
|
||||
- id: materialized_view
|
||||
label: матвью
|
||||
label: материализованное представление
|
||||
line: solid
|
||||
rhythm: непрерывно, по мере прихода сообщений
|
||||
- id: airflow_dag
|
||||
label: даг Airflow
|
||||
line: dashed
|
||||
rhythm: 'пачкой: раз в модельные сутки (`orders_ingest`) или на каждом прогоне конвейера'
|
||||
rhythm: 'пакетно: раз в модельные сутки (`orders_ingest`) или при каждом запуске конвейера'
|
||||
- id: view
|
||||
label: представление
|
||||
line: dotted
|
||||
rhythm: без копии, данные читаются на месте
|
||||
rhythm: при запросе к представлению, без копирования данных
|
||||
|
||||
nodes:
|
||||
- id: kafka_hits
|
||||
@@ -56,9 +55,9 @@ nodes:
|
||||
order: 0
|
||||
streams: [events]
|
||||
what: Топик Kafka с событиями кликстрима. Каждое сообщение содержит одно событие.
|
||||
read: 'Топик напрямую не читают: таблица `stg.hits_raw_kafka` забирает сообщения из топика. Для данных событий используйте `stg.hits_raw_dist`.'
|
||||
read: "Для просмотра загруженных сообщений используйте `stg.hits_raw_dist`. Таблица `stg.hits_raw_kafka` служит для приема из топика."
|
||||
fill:
|
||||
- Источник событий пишет сообщения в топик в темпе выбранного режима мира.
|
||||
- "Генератор отправляет события без пауз в пакетном режиме или постепенно в живом режиме."
|
||||
links:
|
||||
- label: Справочник
|
||||
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
|
||||
order: 1
|
||||
streams: [orders]
|
||||
what: Топик Kafka со слепками заказов магазина. После модельного дня приходит снимок изменяемого окна.
|
||||
read: 'Топик напрямую не читают: таблица `stg.orders_raw_kafka` забирает сообщения из топика. Для заказов используйте `stg.orders_raw_dist`.'
|
||||
what: "Топик Kafka с дневными выгрузками заказов — слепками. Каждый слепок содержит состояние заказов за последние семь модельных дней."
|
||||
read: "Для просмотра загруженных сообщений используйте `stg.orders_raw_dist`. Таблица `stg.orders_raw_kafka` служит для приема из топика."
|
||||
fill:
|
||||
- Источник заказов публикует слепок пачкой после каждого модельного дня.
|
||||
- "После генерации событий дня D источник отправляет слепок заказов за предыдущий день D−1."
|
||||
links:
|
||||
- label: Справочник
|
||||
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
|
||||
order: 0
|
||||
streams: [events]
|
||||
what: Сырая строка события и метаданные доставки. STG сохраняет сообщение без разбора.
|
||||
what: "Исходное сообщение события и сведения о его доставке из Kafka. Содержимое сохраняется целой строкой, без разбора на поля."
|
||||
read: 'Читайте `_dist`: у STG нет представления `_v`, а распределенная таблица показывает оба шарда.'
|
||||
parts:
|
||||
- '`stg.hits_raw_kafka` — чтец топика `hits`.'
|
||||
- '`stg.hits_raw_rep` — реплицируемая таблица на шарде.'
|
||||
- '`stg.hits_raw_dist` — распределенная поверхность записи и чтения.'
|
||||
- '`stg.hits_raw_mv` — матвью приема сообщений из чтеца.'
|
||||
- "`stg.hits_raw_kafka` — таблица для чтения сообщений топика `hits`."
|
||||
- "`stg.hits_raw_rep` — реплицируемая таблица на шарде."
|
||||
- "`stg.hits_raw_dist` — распределенная таблица для записи и чтения."
|
||||
- "`stg.hits_raw_mv` — материализованное представление, которое записывает сообщения из Kafka в STG."
|
||||
fill:
|
||||
- '`stg.hits_raw_mv` пишет непрерывно, по мере прихода сообщений.'
|
||||
- Сырые строки живут трое суток реального времени плюс хвост до суток.
|
||||
- Строки хранятся не менее трех суток от загрузки. Удаление целыми частями таблицы добавляет до суток к этому сроку; оно выполняется при очередном слиянии по TTL.
|
||||
links:
|
||||
- label: Справочник
|
||||
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/storage.md#user-content-при%D1%91м-поток-и-его-свойства
|
||||
- label: DDL таблиц
|
||||
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
|
||||
|
||||
- id: stg_orders
|
||||
@@ -108,15 +107,15 @@ nodes:
|
||||
layer: stg
|
||||
order: 1
|
||||
streams: [orders]
|
||||
what: Сырые строки слепка заказов, метаданные доставки и идентификатор пачки `_load_id`.
|
||||
what: "Исходные строки выгрузки заказов, сведения о доставке и идентификатор загруженной партии `_load_id`."
|
||||
read: 'Читайте `_dist`: у STG нет представления `_v`, а распределенная таблица показывает оба шарда.'
|
||||
parts:
|
||||
- '`stg.orders_raw_kafka` — чтец топика `orders` на первой ноде.'
|
||||
- '`stg.orders_raw_rep` — реплицируемая таблица на шарде.'
|
||||
- '`stg.orders_raw_dist` — распределенная поверхность записи и чтения.'
|
||||
- "`stg.orders_raw_kafka` — таблица для чтения топика `orders` на первом узле."
|
||||
- "`stg.orders_raw_rep` — реплицируемая таблица на шарде."
|
||||
- "`stg.orders_raw_dist` — распределенная таблица для записи и чтения."
|
||||
fill:
|
||||
- Задача `pull_batch` дага `orders_ingest` забирает слепок пачкой раз в модельные сутки.
|
||||
- Сырые строки живут трое суток реального времени плюс хвост до суток.
|
||||
- Задача `pull_batch` дага `orders_ingest` загружает слепок раз в модельные сутки.
|
||||
- Строки хранятся не менее трех суток от загрузки. Удаление целыми частями таблицы добавляет до суток к этому сроку; оно выполняется при очередном слиянии по TTL.
|
||||
links:
|
||||
- label: Справочник
|
||||
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
|
||||
order: 0
|
||||
streams: [events]
|
||||
what: Типизированное широкое событие с именами полей источника. Запрос представления с `FINAL` возвращает актуальную версию события.
|
||||
read: 'Читайте `ods.event_v`: запрос представления с `FINAL` возвращает актуальные строки вместо физических версий.'
|
||||
what: "События, разобранные на колонки с типами данных. Имена полей сохранены как в источнике."
|
||||
read: "Читайте `ods.event_v`: представление использует `FINAL` и возвращает последнюю версию каждого события."
|
||||
parts:
|
||||
- '`ods.event_rep` — реплицируемая таблица версий события на шарде.'
|
||||
- '`ods.event_dist` — распределенная поверхность записи и диагностики.'
|
||||
- '`ods.event_dist` — распределенная таблица записи и диагностики.'
|
||||
- '`ods.event_v` — представление актуального состояния с `FINAL`.'
|
||||
- '`ods.event_mv` — матвью разбора годных строк STG.'
|
||||
- '`ods.event_mv` — материализованное представление для разбора сообщений STG без ошибок.'
|
||||
keys:
|
||||
- '`WatchID` — тот же идентификатор, что `event_id` в DDS.'
|
||||
- '`ClientID` — тот же посетитель, что `client_id` в DDS.'
|
||||
@@ -155,21 +154,21 @@ nodes:
|
||||
layer: ods
|
||||
order: 1
|
||||
streams: [events]
|
||||
what: Сырые события, которые не прошли строгий прием, с классом ошибки и метаданными доставки.
|
||||
what: "Сообщения событий, которые не удалось разобрать по правилам ODS. Сохранены исходная строка, класс ошибки и сведения о доставке."
|
||||
read: 'Читайте `_dist`: представления `_v` нет, а распределенная таблица показывает ошибки с обоих шардов.'
|
||||
parts:
|
||||
- '`ods.event_errors_rep` — реплицируемая таблица ошибок на шарде.'
|
||||
- '`ods.event_errors_dist` — распределенная поверхность чтения и записи.'
|
||||
- '`ods.event_errors_mv` — матвью, которое отбирает негодные строки STG.'
|
||||
- '`ods.event_errors_dist` — распределенная таблица чтения и записи.'
|
||||
- '`ods.event_errors_mv` — материализованное представление для отбора сообщений STG с ошибками.'
|
||||
fill:
|
||||
- '`ods.event_errors_mv` пишет непрерывно, по мере записи в STG.'
|
||||
- Ошибки живут один месяц и не останавливают прием годных событий.
|
||||
- Ошибки хранятся один месяц. Остальные события продолжают загружаться.
|
||||
links:
|
||||
- label: Справочник
|
||||
url: https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/architecture/storage.md#user-content-таблица-ошибок
|
||||
- label: DDL таблиц
|
||||
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
|
||||
|
||||
- id: ods_order
|
||||
@@ -177,17 +176,17 @@ nodes:
|
||||
layer: ods
|
||||
order: 2
|
||||
streams: [orders]
|
||||
what: Текущая версия заказа на языке источника. Позиции пока остаются сырым фрагментом JSON.
|
||||
what: "Последняя загруженная версия заказа с именами полей из источника. Состав заказа пока хранится как фрагмент JSON."
|
||||
read: 'Читайте `ods.order_v`: запрос представления с `FINAL` возвращает последнюю версию по `snapshot_date`.'
|
||||
parts:
|
||||
- '`ods.order_rep` — реплицируемая таблица версий заказа на шарде.'
|
||||
- '`ods.order_dist` — распределенная поверхность записи и диагностики.'
|
||||
- '`ods.order_dist` — распределенная таблица записи и диагностики.'
|
||||
- '`ods.order_v` — представление текущего состояния с `FINAL`.'
|
||||
keys:
|
||||
- '`order_id` — тот же заказ в `dds.order_v` и `dm.purchase_vs_orders_v`.'
|
||||
- '`user_id` — пользователь магазина, который переходит в заказ DDS.'
|
||||
fill:
|
||||
- Задача `parse_batch` дага `orders_ingest` разбирает пачку раз в модельные сутки.
|
||||
- Задача `parse_batch` дага `orders_ingest` разбирает загруженный слепок раз в модельные сутки.
|
||||
- '`ReplacingMergeTree(snapshot_date)` оставляет версию из последнего принятого слепка.'
|
||||
links:
|
||||
- label: Справочник
|
||||
@@ -202,14 +201,14 @@ nodes:
|
||||
layer: ods
|
||||
order: 3
|
||||
streams: [orders]
|
||||
what: Сырые строки слепка, которые не прошли строгий прием, с классом ошибки и `_load_id`.
|
||||
what: "Строки выгрузки заказов, которые не удалось разобрать по правилам ODS. Сохранены исходная строка, класс ошибки и `_load_id`."
|
||||
read: 'Читайте `_dist`: представления `_v` нет, а распределенная таблица показывает ошибки с обоих шардов.'
|
||||
parts:
|
||||
- '`ods.order_errors_rep` — реплицируемая таблица ошибок на шарде.'
|
||||
- '`ods.order_errors_dist` — распределенная поверхность чтения и записи.'
|
||||
- '`ods.order_errors_dist` — распределенная таблица чтения и записи.'
|
||||
fill:
|
||||
- Задача `parse_batch` дага `orders_ingest` пишет ошибки из той же пачки, что и годные заказы.
|
||||
- Ошибки живут один месяц и не останавливают прием годных заказов.
|
||||
- Задача `parse_batch` дага `orders_ingest` сохраняет строки с ошибками при разборе той же выгрузки, из которой загружаются заказы.
|
||||
- Ошибки хранятся один месяц. Остальные заказы продолжают загружаться.
|
||||
links:
|
||||
- label: Справочник
|
||||
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
|
||||
order: 0
|
||||
streams: [events]
|
||||
what: Публичное представление события на языке бизнеса. Его запрос переименовывает поля и расшифровывает категорию устройства.
|
||||
read: 'Читайте `dds.event_v`: запрос представления возвращает явный список колонок DDS.'
|
||||
what: "События с названиями полей, принятыми в DDS. Например, `WatchID` переименован в `event_id`, а числовой код устройства заменен названием категории."
|
||||
read: "Читайте `dds.event_v`: представление возвращает явно перечисленные колонки DDS."
|
||||
parts:
|
||||
- '`dds.event_v` — только представление над `ods.event_v`; своей таблицы в DDS нет.'
|
||||
keys:
|
||||
@@ -231,7 +230,7 @@ nodes:
|
||||
- '`event_date` — тот же день, что `EventDate` в ODS.'
|
||||
- '`purchase_id[1]` — тот же заказ, что `order_id` в DDS и DM.'
|
||||
fill:
|
||||
- Данные для `dds.event_v` читаются из актуальных событий ODS без копии и отдельного расписания.
|
||||
- "При запросе к `dds.event_v` данные читаются из актуальных событий ODS. Отдельная загрузка в DDS не нужна."
|
||||
links:
|
||||
- label: Справочник
|
||||
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
|
||||
order: 1
|
||||
streams: [events]
|
||||
what: Сессия, которую запрос дага `dds_transform` собирает из последовательных событий одной куки. В источнике готовой сессии нет.
|
||||
read: 'Читайте `dds.session_v`: запрос представления возвращает деловые и служебные колонки.'
|
||||
what: "Сессии, рассчитанные дагом `dds_transform` по событиям посетителя. Новая сессия начинается после паузы дольше 30 минут или при смене модельной даты."
|
||||
read: "Читайте `dds.session_v`: здесь есть начало, конец, длительность сессии, показатели событий и служебные поля загрузки."
|
||||
parts:
|
||||
- '`dds.session_rep` — реплицируемая таблица сессий на шарде.'
|
||||
- '`dds.session_dist` — распределенная поверхность загрузки и диагностики.'
|
||||
- '`dds.session_stage_rep` — донор дневных партиций на шарде.'
|
||||
- '`dds.session_stage_dist` — распределенная поверхность донора.'
|
||||
- '`dds.session_v` — публичное представление.'
|
||||
- '`dds.session_dist` — распределенная таблица загрузки и диагностики.'
|
||||
- '`dds.session_stage_rep` — промежуточная таблица для пересчета данных по дням на шарде.'
|
||||
- '`dds.session_stage_dist` — распределенная таблица для записи и чтения промежуточных данных.'
|
||||
- '`dds.session_v` — представление для чтения готовых данных.'
|
||||
keys:
|
||||
- '`client_id` — тот же посетитель, что в событии DDS и карте идентичностей.'
|
||||
- '`session_date` — день, который в `dm.daily_traffic_v` называется `report_date`.'
|
||||
fill:
|
||||
- Даг `dds_transform` пересобирает данные прожитых дней при каждом прогоне конвейера.
|
||||
- Задачи `session.rebuild` и `session.replace` собирают день в доноре и заменяют партицию.
|
||||
- "При каждом запуске `dds_transform` выбирает дни от первого пропущенного в DDS до последнего дня с завершенной генерацией."
|
||||
- "Задача `session.rebuild` рассчитывает данные в промежуточной таблице (доноре). `session.replace` заменяет ими дневные партиции основной таблицы."
|
||||
links:
|
||||
- label: Справочник
|
||||
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
|
||||
order: 2
|
||||
streams: [orders]
|
||||
what: Заказ в текущем состоянии на языке бизнеса. Артикулы, количества и цены позиций хранятся в согласованных массивах.
|
||||
read: 'Читайте `dds.order_v`: запрос представления возвращает данные без таблиц и донора партиций.'
|
||||
what: "Последнее известное состояние заказа, подготовленное для анализа. Артикулы, количества и цены товаров хранятся в массивах; элементы с одинаковым индексом относятся к одной позиции заказа."
|
||||
read: "Читайте `dds.order_v`, чтобы получить готовые заказы из основной таблицы."
|
||||
parts:
|
||||
- '`dds.order_rep` — реплицируемая таблица заказов на шарде.'
|
||||
- '`dds.order_dist` — распределенная поверхность загрузки и диагностики.'
|
||||
- '`dds.order_stage_rep` — донор дневных партиций на шарде.'
|
||||
- '`dds.order_stage_dist` — распределенная поверхность донора.'
|
||||
- '`dds.order_v` — публичное представление.'
|
||||
- '`dds.order_dist` — распределенная таблица загрузки и диагностики.'
|
||||
- '`dds.order_stage_rep` — промежуточная таблица для пересчета данных по дням на шарде.'
|
||||
- '`dds.order_stage_dist` — распределенная таблица для записи и чтения промежуточных данных.'
|
||||
- '`dds.order_v` — представление для чтения готовых данных.'
|
||||
keys:
|
||||
- '`order_id` — тот же заказ, что в ODS, событии покупки и витрине сверки.'
|
||||
- '`user_id` — пользователь магазина, который входит в карту идентичностей.'
|
||||
- '`item_sku` — тот же артикул, что `sku` в `dic.products`.'
|
||||
- '`order_date` — день, который в витринах называется `report_date` или `order_day`.'
|
||||
fill:
|
||||
- Даг `dds_transform` пересобирает изменяемое окно при каждом прогоне конвейера.
|
||||
- Задачи `order.rebuild` и `order.replace` собирают дни в доноре и заменяют партиции.
|
||||
- "При каждом запуске `dds_transform` пересчитывает дни заказов из последнего слепка и заполняет пропуски в DDS."
|
||||
- "Задача `order.rebuild` рассчитывает данные в промежуточной таблице (доноре). `order.replace` заменяет ими дневные партиции основной таблицы."
|
||||
links:
|
||||
- label: Справочник
|
||||
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
|
||||
order: 3
|
||||
streams: [events, orders]
|
||||
what: Известные пары куки трекера и пользователя магазина. Связь появляется после сопоставленной покупки.
|
||||
read: 'Читайте `dds.identity_map_v`: запрос представления с `FINAL` возвращает актуальные строки полного пересчета.'
|
||||
what: "Связи между посетителем сайта и пользователем магазина. Они появляются, когда событие покупки сопоставлено с заказом по его номеру."
|
||||
read: "Читайте `dds.identity_map_v`: представление с `FINAL` возвращает последнюю версию каждой пары посетителя и пользователя."
|
||||
parts:
|
||||
- '`dds.identity_map_rep` — реплицируемая таблица версий пар на шарде.'
|
||||
- '`dds.identity_map_dist` — распределенная поверхность записи.'
|
||||
- '`dds.identity_map_v` — точное публичное представление с `FINAL`.'
|
||||
- '`dds.identity_map_dist` — распределенная таблица записи.'
|
||||
- '`dds.identity_map_v` — представление последних версий строк с `FINAL`.'
|
||||
keys:
|
||||
- '`client_id` — кука из события и сессии DDS.'
|
||||
- '`user_id` — пользователь магазина из заказа DDS.'
|
||||
- "`client_id` — идентификатор посетителя из cookie браузера, как в событии и сессии DDS."
|
||||
- "`user_id` — пользователь магазина из заказа DDS."
|
||||
fill:
|
||||
- Задача `identity_map` дага `dds_transform` пересчитывает все известные пары после обновления заказов.
|
||||
- Новая версия полного набора пишется при каждом прогоне; `FINAL` скрывает физические повторы.
|
||||
- "Задача `identity_map` дага `dds_transform` пересчитывает все известные пары после обновления заказов."
|
||||
- "При каждом запуске записывается новая версия набора связей."
|
||||
links:
|
||||
- label: Справочник
|
||||
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
|
||||
order: 0
|
||||
streams: [events]
|
||||
what: Дневные счетчики посетителей и известных пользователей после склейки идентичностей.
|
||||
read: 'Читайте `dm.daily_traffic_v`: запрос представления с `FINAL` возвращает актуальные строки полного пересчета.'
|
||||
what: "Число посетителей и известных пользователей за каждый день. Известными считаются пользователи магазина, связанные с посетителями сайта через карту идентичностей."
|
||||
read: "Читайте `dm.daily_traffic_v`: представление с `FINAL` возвращает последний результат расчета за каждый день."
|
||||
parts:
|
||||
- '`dm.daily_traffic_rep` — реплицируемая таблица версий счетчиков.'
|
||||
- '`dm.daily_traffic_dist` — распределенная поверхность записи.'
|
||||
- '`dm.daily_traffic_v` — точное публичное представление с `FINAL`.'
|
||||
- '`dm.daily_traffic_dist` — распределенная таблица записи.'
|
||||
- '`dm.daily_traffic_v` — представление последних версий строк с `FINAL`.'
|
||||
keys:
|
||||
- '`report_date` — тот же день, что `session_date` в сессии DDS.'
|
||||
fill:
|
||||
- Задача `daily_traffic` дага `dm_transform` при каждом прогоне пересчитывает всю историю и записывает новую версию.
|
||||
- '`FINAL` оставляет текущую строку каждого дня.'
|
||||
- "Задача `daily_traffic` дага `dm_transform` при каждом запуске пересчитывает всю историю и записывает новую версию."
|
||||
links:
|
||||
- label: Справочник
|
||||
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
|
||||
order: 1
|
||||
streams: [orders]
|
||||
what: Дневная выручка, число заказов и единиц товара по категориям.
|
||||
read: 'Читайте `dm.revenue_daily_v`: это публичное представление готовой дневной витрины.'
|
||||
what: "Выручка, число заказов и единиц товара за день по категориям. В расчет входят оплаченные заказы."
|
||||
read: "Читайте `dm.revenue_daily_v`, чтобы получить готовые показатели по дням и категориям товаров."
|
||||
parts:
|
||||
- '`dm.revenue_daily_rep` — реплицируемая таблица витрины на шарде.'
|
||||
- '`dm.revenue_daily_dist` — распределенная поверхность загрузки.'
|
||||
- '`dm.revenue_daily_stage_rep` — донор дневных партиций на шарде.'
|
||||
- '`dm.revenue_daily_stage_dist` — распределенная поверхность донора.'
|
||||
- '`dm.revenue_daily_v` — публичное представление.'
|
||||
- '`dm.revenue_daily_dist` — распределенная таблица загрузки.'
|
||||
- '`dm.revenue_daily_stage_rep` — промежуточная таблица для пересчета данных по дням на шарде.'
|
||||
- '`dm.revenue_daily_stage_dist` — распределенная таблица для записи и чтения промежуточных данных.'
|
||||
- '`dm.revenue_daily_v` — представление для чтения готовых данных.'
|
||||
keys:
|
||||
- '`report_date` — тот же день, что `order_date` в заказе DDS.'
|
||||
- '`product_category` — категория, найденная по `item_sku` заказа и ключу `sku` словаря.'
|
||||
fill:
|
||||
- Даг `dm_transform` пересобирает изменяемое окно заказов при каждом прогоне конвейера.
|
||||
- Задачи `revenue_daily.rebuild` и `revenue_daily.replace` собирают дни в доноре и заменяют партиции.
|
||||
- "При каждом запуске `dm_transform` пересчитывает дни заказов из последнего слепка и заполняет пропущенные дни витрины."
|
||||
- "Задача `revenue_daily.rebuild` рассчитывает данные в промежуточной таблице (доноре). `revenue_daily.replace` заменяет ими дневные партиции витрины."
|
||||
links:
|
||||
- label: Справочник
|
||||
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
|
||||
order: 2
|
||||
streams: [events, orders]
|
||||
what: Сверка события покупки с заказом магазина. Одна строка показывает совпадение или класс расхождения.
|
||||
read: 'Читайте `dm.purchase_vs_orders_v`: в результате запроса отсутствующей стороне соответствует `NULL`.'
|
||||
what: "Результат сверки покупок из кликстрима с заказами магазина. Для каждого номера заказа указано, совпали ли данные двух источников и какое расхождение найдено."
|
||||
read: "Читайте `dm.purchase_vs_orders_v`: если покупка или заказ отсутствует, поля этой стороны содержат `NULL`."
|
||||
parts:
|
||||
- '`dm.purchase_vs_orders_rep` — реплицируемая таблица сверки на шарде.'
|
||||
- '`dm.purchase_vs_orders_dist` — распределенная поверхность загрузки.'
|
||||
- '`dm.purchase_vs_orders_stage_rep` — донор дневных партиций на шарде.'
|
||||
- '`dm.purchase_vs_orders_stage_dist` — распределенная поверхность донора.'
|
||||
- '`dm.purchase_vs_orders_v` — публичное представление.'
|
||||
- '`dm.purchase_vs_orders_dist` — распределенная таблица загрузки.'
|
||||
- '`dm.purchase_vs_orders_stage_rep` — промежуточная таблица для пересчета данных по дням на шарде.'
|
||||
- '`dm.purchase_vs_orders_stage_dist` — распределенная таблица для записи и чтения промежуточных данных.'
|
||||
- '`dm.purchase_vs_orders_v` — представление для чтения готовых данных.'
|
||||
keys:
|
||||
- '`order_id` — тот же заказ, что `purchase_id[1]` в событии и `order_id` в заказе DDS.'
|
||||
- '`order_day` — общий день события покупки и заказа.'
|
||||
- "`order_id` — тот же заказ, что `purchase_id[1]` в событии и `order_id` в заказе DDS."
|
||||
- "`order_day` — дата заказа; если заказа еще нет, берется дата события покупки."
|
||||
fill:
|
||||
- Даг `dm_transform` пересобирает изменяемый хвост обоих источников при каждом прогоне конвейера.
|
||||
- Задачи `purchase_vs_orders.rebuild` и `purchase_vs_orders.replace` собирают дни в доноре и заменяют партиции.
|
||||
- "При каждом запуске `dm_transform` пересчитывает дни последнего слепка, добавляет новые дни покупок, заполняет пропуски и повторяет сверку дней с покупками без заказа."
|
||||
- "Задача `purchase_vs_orders.rebuild` рассчитывает данные в промежуточной таблице (доноре). `purchase_vs_orders.replace` заменяет ими дневные партиции витрины."
|
||||
links:
|
||||
- label: Справочник
|
||||
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
|
||||
order: 3
|
||||
streams: [events, orders]
|
||||
what: Текущее состояние пяти атомарных проверок качества по дням. История запусков остается в Airflow.
|
||||
read: 'Читайте `dm.dq_summary_v`: запрос к публичному представлению возвращает последнюю опубликованную сводку дня.'
|
||||
what: "Последние результаты пяти проверок качества данных за каждый день. Историю запусков можно посмотреть в Airflow."
|
||||
read: "Читайте `dm.dq_summary_v`, чтобы увидеть результат каждой проверки за день."
|
||||
parts:
|
||||
- '`dm.dq_summary_rep` — реплицируемая таблица опубликованной сводки.'
|
||||
- '`dm.dq_summary_dist` — распределенная поверхность чтения и записи.'
|
||||
- '`dm.dq_summary_stage_rep` — общий донор дневных партиций.'
|
||||
- '`dm.dq_summary_stage_dist` — распределенная поверхность донора.'
|
||||
- '`dm.dq_summary_v` — публичное представление.'
|
||||
- '`dm.dq_summary_dist` — распределенная таблица чтения и записи.'
|
||||
- '`dm.dq_summary_stage_rep` — промежуточная таблица для результатов всех проверок за день.'
|
||||
- '`dm.dq_summary_stage_dist` — распределенная таблица для записи и чтения промежуточных данных.'
|
||||
- '`dm.dq_summary_v` — представление для чтения готовых данных.'
|
||||
fill:
|
||||
- Даг `dq_check` при каждом прогоне сначала выполняет все пять проверок, затем заменяет готовые дневные партиции.
|
||||
- Даг `dq_check` сначала рассчитывает результаты всех пяти проверок в промежуточной таблице, затем заменяет ими дневные партиции сводки.
|
||||
- '`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`.'
|
||||
- '`purchase_vs_orders_vs_dds` — `sql/dq/purchase_vs_orders_vs_dds.sql`.'
|
||||
- '`daily_traffic_vs_dds` — `sql/dq/daily_traffic_vs_dds.sql`.'
|
||||
@@ -430,15 +428,15 @@ nodes:
|
||||
layer: dic
|
||||
order: 0
|
||||
streams: [orders]
|
||||
what: Словарь товаров вне цепочки слоев. По артикулу доступны название, бренд, категория и цена.
|
||||
read: 'Читайте `dic.products`: у словаря одна поверхность без суффикса.'
|
||||
what: "Словарь товаров. По артикулу можно получить название, бренд, категорию и цену. Эти данные доступны для расчетов в разных слоях."
|
||||
read: "Читайте `dic.products`: к словарю обращаются напрямую, суффикса `_v` у него нет."
|
||||
parts:
|
||||
- '`dic.products_file` — чтец CSV-каталога из файла репозитория.'
|
||||
- '`dic.products` — словарь в памяти поверх подложки.'
|
||||
- "`dic.products_file` — таблица, которая читает каталог товаров из CSV-файла репозитория."
|
||||
- "`dic.products` — словарь в памяти, загруженный из этой таблицы."
|
||||
keys:
|
||||
- '`sku` — тот же артикул, что `item_sku` в позиции заказа DDS.'
|
||||
fill:
|
||||
- ClickHouse перечитывает подложку каждые 60–90 секунд по настройке `LIFETIME`.
|
||||
- "ClickHouse обновляет словарь из `dic.products_file` каждые 60–90 секунд согласно настройке `LIFETIME`."
|
||||
links:
|
||||
- label: Справочник
|
||||
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
|
||||
streams: [events]
|
||||
rhythm: непрерывно, по мере прихода сообщений
|
||||
actor: Чтец `stg.hits_raw_kafka` и матвью `stg.hits_raw_mv`.
|
||||
join: 'Соединения нет: сообщение кладется строкой целиком.'
|
||||
actor: "Таблица `stg.hits_raw_kafka` и материализованное представление `stg.hits_raw_mv`."
|
||||
join: "Каждое сообщение Kafka записывается целой строкой, без разбора на поля."
|
||||
links:
|
||||
- label: 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
|
||||
streams: [events]
|
||||
rhythm: непрерывно, по мере прихода сообщений
|
||||
actor: Матвью `ods.event_mv` разбирает годные строки.
|
||||
join: 'Соединения нет: матвью разбирает строку в колонки.'
|
||||
actor: Материализованное представление `ods.event_mv` разбирает сообщения без ошибок.
|
||||
join: "Материализованное представление разбирает сообщение из STG на колонки с нужными типами данных."
|
||||
links:
|
||||
- label: 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
|
||||
streams: [events]
|
||||
rhythm: непрерывно, по мере прихода сообщений
|
||||
actor: Матвью `ods.event_errors_mv` отбирает строки, которые не прошли строгий прием.
|
||||
join: 'Соединения нет: негодная строка уходит целиком вместе с классом ошибки.'
|
||||
actor: "Материализованное представление `ods.event_errors_mv` отбирает сообщения с ошибками."
|
||||
join: "Сообщение, которое не удалось разобрать по правилам ODS, сохраняется целиком вместе с классом ошибки."
|
||||
links:
|
||||
- label: 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
|
||||
mechanism: airflow_dag
|
||||
streams: [orders]
|
||||
rhythm: пачкой, раз в модельные сутки
|
||||
rhythm: одной выгрузкой, раз в модельные сутки
|
||||
actor: Задача `pull_batch` дага `orders_ingest` читает `stg.orders_raw_kafka`.
|
||||
join: 'Соединения нет: сообщение слепка кладется целиком, пачка помечена `_load_id`.'
|
||||
join: "Строки выгрузки сохраняются целиком. Общий `_load_id` позволяет найти все строки одной загрузки."
|
||||
links:
|
||||
- label: 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
|
||||
mechanism: airflow_dag
|
||||
streams: [orders]
|
||||
rhythm: пачкой, раз в модельные сутки
|
||||
actor: Задача `parse_batch` дага `orders_ingest` пишет годные версии заказа.
|
||||
join: 'Соединения нет: строка разбирается в колонки, версия задается `snapshot_date`.'
|
||||
rhythm: одной выгрузкой, раз в модельные сутки
|
||||
actor: Задача `parse_batch` дага `orders_ingest` записывает версии заказов без ошибок разбора.
|
||||
join: "Строка разбирается на колонки заказа. Версия определяется датой слепка `snapshot_date`."
|
||||
links:
|
||||
- label: 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
|
||||
mechanism: airflow_dag
|
||||
streams: [orders]
|
||||
rhythm: пачкой, раз в модельные сутки
|
||||
actor: Задача `parse_batch` дага `orders_ingest` пишет негодные строки той же пачки.
|
||||
join: 'Соединения нет: негодная строка пачки уходит целиком.'
|
||||
rhythm: одной выгрузкой, раз в модельные сутки
|
||||
actor: Задача `parse_batch` дага `orders_ingest` записывает строки с ошибками из загруженной выгрузки.
|
||||
join: "Строка с ошибкой сохраняется целиком вместе с классом ошибки и идентификатором загрузки."
|
||||
links:
|
||||
- label: 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
|
||||
mechanism: view
|
||||
streams: [events]
|
||||
rhythm: без копии, данные читаются на месте
|
||||
actor: Запрос `dds.event_v` читает данные из `ods.event_v` и переименовывает поля на языке бизнеса.
|
||||
join: 'Соединения нет: представление переименовывает колонки той же строки.'
|
||||
rhythm: при запросе к представлению, без копирования данных
|
||||
actor: "Представление `dds.event_v` читает данные из `ods.event_v`."
|
||||
join: "Колонки события переименовываются по правилам DDS. Данные не копируются в отдельную таблицу."
|
||||
links:
|
||||
- label: 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
|
||||
mechanism: airflow_dag
|
||||
streams: [events]
|
||||
rhythm: пачкой, при каждом прогоне `dds_transform`
|
||||
rhythm: при каждом запуске `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:
|
||||
- label: 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
|
||||
mechanism: airflow_dag
|
||||
streams: [orders]
|
||||
rhythm: пачкой, при каждом прогоне `dds_transform`
|
||||
rhythm: при каждом запуске `dds_transform`
|
||||
actor: Задачи `order.rebuild` и `order.replace` дага `dds_transform`.
|
||||
join: 'Соединения нет: из заказа берутся поля и позиции, `order_date` выводится из `created_at`.'
|
||||
join: "Поля заказа приводятся к типам DDS, а позиции из JSON разбираются в массивы. Дата `order_date` рассчитывается из `created_at`."
|
||||
links:
|
||||
- label: 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
|
||||
mechanism: airflow_dag
|
||||
streams: [events]
|
||||
rhythm: пачкой, при каждом прогоне `dds_transform`
|
||||
rhythm: при каждом запуске `dds_transform`
|
||||
actor: Задача `identity_map` дага `dds_transform` соединяет покупки с заказами.
|
||||
join: 'Строки покупки и заказа соединяются по `purchase_id[1] = order_id`.'
|
||||
join: "Событие покупки соединяется с заказом по `purchase_id[1] = order_id`. В результате связываются посетитель сайта и пользователь магазина."
|
||||
links:
|
||||
- label: 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
|
||||
mechanism: airflow_dag
|
||||
streams: [orders]
|
||||
rhythm: пачкой, при каждом прогоне `dds_transform`
|
||||
rhythm: при каждом запуске `dds_transform`
|
||||
actor: Задача `identity_map` дага `dds_transform` соединяет заказы с покупками.
|
||||
join: 'Строки заказа и покупки соединяются по `order_id = purchase_id[1]`; берется `user_id`.'
|
||||
join: "Заказ соединяется с событием покупки по `order_id = purchase_id[1]`. Из заказа берется `user_id` пользователя магазина."
|
||||
links:
|
||||
- label: 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
|
||||
mechanism: airflow_dag
|
||||
streams: [events]
|
||||
rhythm: пачкой, при каждом прогоне `dm_transform`
|
||||
rhythm: при каждом запуске `dm_transform`
|
||||
actor: Задача `daily_traffic` дага `dm_transform` считает посетителей по сессиям.
|
||||
join: 'Соединения нет: сессии группируются по `session_date`, который становится `report_date`.'
|
||||
join: "К сессиям присоединяется карта идентичностей по `client_id`, затем данные группируются по `session_date`. Эта дата записывается в `report_date`."
|
||||
links:
|
||||
- label: 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
|
||||
mechanism: airflow_dag
|
||||
streams: [events]
|
||||
rhythm: пачкой, при каждом прогоне `dm_transform`
|
||||
rhythm: при каждом запуске `dm_transform`
|
||||
actor: Задача `daily_traffic` дага `dm_transform` считает известных пользователей по `client_id`.
|
||||
join: 'GLOBAL LEFT JOIN по `client_id`; сессии без пары остаются в витрине.'
|
||||
join: "`GLOBAL LEFT JOIN` присоединяет карту идентичностей к сессиям по `client_id`. Сессии без известного пользователя тоже участвуют в расчете посетителей."
|
||||
links:
|
||||
- label: 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
|
||||
mechanism: airflow_dag
|
||||
streams: [orders]
|
||||
rhythm: пачкой, при каждом прогоне `dm_transform`
|
||||
rhythm: при каждом запуске `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:
|
||||
- label: 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
|
||||
mechanism: airflow_dag
|
||||
streams: [orders]
|
||||
rhythm: пачкой, при каждом прогоне `dm_transform`
|
||||
actor: Даг `dm_transform` получает категорию через `dictGet` по `item_sku`; ключ словаря — `sku`.
|
||||
join: 'Категория читается через `dictGet` по `item_sku`; ключ справочника - `sku`.'
|
||||
rhythm: при каждом запуске `dm_transform`
|
||||
actor: "Задача `revenue_daily.rebuild` дага `dm_transform`."
|
||||
join: "Функция `dictGet` получает категорию товара по артикулу `item_sku` из заказа. В словаре этому артикулу соответствует ключ `sku`."
|
||||
links:
|
||||
- label: 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
|
||||
mechanism: airflow_dag
|
||||
streams: [events]
|
||||
rhythm: пачкой, при каждом прогоне `dm_transform`
|
||||
rhythm: при каждом запуске `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:
|
||||
- label: 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
|
||||
mechanism: airflow_dag
|
||||
streams: [orders]
|
||||
rhythm: пачкой, при каждом прогоне `dm_transform`
|
||||
rhythm: при каждом запуске `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:
|
||||
- label: 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);
|
||||
addSection("Что это", node.what);
|
||||
addSection("Как читать", node.read);
|
||||
addSection("Из чего состоит", node.parts);
|
||||
addSection("Ключи", node.keys);
|
||||
addSection("Кто наполняет и как часто", node.fill);
|
||||
addSection("Связанные таблицы и представления", node.parts);
|
||||
addSection("Как связать данные", node.keys);
|
||||
addSection("Как обновляются данные", node.fill);
|
||||
addLinks(node.links);
|
||||
}
|
||||
function renderEdgeCard(edge) {
|
||||
@@ -253,10 +253,10 @@
|
||||
const heading = document.createElement("h2");
|
||||
heading.textContent = `${nodeById.get(edge.from).name} → ${nodeById.get(edge.to).name}`;
|
||||
details.append(heading);
|
||||
addSection("Механизм", mechanismById.get(edge.mechanism).label);
|
||||
addSection("Ритм", edge.rhythm);
|
||||
addSection("Кто именно", edge.actor);
|
||||
addSection("Как соединяет", edge.join);
|
||||
addSection("Способ обработки", mechanismById.get(edge.mechanism).label);
|
||||
addSection("Когда выполняется", edge.rhythm);
|
||||
addSection("Что выполняет обработку", edge.actor);
|
||||
addSection("Что происходит с данными", edge.join);
|
||||
addLinks(edge.links);
|
||||
}
|
||||
function layoutMap() {
|
||||
|
||||
+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) считает каждый модельный день отдельно:
|
||||
-- события разных дней не попадут в одну сессию даже при короткой паузе.
|
||||
-- Учитываются все типы событий.
|
||||
--
|
||||
-- Нарезка — центральный расчёт слоя, и стоит она на двух окнах подряд.
|
||||
-- Первое спрашивает у каждого события, сколько прошло с предыдущего события
|
||||
-- той же куки в тот же день, и ставит единицу там, где пауза длиннее
|
||||
-- таймаута: это шов между визитами. Второе считает нарастающий итог этих
|
||||
-- единиц — номер визита внутри дня куки. Дальше визит становится обычной
|
||||
-- группировкой.
|
||||
--
|
||||
-- Границу суток окна не проверяют: она уже в разбиении PARTITION BY
|
||||
-- (client_id, event_date), поэтому визит через полночь не склеится и при
|
||||
-- короткой паузе. Так правило визита ложится в запрос целиком: одна кука,
|
||||
-- один день, паузы не длиннее таймаута (CONTEXT.md, «Визит»).
|
||||
--
|
||||
-- Первое событие куки за день сравнивается само с собой — третий аргумент
|
||||
-- lagInFrame. Паузы у него нет, шов ему не нужен, и номер первого визита
|
||||
-- получается нулём: номера различают визиты внутри дня куки, а не считают их
|
||||
-- от единицы.
|
||||
--
|
||||
-- Порядок в окнах полный — метка времени и event_id. Окна считаются двумя
|
||||
-- разными сортировками (видно в EXPLAIN PIPELINE), и при равных метках они
|
||||
-- вправе разойтись: шов достался бы одной строке, а нарастающий итог другой,
|
||||
-- и сессия разъехалась бы надвое.
|
||||
--
|
||||
-- Тридцать минут написаны числом, а не приехали параметром: это правило
|
||||
-- аналитики, а не константа мира. Генератор режет визиты тем же порогом;
|
||||
-- разойдись эти два числа — увидит счётная сверка с VisitID
|
||||
-- (docs/architecture/dds/session.md, «Сверка с эталоном»).
|
||||
|
||||
-- У первого события lagInFrame возвращает его собственное время: разница
|
||||
-- равна нулю, opens_visit = 0, и первый visit_no тоже равен нулю.
|
||||
-- Оба окна используют (event_time, event_id), чтобы при равном времени
|
||||
-- отметка начала и номер визита относились к одним событиям.
|
||||
-- Порог 1800 секунд — правило визита; он не передается параметром.
|
||||
ALTER TABLE dds.session_stage_rep ON CLUSTER clickstream_cluster DROP PARTITION ALL;
|
||||
|
||||
INSERT INTO dds.session_stage_dist
|
||||
@@ -93,8 +78,8 @@ WITH
|
||||
FROM seams
|
||||
)
|
||||
SELECT
|
||||
-- Чекан визита: кука и его начало. Время входит в хеш секундой эпохи,
|
||||
-- поэтому id держится за содержание сессии, а не за порядок сборки.
|
||||
-- Идентификатор зависит от посетителя и начала визита.
|
||||
-- Повторная сборка тех же событий дает тот же session_id.
|
||||
cityHash64(client_id, min(event_time)),
|
||||
client_id,
|
||||
event_date,
|
||||
@@ -107,8 +92,10 @@ SELECT
|
||||
countIf(event_type = 'purchase'),
|
||||
argMin(url, event_time),
|
||||
argMax(url, event_time),
|
||||
-- Атрибуция и паспорт куки берутся с первого события визита. У паспорта
|
||||
-- любое событие дало бы то же: устройство и город у куки постоянны.
|
||||
-- UTM, устройство и город берутся по минимальному event_time.
|
||||
-- Как и у страниц выше, при равном времени выбор между событиями
|
||||
-- не определен: event_id в эти argMin/argMax не входит.
|
||||
-- Устройство и город на стенде постоянны для одной куки.
|
||||
argMin(utm_source, event_time),
|
||||
argMin(utm_medium, event_time),
|
||||
argMin(utm_campaign, event_time),
|
||||
@@ -120,12 +107,7 @@ SELECT
|
||||
now64(3, 'UTC')
|
||||
FROM numbered
|
||||
GROUP BY client_id, event_date, visit_no
|
||||
-- Капкана parallel_distributed_insert_select здесь нет, и это выигрыш
|
||||
-- ко-локации: ключ шардирования у источника и цели один — cityHash64 от
|
||||
-- куки, — поэтому визит ляжет туда же, где лежали его события, исполнись
|
||||
-- вставка хоть на инициаторе, хоть локально на шардах. Заказу приходится
|
||||
-- называть ноль вслух: совпадение ключей у него случайное
|
||||
-- (sql/dds/order_rebuild.sql). Здесь выбор сделан обратный и он тоже
|
||||
-- осознанный: настройку не называть, чтобы не притворяться, будто её значение
|
||||
-- что-то решает.
|
||||
-- Источник и цель распределены по cityHash64(client_id): локальная вставка
|
||||
-- сохраняет нужную раскладку, parallel_distributed_insert_select менять
|
||||
-- не требуется. Перед заменой партиций ждем доставки строк на шарды.
|
||||
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: замена идёт по
|
||||
-- локальным таблицам, шард за шардом, атомарность пошардовая, день
|
||||
-- подставляет Jinja, потому что параметров ALTER … PARTITION не берёт.
|
||||
--
|
||||
-- Разница одна, и она про отказ. Несобранный день заказа вернуло бы в работу
|
||||
-- следующее окно; сессиям возвращать нечем, и упавшая замена оставляет дыру
|
||||
-- до тех пор, пока её не найдёт счёт хвоста (sql/dds/session_scope.sql).
|
||||
-- Дату из session_scope.sql подставляет Jinja: параметр ClickHouse в
|
||||
-- ALTER ... PARTITION здесь не поддерживается. Поиск дней следующего
|
||||
-- запуска не обнаружит неполный день, если часть его строк уже есть в DDS.
|
||||
ALTER TABLE dds.session_rep ON CLUSTER clickstream_cluster
|
||||
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.
|
||||
-- Позиция мира сама по себе не подтверждает доставку событий.
|
||||
--
|
||||
-- Правый край — последний прожитый день, и знает его только позиция мира.
|
||||
-- События живой день от прожитого не отличают: и посреди дня 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, «Готовность источника»).
|
||||
-- Точки с запятой в конце нет: драйвер добавляет FORMAT к этому запросу.
|
||||
WITH
|
||||
(SELECT min(EventDate) FROM ods.event_dist) AS first_source_day,
|
||||
first_source_day + ({position:UInt16} - 1) AS last_lived_day,
|
||||
-- Хвост считается от первой дыры, а не от последнего собранного дня:
|
||||
-- замены партиций падают порознь, и счёт по максимуму собранного дня
|
||||
-- дыру позади себя не заметил бы (доводы целиком — sql/dds/order_scope.sql).
|
||||
-- У сессий это единственный способ дыру найти: дышащего окна, которое
|
||||
-- пересобрало бы день заодно, здесь нет.
|
||||
--
|
||||
-- GLOBAL обязателен: обе таблицы распределённые, и обычный NOT IN сверял
|
||||
-- бы дни источника с локальной частью слоя на каждом шарде.
|
||||
-- minOrNull отличает «дыр нет» от даты по умолчанию.
|
||||
-- Ищем первую дату событий ODS, за которую еще нет сессий DDS.
|
||||
-- Поиск после max(session_date) пропустил бы более раннюю потерянную дату.
|
||||
-- Если за день есть хотя бы одна сессия, он уже не считается пропущенным.
|
||||
-- GLOBAL берет даты сессий со всех шардов.
|
||||
-- minOrNull возвращает NULL, если пропусков нет.
|
||||
(
|
||||
SELECT minOrNull(day)
|
||||
FROM (SELECT DISTINCT EventDate AS day FROM ods.event_dist)
|
||||
WHERE day GLOBAL NOT IN (SELECT session_date FROM dds.session_dist)
|
||||
) AS first_missing_day
|
||||
SELECT
|
||||
-- Живой день в слое отсутствует всегда, и работой это не считается:
|
||||
-- дырой он станет, только когда мир его проживёт.
|
||||
-- Отсутствующий день попадет в сборку только после его завершения.
|
||||
if(first_missing_day <= last_lived_day, first_missing_day, NULL) AS first_day,
|
||||
last_lived_day AS last_day
|
||||
-- FINAL не нужен: повтор доставки не двигает ни край оси, ни набор дней.
|
||||
-- FINAL не нужен: повтор доставки не меняет начало календаря и набор дат.
|
||||
WHERE (SELECT count() FROM ods.event_dist) > 0
|
||||
@@ -1,7 +1,10 @@
|
||||
-- DM: сборка выручки в донор перед заменой дневных партиций.
|
||||
-- DROP PARTITION ALL очищает локальные таблицы на каждом узле; причина не
|
||||
-- применять TRUNCATE разобрана в соседнем sql/dds/order_rebuild.sql.
|
||||
|
||||
-- DM: расчет выручки за дни от first_day до last_day включительно.
|
||||
-- DROP PARTITION ALL очищает промежуточную revenue_daily_stage_rep.
|
||||
-- Затем 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
|
||||
DROP PARTITION ALL;
|
||||
|
||||
@@ -22,8 +25,8 @@ SELECT
|
||||
orders,
|
||||
units,
|
||||
CAST(revenue, 'Decimal(18, 2)'),
|
||||
-- Обычное деление Decimal усекает доли копейки вместо округления;
|
||||
-- запас масштаба сохраняет их до денежного результата.
|
||||
-- Делим с шестью знаками после запятой, затем округляем до копеек.
|
||||
-- Так дробная часть сохраняется до округления среднего чека.
|
||||
CAST(
|
||||
round(divideDecimal(revenue, toDecimal128(orders, 0), 6), 2),
|
||||
'Decimal(18, 2)'
|
||||
@@ -49,7 +52,8 @@ FROM
|
||||
report_date,
|
||||
product_category
|
||||
)
|
||||
-- Источник лежит по заказу, цель — по категории. Ноль заставляет инициатор
|
||||
-- слить частичные агрегаты и разложить готовые строки по ключу цели.
|
||||
-- Источник распределен по заказу, цель — по категории. Значение 0 оставляет
|
||||
-- итоговую агрегацию и распределение по ключу цели на ноде, принявшей запрос.
|
||||
-- distributed_foreground_insert = 1 ждет доставки строк перед заменой.
|
||||
SETTINGS distributed_foreground_insert = 1,
|
||||
parallel_distributed_insert_select = 0;
|
||||
@@ -1,8 +1,9 @@
|
||||
-- DM: подмена одного дня выручки готовой партицией донора.
|
||||
--
|
||||
-- День приходит из результата revenue_daily_scope.sql, а не от пользователя.
|
||||
-- Jinja нужна потому, что ClickHouse не подставляет параметр в PARTITION.
|
||||
-- Замена идет на локальной таблице ON CLUSTER: граница атомарности и повтор
|
||||
-- разобраны на таком же шаге в sql/dds/order_replace.sql.
|
||||
-- DM: заменяем раздел revenue_daily_rep за выбранный день готовым разделом
|
||||
-- промежуточной revenue_daily_stage_rep. Раздел таблицы называется партицией.
|
||||
-- Дату из revenue_daily_scope.sql подставляет Jinja: параметр ClickHouse
|
||||
-- в ALTER ... PARTITION здесь не поддерживается.
|
||||
-- ON CLUSTER выполняет замену на локальных таблицах каждого шарда.
|
||||
-- На одном шарде раздел меняется целиком; общей транзакции для кластера нет.
|
||||
-- Повтор с теми же строками промежуточной таблицы не добавляет копий в основной.
|
||||
ALTER TABLE dm.revenue_daily_rep ON CLUSTER clickstream_cluster
|
||||
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 называет дни, состояние которых источник еще может
|
||||
-- изменить. Сам агрегат читает только DDS; ODS здесь нужен лишь для границ.
|
||||
-- Первый запуск и повтор после частичного отказа расширяют левую границу до
|
||||
-- первого дня оплаченных заказов, которого еще нет в витрине.
|
||||
-- Почему хвост начинается с первого пропуска и чем за это платит ODS,
|
||||
-- разобрано в sql/dds/order_scope.sql.
|
||||
--
|
||||
-- Файл кончается без точки с запятой: даг читает его одним запросом и ждет
|
||||
-- одну строку с границами.
|
||||
-- Точки с запятой в конце нет: драйвер добавляет FORMAT к этому запросу.
|
||||
WITH
|
||||
(SELECT max(snapshot_date) FROM ods.order_dist) AS last_snapshot,
|
||||
(
|
||||
@@ -24,8 +21,7 @@ WITH
|
||||
(
|
||||
SELECT minOrNull(report_date)
|
||||
FROM (SELECT DISTINCT order_date AS report_date FROM dds.order_v WHERE status = 'paid')
|
||||
-- Правая сторона тоже распределенная: GLOBAL собирает множество дней
|
||||
-- на инициаторе и не превращает запрос в запрещенное двойное чтение.
|
||||
-- GLOBAL собирает список дней витрины со всех шардов.
|
||||
WHERE report_date GLOBAL NOT IN (
|
||||
SELECT report_date FROM dm.revenue_daily_dist
|
||||
)
|
||||
@@ -33,7 +29,7 @@ WITH
|
||||
SELECT
|
||||
least(window_start, coalesce(first_missing_day, window_start)) AS first_day,
|
||||
window_end AS last_day
|
||||
-- Без ODS неизвестно окно. Без готового DDS донор останется пустым, а замена
|
||||
-- сотрет уже собранные дни выручки. Поэтому для прогона нужны оба источника.
|
||||
-- Для выбора диапазона нужны заказы в ODS, для расчета — в DDS.
|
||||
-- Эта проверка наличия строк не подтверждает полноту источников.
|
||||
WHERE (SELECT count() FROM dds.order_v) > 0
|
||||
AND (SELECT count() FROM ods.order_dist) > 0
|
||||
Reference in new issue
Block a user