feat(dm): добавлена сверка покупок с заказами
- Зачем: - показать расхождения трекера и бэкенда на зерне заказа - сохранить отложенные покупки наблюдаемыми до следующего слепка - Что: - добавлены таблицы, представление и пересборка purchase_vs_orders - сумма покупки нормализована в Decimal на границе ODS - обновлены договоры и указатели на механику пересборки - Проверка: - `make config-test`, `make lint` - `cd generator && make docs lint typecheck test` - `make rebuild-storage`, явные `dds_transform` и `dm_transform` - `make smoke`, `make check-clickhouse`, `make check-services`
This commit is contained in:
1 parent
fb00bbe4c3
commit
20071c565f
15 files changed
+391
-38
No files matched your search
@@ -45,6 +45,11 @@
|
||||
своё имя здесь создало бы третий синоним. `client_timezone` тоже остаётся —
|
||||
это атрибут посетителя, а не линза времени.
|
||||
|
||||
`purchase_revenue` сохраняет смысл поля источника. Тип денег нормализуется в
|
||||
ODS; точный тип и проверка описаны в разделе
|
||||
[«Актуальные события»](../storage.md#актуальные-события). DDS только
|
||||
переименовывает уже нормализованную колонку.
|
||||
|
||||
## Расшифровки
|
||||
|
||||
Одна: `device_category` — коды 1–4 в строки `desktop` / `mobile` /
|
||||
|
||||
+32
-2
@@ -70,9 +70,17 @@
|
||||
знать, какие связки новые, а полный пересчёт карты этот след затирает —
|
||||
восстановление следа оставлено лабе повышенной сложности (issue #149).
|
||||
|
||||
У `purchase_vs_orders` два источника и одна непрерывная пересборка. Заказная
|
||||
сторона задает изменяемое окно последним принятым слепком ODS, событийная —
|
||||
последний прожитый день с `purchase`. Левая граница возвращает первую дыру и
|
||||
самый ранний день с `awaiting_order`: предварительная строка уже есть в
|
||||
витрине, поэтому поиск отсутствующего дня ее не заметит. Правая граница
|
||||
доходит до более позднего из двух хвостов. Так один прогон догоняет пакетный
|
||||
разгон длиннее K и не оставляет старое ожидание замороженным.
|
||||
|
||||
## Витрины стенда
|
||||
|
||||
Оба публичных представления перечисляют деловые колонки и координаты запуска
|
||||
Публичные представления перечисляют деловые колонки и координаты запуска
|
||||
`_load_id`, `_load_ts` явно. Новая колонка физической таблицы сама в договор не
|
||||
попадет.
|
||||
|
||||
@@ -98,6 +106,29 @@
|
||||
шесть категорий стартового мира близки по объему. Более сложный составной ключ
|
||||
распределил бы эту маленькую таблицу без нового урока.
|
||||
|
||||
### `dm.purchase_vs_orders_v`
|
||||
|
||||
Зерно — заказ. Покупки из `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)`. Классы проверяются по порядку:
|
||||
`awaiting_order` для покупки без заказа, затем `cancelled`, `lost_event`,
|
||||
`duplicate_event`, `amount_delta`, `match`. Поэтому отмененный заказ без
|
||||
события остается отмененным, а не потерянным.
|
||||
|
||||
Витрина нарезана по `order_day`, цель и донор шардируются по
|
||||
`cityHash64(order_id)`. Диапазон собирается в доноре одной вставкой и
|
||||
заменяется по дням. Первичная сборка охватывает всю историю обоих источников;
|
||||
следующие прогоны идут по двустороннему ритму, описанному выше.
|
||||
|
||||
### `dm.daily_traffic_v`
|
||||
|
||||
В строке один день. `visitors` считает разные `client_id` из
|
||||
@@ -163,7 +194,6 @@ Airflow. Партиция — день данных; прогон звена DQ
|
||||
- Ключи шардирования остальных материализованных витрин — вместе с их
|
||||
DDL; правило партиции-донора — в [доке хранилища](storage.md).
|
||||
- Тексты учебных заданий и точный список проверок обоих контуров.
|
||||
- Механика сверки A+C в SQL — рамка в спеке, разделы 4 и 7.
|
||||
|
||||
## Отклонённые варианты
|
||||
|
||||
|
||||
@@ -221,6 +221,13 @@ ODS и в таблицу ошибок. Нужен ли он выше ODS, реш
|
||||
`dds.event_v` отвечает за другое: даёт событию язык бизнес-модели, в том
|
||||
числе snake_case-имена и расшифровку кодов.
|
||||
|
||||
Типы ODS обычно повторяют контракт выгрузки. Денежный `purchaseRevenue` —
|
||||
исключение: источник объявляет `Array(Float64)`, а материализованное
|
||||
представление ODS разбирает числа сырого JSON сразу в
|
||||
`Array(Decimal(18, 2))`. DDS наследует этот тип без нового преобразования.
|
||||
Прямой разбор подтвержден документацией ClickHouse через Context7 и запросом
|
||||
к ClickHouse 26.3; сравнение с сырьем подтвердило: копейки всех покупок целы.
|
||||
|
||||
## Версии заказов
|
||||
|
||||
`ods.order_rep` хранит принятые версии в
|
||||
@@ -483,9 +490,9 @@ kafka_offset)`: смотрят такую таблицу от класса, а
|
||||
административная операция, роль `etl` права на неё не получает.
|
||||
|
||||
Деньги каталога живут в трёх единицах: в CSV — целые копейки, словарь отдаёт
|
||||
`Decimal(18, 2)`, `productPrice` события — целые рубли. Разрыв намеренный, на
|
||||
нём стоит урок про `Float64` ([описание
|
||||
выгрузки](../formats/clickstream-event.md)).
|
||||
`Decimal(18, 2)`, `productPrice` события — целые рубли. Поэтому точную сумму
|
||||
заказа берут из `purchaseRevenue`, а не складывают округленные цены товаров
|
||||
([описание выгрузки](../formats/clickstream-event.md)).
|
||||
|
||||
Устройство обоих объектов — почему такой движок, такая форма пути, такой
|
||||
пользователь и такое окно — расписано в
|
||||
@@ -523,7 +530,7 @@ SQL-файлы исполняет штатный `SQLExecuteQueryOperator`. Пр
|
||||
| `40-stg-views.sql` | матвью приёма: чтец в сырьё |
|
||||
| `45-dds-tables.sql` | физические модели заказа, сессии и карты идентичностей, доноры партиций |
|
||||
| `50-dds-views.sql` | публичные договоры сущностей DDS |
|
||||
| `55-dm-tables.sql` | физические витрины выручки и трафика, донор партиций выручки |
|
||||
| `55-dm-tables.sql` | физические витрины выручки, сверки и трафика, доноры дневных партиций |
|
||||
| `60-dm-views.sql` | публичные договоры витрин DM |
|
||||
|
||||
Справочники идут сразу за базами: зона `dic` не зависит ни от одного слоя, и
|
||||
@@ -594,6 +601,9 @@ SQL-файлы исполняет штатный `SQLExecuteQueryOperator`. Пр
|
||||
| `dm` | `dm.revenue_daily_rep` / `_dist` | дневная выручка по категориям |
|
||||
| `dm` | `dm.revenue_daily_stage_rep` / `_dist` | таблица-двойник, донор дневных партиций выручки |
|
||||
| `dm` | `dm.revenue_daily_v` | публичный договор дневной выручки |
|
||||
| `dm` | `dm.purchase_vs_orders_rep` / `_dist` | сверка покупок с заказами, партиция — день заказа |
|
||||
| `dm` | `dm.purchase_vs_orders_stage_rep` / `_dist` | донор дневных партиций сверки |
|
||||
| `dm` | `dm.purchase_vs_orders_v` | публичный договор сверки с `NULL` для отсутствующей стороны |
|
||||
| `dm` | `dm.daily_traffic_rep` / `_dist` | версии дневных счетчиков трафика |
|
||||
| `dm` | `dm.daily_traffic_v` | точные текущие счетчики трафика с `FINAL` |
|
||||
| `dic` | `dic.products_file` | чтец CSV-каталога, общего с генератором |
|
||||
@@ -605,8 +615,8 @@ SQL-файлы исполняет штатный `SQLExecuteQueryOperator`. Пр
|
||||
|
||||
В слое DDS построены событие, заказ, сессия и карта идентичностей. Событие
|
||||
остаётся представлением над ODS; заказ и сессия гасят повтор заменой партиции;
|
||||
карта пишет полный набор пар в версионную таблицу. В DM выручка заменяет
|
||||
партиции изменяемых дней, а трафик пишет полную историю новой версией.
|
||||
карта пишет полный набор пар в версионную таблицу. В DM выручка и сверка
|
||||
заменяют партиции изменяемых дней, а трафик пишет полную историю новой версией.
|
||||
|
||||
## Что проверено
|
||||
|
||||
@@ -849,6 +859,10 @@ Kafka с пустым значением (ноль байт) и запись-н
|
||||
промежуточного `toDecimal64`. Проверено на живом слепке; честность разбора
|
||||
сторожит инвариант бэкенда — сумма «цена × количество» совпала с `items_total`
|
||||
у всех 1662 заказов.
|
||||
- **`JSONExtract` читает `Decimal` и из числа JSON.** Массив
|
||||
`purchaseRevenue`, объявленный источником как `Array(Float64)`, разбирается
|
||||
сразу в `Array(Decimal(18, 2))`. Синтаксис подтвержден через Context7 и на
|
||||
ClickHouse 26.3; все суммы после ODS совпали с сырьем до копейки.
|
||||
|
||||
**Сказано по памяти, проверки нет.** Группа пуста: оба утверждения, ждавшие
|
||||
матвью разбора, закрыты опытами при исполнении #43 и переехали выше.
|
||||
@@ -11,10 +11,12 @@
|
||||
по элементу на товар. Отдельной сущности «визит» в выгрузке нет — визиты
|
||||
собирают на стороне хранилища, а `VisitID` дан как эталон для самопроверки.
|
||||
|
||||
Имена и типы колонок — стороны источника. Хранилище принимает их как есть и
|
||||
нормализует у себя: то же имя в нашем стиле ждёт в столбце «Нормализованное
|
||||
имя». Это имя источника, приведённое к snake_case, а не имя атрибута в
|
||||
модели данных: слой DDS складывает свою модель и называет атрибуты по ней.
|
||||
Имена и типы колонок описывают сторону источника. Обычно ODS принимает их без
|
||||
изменений. Исключение — `purchaseRevenue`: источник объявляет Float64, а ODS
|
||||
хранит деньги в Decimal. Точный локальный тип назван в документации
|
||||
хранилища. Имя в нашем стиле ждет в столбце «Нормализованное имя». Это имя
|
||||
источника, приведенное к snake_case, а не имя атрибута в модели данных: слой
|
||||
DDS складывает свою модель и называет атрибуты по ней.
|
||||
Столбец «Тип numpy» показывает, чем колонка представлена внутри генератора;
|
||||
у массивов это тип элемента. Номер — место колонки в выгрузке: порядок задан
|
||||
контрактом.
|
||||
@@ -90,7 +92,7 @@
|
||||
| № | Колонка | Тип ClickHouse | Тип numpy | Нормализованное имя | Комментарий |
|
||||
|---|---|---|---|---|---|
|
||||
| 37 | `purchaseID` | `Array(String)` | `object` | `purchase_id` | номер заказа; у события purchase — один элемент |
|
||||
| 38 | `purchaseRevenue` | `Array(Float64)` | `float64` | `purchase_revenue` | выручка заказа глазами клиента; Float64, как у Метрики — на этом держится урок о расхождениях с бэкендом |
|
||||
| 38 | `purchaseRevenue` | `Array(Float64)` | `float64` | `purchase_revenue` | выручка заказа глазами клиента; источник объявляет Float64, ODS хранит точный Decimal |
|
||||
| 39 | `purchaseCurrency` | `Array(String)` | `object` | `purchase_currency` | валюта заказа |
|
||||
| 40 | `purchaseCoupon` | `Array(String)` | `object` | `purchase_coupon` | купон заказа, если был применён |
|
||||
| 41 | `productID` | `Array(String)` | `object` | `product_id` | id товаров события |
|
||||
|
||||
@@ -138,8 +138,9 @@ Ecommerce (заполнены только у торговых событий):
|
||||
| `ecommerce` | String — сырой JSON события, как отдаёт Метрика (кандидат будущей лабы: сырое против разобранного) |
|
||||
|
||||
`add_to_cart` несёт массивы `product*` с одним товаром; `purchase` — состав
|
||||
заказа и блок `purchase*`. Выручка у клиента — во Float64, как у Метрики:
|
||||
это не недосмотр, а часть урока о расхождениях (см. раздел 4).
|
||||
заказа и блок `purchase*`. Выручка приходит JSON-числом Float64, как у
|
||||
Метрики. ODS один раз нормализует деньги в `Array(Decimal(18, 2))`, не меняя
|
||||
формат источника; DDS наследует тип хранения без нового вычисления.
|
||||
|
||||
### 1.3 Ключи и движки
|
||||
|
||||
@@ -210,9 +211,9 @@ Ecommerce (заполнены только у торговых событий):
|
||||
ключей совпадает с порядком полей в таблице контракта, у позиции — `sku`,
|
||||
`qty`, `price`: так байты воспроизводимы без сортировки ключей. Разница с
|
||||
кликстримом намеренна: там значения `purchaseRevenue` приезжают JSON-числами
|
||||
и разбираются как `Array(Float64)`, а бэкенд передаёт деньги строками для
|
||||
точного `Decimal`. Это показывает расхождение представлений денег в двух
|
||||
источниках.
|
||||
Float64, а бэкенд передает деньги строками. На границе ODS оба источника
|
||||
становятся `Decimal(18, 2)`: клиентское число разбирается сразу, без
|
||||
промежуточного Float64 в хранилище.
|
||||
Запись собирает явная функция существующего канонического сериализатора:
|
||||
один словарь с вложенным списком и один `orjson.dumps`, без универсального
|
||||
слоя кодеков.
|
||||
@@ -285,7 +286,7 @@ CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `cate
|
||||
под именем `declared_*` там, где атрибуция без трекера невозможна (UTM);
|
||||
слово `declared` в имени — сигнал «это заявка клиента, не деньги
|
||||
отчётности». Оно выучивается на конфликте: суммы не сойдутся, менти сам
|
||||
раскопает почему (Float64 против Decimal, промокод, доставка, отмены).
|
||||
раскопает почему (состав позиций, промокод, доставка, отмены).
|
||||
|
||||
## 5. Анонимность и склейка идентичностей
|
||||
|
||||
|
||||
Reference in new issue
Block a user