Files
clickstream-data-platform/docs/architecture/dds/order.md
T
ddadminandClaude Fable 5 7972847601 docs(dds): спроектирована модель заказа в DDS и заведён набор доков слоя
- Зачем:
  - развилка #130 карты #127: заказу нужен объект в DDS — зерно, состав,
    движок и судьба служебных колонок; резолюции этапа 4 нужен дом в git.
- Что:
  - создан набор docs/architecture/dds/ по ADR 0011: README с картой
    модели и правилами слоя, order.md с моделью заказа — зерно «заказ в
    текущем состоянии», позиции массивами, MergeTree с заменой партиций,
    свои _load_id/_load_ts вместо ярлыков ODS (закрывает наследство #85);
  - указатели на модель добавлены в orders/README.md, orders/ingestion.md
    и storage.md.
- Проверка:
  - только документы; относительные ссылки сверены с деревом docs/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 12:03:26 -04:00

10 KiB

Модель заказа в DDS

Учебный результат: менти проектирует бизнес-сущность поверх готового ODS — выбирает зерно, состав строки и способ наполнения — и видит идемпотентность трансформации, устроенную иначе, чем в приёме: заменой партиции, а не версиями.

Зерно

Одна строка — заказ в текущем состоянии, ключ — order_id. Модель отвечает на вопрос «что сейчас известно о заказе»; историю версий не хранит: читателя у истории нет ни в одной витрине, а ODS её и не обещает — промежуточные версии съедают фоновые слияния. «Дыхание» выручки внутри окна K живёт на текущем состоянии само: пересборка дня меняет статусы заказов, и число дня меняется вместе с ними.

Состав строки

Деловые поля источника переезжают из ods.order_v без переименований — бэкенд уже говорит в snake_case: order_id, user_id, status, деньги в Decimal (items_total, discount, delivery, total).

Времена created_at и updated_at пересчитываются при загрузке в пояс счётчика: DDS обслуживает человека с дашбордом (дока хранилища, «Часовые пояса»). Там же один раз считается день заказа — колонка order_date, по образцу EventDate у события: суточные срезы группируются по готовой дате и пояса не упоминают.

Позиции разобраны из сырой строки items в три типизированных массива одной длины — артикул, количество, цена (Decimal). Это родная для ClickHouse денормализация: позиция не живёт без заказа и собственного ключа не имеет, а разворачивает массивы читатель — ARRAY JOIN в витрине выручки, с обогащением категорией через dictGet (спека, раздел 7). Разбор JSON происходит один раз, на границе ODS → DDS, куда его и положила спека (раздел 2); записывать его параллельными колонками-массивами или Nested — выбор DDL, не модели.

Движок и наполнение

MergeTree без колонки версии: партиция — order_date (день заказа неизменен), ключ сортировки — order_id, шардирование распределённой таблицы — cityHash64(order_id), тем же выражением, что у ODS: это единственный устойчивый ключ сущности.

Идемпотентность держит замена партиции, а не версии. Сбор дня — батч после прихода слепка: трансформация собирает партицию заново из ods.order_v и атомарно подменяет. Поэтому каждая строка таблицы — ровно один заказ, и таблица сама служит поверхностью чтения слоя: голый SELECT, без FINAL и без обёртки-представления. Пока окно K живо, день «дышит» — статусы заказов меняются, и партиция дня пересобирается очередным запуском; на выходе из окна день замерзает.

Механика запуска — где собирается партиция и как подменяется на кластере — сюда не входит: это устройство трансформаций (#134, этап 5).

Служебные колонки

Строка получает _load_id и _load_ts запуска трансформации, собравшего её партицию, — по общей конвенции хранилища. При замене партиции они одинаковы у всех строк дня и отвечают на вопрос «каким запуском и когда собран этот день».

Ярлыки ODS — snapshot_date, _load_id приёма, _load_ts прибытия — в модель не переезжают. Версию заказа они не задают, а физически выжившие их значения — жребий: у строк с равным updated_at деловые поля совпадают побайтово, и какая переживёт схлопывание — произвол (замер — приём заказов, «Что проверено»). Перенесённый ярлык выглядел бы фактом, оставаясь жребием; лживая координата хуже отсутствующей. Отсюда правило слоя: служебные колонки — координаты собственной загрузки слоя; чужие координаты спрашиваются у их слоя по ключу сущности. Этим закрыто наследство #85: читатель у чужих ярлыков выше ODS не нашёлся и при названных читателях модели.

Происшествие качества данных расследуется цепочкой, где каждый шаг спрашивает свой слой на его языке: по _load_id строки DDS видно, какой запуск собрал день; по order_id ODS показывает физические версии и брак — там свой _load_id приёма связывает годные строки, ошибки и сырьё; дальше сырьё STG, пока живо. Окно расследования задано сроками хранения: сырьё — трое суток, брак — месяц (дока хранилища).

Читатели

  • Витрина выручки dm.revenue_daily_v: заказы в paid, ARRAY JOIN позиций, категория через dictGet (спека, раздел 7).
  • Сверка dm.purchase_vs_orders_v: order_id, items_total, status; соединение — GLOBAL JOIN с обеими сторонами-подзапросами (исследование соединений).
  • Сборка dds.identity_map: пара order_id ↔ user_id.

Не входит

  • Механика запуска трансформации и замены партиций на кластере — #134 и этап 5.
  • Контур проверок качества (даг DQ, dm.dq_summary) — отдельный разговор этапа 4.
  • Инкрементальное наполнение вместо пересборки — техдолг #2.

Отклонённые варианты

  • Представление-контракт вместо физической таблицы. Рамка стенда — показать модель данных, которую хранилище готовит потребителям; урок «слой — контракт, а не копия» уже закреплён за dds.event_v, а разбор позиций и пересчёт времён при каждом чтении собирали бы модель заново на лету.
  • ReplacingMergeTree(updated_at) ещё раз. Повторил бы механику ODS вторым разом: физически хранил бы устаревшие состояния, вернул бы FINAL и обёртку-представление — а урок версий менти уже отыграл.
  • Отдельная сущность dds.order_item. Вторая таблица, второй вопрос шардирования и JOIN там, где хватает ARRAY JOIN; нормализация из реляционного мира, а не модель под колоночное хранилище. JOIN менти увидит в сверке, работу с массивами — только здесь.
  • Зерно «версия заказа» (история). Читателя нет; ODS историю не обещает, а мир при нужде переигрывается генератором.
  • Перенос ярлыков ODS — см. «Служебные колонки».
  • Без модели вовсе: витрины читают ods.order_v напрямую. Разбор JSON и пересчёт времён расползлись бы по витринам, а модель данных — то, ради чего сущность и строится, — не была бы показана.

Связанные решения