docs(ingestion): упрощены объяснения источников и приема данных
Зачем: помочь начинающему инженеру проследить событие и версию заказа от генератора до хранилища, включая ошибки и повторные поступления. Что: обновлены вводная генератора, справочники приема и модели заказа, разделы хранилища и комментарии SQL и дага. Сохранены технические условия; исполняемый код, схемы и поведение стенда не изменены. Проверка: make lint, git diff --check, 142 локальные ссылки и справка CLI. SQL без комментариев и Python AST без строк документации совпали с исходными. Генерация данных и сброс состояния не выполнялись. Refs #250
This commit is contained in:
1 parent
83434102d5
commit
75f28fe99f
13 files changed
+663
-974
No files matched your search
+86
-107
@@ -1,134 +1,113 @@
|
||||
# Модель заказа в DDS
|
||||
|
||||
Учебный результат: менти проектирует бизнес-сущность поверх готового ODS —
|
||||
выбирает зерно, состав строки и способ наполнения — и видит идемпотентность
|
||||
трансформации, устроенную иначе, чем в приёме: заменой партиции, а не
|
||||
версиями.
|
||||
DDS готовит заказ для анализа: выбирает текущее состояние из ODS,
|
||||
разбирает позиции и переводит время в часовой пояс счетчика. Этот слой
|
||||
показывает, как собрать модель данных для витрин и повторить загрузку
|
||||
без удвоения заказов.
|
||||
|
||||
## Зерно
|
||||
|
||||
Одна строка — заказ в текущем состоянии, ключ — `order_id`. Модель отвечает
|
||||
на вопрос «что сейчас известно о заказе»; историю версий не хранит:
|
||||
читателя у истории нет ни в одной витрине, а ODS её и не обещает —
|
||||
промежуточные версии съедают фоновые слияния. «Дыхание» выручки внутри окна
|
||||
K живёт на текущем состоянии само: пересборка дня меняет статусы заказов, и
|
||||
число дня меняется вместе с ними.
|
||||
Одна строка — один заказ в последнем известном состоянии, ключ — `order_id`.
|
||||
История изменений здесь не хранится. ODS тоже не служит ее архивом:
|
||||
промежуточные версии удаляются при фоновых слияниях.
|
||||
|
||||
## Состав строки
|
||||
|
||||
Деловые поля источника переезжают из `ods.order_v` без переименований —
|
||||
бэкенд уже говорит в snake_case: `order_id`, `user_id`, `status`, деньги в
|
||||
`Decimal` (`items_total`, `discount`, `delivery`, `total`).
|
||||
Поля заказа приходят из `ods.order_v` с именами источника: `order_id`,
|
||||
`user_id`, `status`, `items_total`, `discount`, `delivery`, `total`.
|
||||
Денежные колонки хранят рубли с копейками в `Decimal(18, 2)`.
|
||||
|
||||
Времена `created_at` и `updated_at` пересчитываются при загрузке в пояс
|
||||
счётчика: DDS обслуживает человека с дашбордом ([дока
|
||||
хранилища](../storage.md), «Часовые пояса»). Там же один раз считается
|
||||
**день заказа** — колонка `order_date`, по образцу `EventDate` у события:
|
||||
суточные срезы группируются по готовой дате и пояса не упоминают.
|
||||
`created_at` и `updated_at` переводятся из UTC в `Europe/Samara`, пояс
|
||||
счетчика сайта. Из `created_at` в этом поясе вычисляется `order_date` —
|
||||
день оформления заказа. Витрины группируют по готовой дате;
|
||||
правило времени общее для [хранилища](../storage.md#часовые-пояса).
|
||||
|
||||
Позиции разобраны из сырой строки `items` в три типизированных массива
|
||||
одной длины — артикул, количество, цена (`Decimal`). Это родная для
|
||||
ClickHouse денормализация: позиция не живёт без заказа и собственного ключа
|
||||
не имеет, а разворачивает массивы читатель — `ARRAY JOIN` в витрине
|
||||
выручки, с обогащением категорией через `dictGet`
|
||||
([мастер-спека](../../specs/2026-07-30-stand-v2-realism.md), раздел 7). Разбор
|
||||
JSON происходит один раз, на границе ODS → DDS, куда его и положила спека
|
||||
(раздел 2); записывать его параллельными колонками-массивами или `Nested` —
|
||||
выбор DDL, не модели.
|
||||
Строка `items` из ODS разбирается в три массива одной длины:
|
||||
|
||||
| Колонка | Что хранит |
|
||||
|---|---|
|
||||
| `item_sku` | Артикулы товаров |
|
||||
| `item_quantity` | Количество каждого товара, `UInt32` |
|
||||
| `item_price` | Цены за единицу в рублях, `Decimal(18, 2)` |
|
||||
|
||||
Элементы с одинаковым номером описывают одну позицию заказа. Витрина
|
||||
выручки разворачивает их через `ARRAY JOIN` и получает категорию товара
|
||||
из справочника через `dictGet`. Разбор JSON выполняется один раз
|
||||
при загрузке DDS.
|
||||
|
||||
ODS проверяет только то, что `items` — массив. Разбор в DDS не заменяет
|
||||
проверку его содержимого: например, некорректное количество может стать
|
||||
нулем. Проверку суммы позиций участник добавляет в
|
||||
[первом задании ступени 5](../../labs/5-1-order-checks.md).
|
||||
|
||||
## Движок и наполнение
|
||||
|
||||
`dds.order_rep` — `ReplicatedMergeTree` без колонки версии: партиция —
|
||||
`order_date` (день заказа неизменен), ключ сортировки — `order_id`.
|
||||
`dds.order_dist` шардирует по `cityHash64(order_id)`, тем же выражением, что
|
||||
ODS: это единственный устойчивый ключ сущности.
|
||||
`dds.order_rep` — таблица на `ReplicatedMergeTree`, без колонки версии.
|
||||
Партиция хранит один `order_date`, ключ сортировки — `order_id`.
|
||||
Запись через `dds.order_dist` распределяет заказы по `cityHash64(order_id)`,
|
||||
как в ODS. Для чтения служит `dds.order_v` с явным списком колонок
|
||||
над `dds.order_dist`; `FINAL` в этом слое не нужен.
|
||||
|
||||
Идемпотентность держит замена партиции, а не версии. Сбор дня — батч после
|
||||
прихода слепка: трансформация собирает партицию заново из `ods.order_v` и
|
||||
атомарно подменяет. Поэтому каждая строка физической таблицы — ровно один
|
||||
заказ, а чтению не нужен `FINAL`. Пока окно K живо, день «дышит» — статусы
|
||||
заказов меняются, и партиция дня пересобирается очередным запуском; на выходе
|
||||
из окна день замерзает.
|
||||
Даг `dds_transform` пересобирает выбранные дни из `ods.order_v`:
|
||||
|
||||
Публичная поверхность — `dds.order_v`: обычное представление с явным списком
|
||||
деловых и собственных служебных колонок над `dds.order_dist`, без `FINAL`.
|
||||
Роли объектов и доступ следуют общей [конвенции](../storage.md), «Имена
|
||||
объектов».
|
||||
1. [`order_scope.sql`](../../../sql/dds/order_scope.sql) определяет
|
||||
непрерывный отрезок `first_day`…`last_day`. Он включает дни заказов
|
||||
из последнего слепка и начинается раньше, если в DDS отсутствует
|
||||
день, который есть в ODS.
|
||||
2. [`order_rebuild.sql`](../../../sql/dds/order_rebuild.sql) очищает
|
||||
промежуточную таблицу `dds.order_stage_rep` и наполняет ее через
|
||||
`dds.order_stage_dist`. Это донор: в нем готовится результат,
|
||||
пока основная таблица остается доступной для чтения.
|
||||
3. [`order_replace.sql`](../../../sql/dds/order_replace.sql) заменяет
|
||||
каждую дневную партицию основной таблицы готовой партицией донора.
|
||||
|
||||
Механика запуска — где собирается партиция и как подменяется на кластере —
|
||||
сюда не входит: это устройство [дневного конвейера](../etl.md).
|
||||
Повтор при неизменном ODS дает те же деловые данные без дублей —
|
||||
это идемпотентность загрузки. Пока заказы дня входят в семидневное окно
|
||||
источника, их состояние может меняться, и день пересобирается.
|
||||
За пределами окна новые состояния не приходят, но пропущенный день
|
||||
можно восстановить из ODS. Границы пересборки берутся из данных;
|
||||
число семь в SQL хранилища не повторяется.
|
||||
|
||||
Замена атомарна внутри одного шарда: читатель видит прежнюю или новую
|
||||
партицию целиком. Замены на двух шардах независимы, поэтому между ними
|
||||
запрос через Distributed может увидеть смешанный результат.
|
||||
Донор остается до следующей сборки и доступен для диагностики.
|
||||
Порядок задач и повторы — в [описании дневного конвейера](../etl.md).
|
||||
|
||||
## Служебные колонки
|
||||
|
||||
Строка получает `_load_id` и `_load_ts` запуска трансформации, собравшего
|
||||
её партицию, — по общей конвенции хранилища. При замене партиции они
|
||||
одинаковы у всех строк дня и отвечают на вопрос «каким запуском и когда
|
||||
собран этот день».
|
||||
`_load_id` и `_load_ts` показывают, какой запуск DDS собрал строку
|
||||
и когда он выполнил вставку. Они одинаковы у строк одной сборки.
|
||||
При следующей пересборке эти метки обновятся, даже если заказ не изменился.
|
||||
|
||||
Ярлыки ODS (`snapshot_date`, `_load_id` приёма, `_load_ts` прибытия) в модель
|
||||
не переезжают. Они описывают наблюдение и доставку строки в ODS. Модель DDS
|
||||
хранит текущее деловое состояние заказа. В ODS `snapshot_date` задаёт версию и
|
||||
называет последний слепок, в котором источник показал заказ
|
||||
([ADR 0014](../../adr/0014-order-version-snapshot-date.md)). Дату
|
||||
наблюдения и координаты доставки читатель спрашивает у ODS по
|
||||
`order_id`. Правило слоя:
|
||||
**служебные колонки — координаты собственной загрузки слоя; чужие координаты
|
||||
спрашиваются у их слоя по ключу сущности**.
|
||||
`snapshot_date` и метки приема ODS в DDS не переносятся: они описывают
|
||||
получение сообщения источника. Их можно найти в ODS по `order_id`.
|
||||
В каждом слое служебные колонки относятся к его собственной загрузке.
|
||||
|
||||
Происшествие качества данных расследуется цепочкой, где каждый шаг спрашивает
|
||||
свой слой на его языке: по `_load_id` строки DDS видно, какой запуск собрал
|
||||
день; по `order_id` ODS показывает физические версии и брак — там свой
|
||||
`_load_id` приёма связывает годные строки, ошибки и данные STG; дальше читают
|
||||
STG, пока данные живы. Окно расследования задано сроками хранения: сырые данные
|
||||
— трое суток, брак — месяц ([дока хранилища](../storage.md)).
|
||||
При поиске ошибки начните с `_load_id` в DDS: он указывает на запуск
|
||||
сборки. По `order_id` найдите версии заказа в ODS, а по их `_load_id` —
|
||||
соответствующие сырые сообщения в STG и ошибки того же приема.
|
||||
В таблице ошибок `order_id` может быть не разобран, поэтому нужное
|
||||
сообщение ищут по `raw` и координатам Kafka.
|
||||
Сырые сообщения хранятся около трех реальных суток, ошибки — месяц;
|
||||
уточнения к срокам — в [справочнике хранилища](../storage.md#срок-жизни-сырых-данных).
|
||||
|
||||
## Читатели
|
||||
|
||||
- Витрина выручки `dm.revenue_daily_v`: заказы в `paid`, `ARRAY JOIN`
|
||||
позиций, категория через `dictGet` (спека, раздел 7).
|
||||
- Сверка `dm.purchase_vs_orders_v`: `order_id`, `order_date`, `items_total`,
|
||||
`status`; соединение — GLOBAL JOIN с обеими сторонами-подзапросами
|
||||
([исследование соединений](../../research/2026-08-22-distributed-join.md)).
|
||||
- Сборка карты идентичностей: пара `order_id` ↔ `user_id`, источник —
|
||||
`dds.order_v`.
|
||||
|
||||
## Не входит
|
||||
|
||||
- Механика запуска трансформации и замены партиций на кластере —
|
||||
[дневной конвейер](../etl.md).
|
||||
- Контур проверок качества (даг DQ, `dm.dq_summary`) — отдельный разговор
|
||||
этапа 4.
|
||||
- Инкрементальное наполнение вместо пересборки — техдолг #2.
|
||||
|
||||
## Отклонённые варианты
|
||||
|
||||
- **Одно динамическое представление вместо физической таблицы.** Рамка стенда —
|
||||
показать модель данных, которую хранилище готовит потребителям; урок
|
||||
«слой — контракт, а не копия» уже закреплён за `dds.event_v`, а разбор
|
||||
позиций и пересчёт времён при каждом чтении собирали бы модель заново на
|
||||
лету. Публичное `dds.order_v` поверх готовой физической модели этому доводу
|
||||
не противоречит: оно ничего заново не вычисляет.
|
||||
- **`ReplacingMergeTree` ещё раз.** Повторил бы механику ODS вторым разом:
|
||||
физически хранил бы устаревшие состояния, вернул бы `FINAL`
|
||||
и обёртку-представление — а урок версий менти уже отыграл.
|
||||
- **Отдельная сущность `dds.order_item`.** Вторая таблица, второй вопрос
|
||||
шардирования и `JOIN` там, где хватает `ARRAY JOIN`; нормализация из
|
||||
реляционного мира, а не модель под колоночное хранилище. `JOIN` менти
|
||||
увидит в сверке, работу с массивами — только здесь.
|
||||
- **Зерно «версия заказа» (история).** Читателя нет; ODS историю не
|
||||
обещает, а мир при нужде переигрывается генератором.
|
||||
- **Перенос ярлыков ODS** — см. «Служебные колонки».
|
||||
- **Без модели вовсе: витрины читают `ods.order_v` напрямую.** Разбор JSON
|
||||
и пересчёт времён расползлись бы по витринам, а модель данных — то, ради
|
||||
чего сущность и строится, — не была бы показана.
|
||||
- `dm.revenue_daily_v` считает выручку по заказам со статусом `paid`,
|
||||
разворачивает позиции и дополняет их категорией товара.
|
||||
- `dm.purchase_vs_orders_v` сопоставляет заказ с покупкой на сайте
|
||||
по `order_id`, используя `order_date`, `items_total` и `status`.
|
||||
- `dds.identity_map` связывает идентификаторы посетителей с `user_id`
|
||||
через покупки и заказы.
|
||||
|
||||
## Связанные решения
|
||||
|
||||
- [ADR 0010](../../adr/0010-order-versions-in-ods.md) — версии заказов в
|
||||
ODS, устойчивая поверхность `ods.order_v`.
|
||||
- [Приём заказов](../orders/ingestion.md) — граница строгого приёма и
|
||||
четыре координаты строки ODS.
|
||||
- [Проталкивание условий сквозь
|
||||
представления](../../research/2026-08-22-view-predicate-pushdown.md) —
|
||||
резолюция #128 той же карты: цена `FINAL` и цепочек представлений, фон, на
|
||||
котором здесь выбрана физическая таблица.
|
||||
- [ADR 0010](../../adr/0010-order-versions-in-ods.md) — версии заказов в ODS
|
||||
и чтение через `ods.order_v`.
|
||||
- [Прием заказов](../orders/ingestion.md) — проверки формата и выбор
|
||||
актуальной версии.
|
||||
- [Исследование соединений](../../research/2026-08-22-distributed-join.md) —
|
||||
почему сверка распределенных источников использует `GLOBAL JOIN`.
|
||||
- [Проталкивание условий сквозь представления](../../research/2026-08-22-view-predicate-pushdown.md) —
|
||||
как фильтр запроса влияет на объем чтения ODS.
|
||||
Reference in new issue
Block a user