feat(dds): добавлена модель заказа и пересборка окна заменой партиций
Зачем: - Дать слою первую физическую сущность и показать идемпотентность, устроенную иначе, чем в приёме: заменой партиции, а не колонкой версии. - Родить механику дневной пересборки на кластере — образец для сессий и витрин. Что: - Добавлены dds.order_rep/_dist, двойник-донор dds.order_stage_rep/_dist и публичный договор dds.order_v с разбором позиций в три массива. - Добавлены запросы объёма прогона, сборки в двойник и замены партиции по дню. - Добавлен даг dds_transform: группа задач order, замена — размноженной задачей. - Окно изменяемости K пришло на сторону стенда якорем compose; владелец числа остаётся у генератора. - Объём считается от позднейшего дня заказа и первого недостающего дня: ярлыки доставки ODS для этого непригодны, а максимум собранного дня прячет дыры. - Записано, чего конвейер о себе не знает, и адресовано контуру DQ. Проверка: - make lint - make smoke, make check-clickhouse, make check-services - Прогон по стартовому миру: 1917 заказов, счёт равен ods.order_v, каждый заказ на шарде своего cityHash64(order_id), сумма позиций равна items_total. - Повтор без нового слепка: содержимое то же, _load_id новый; после слепка пересобрано окно, день за окном не тронут. - Снесённая руками партиция 2026-06-01 восстановлена побайтово. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
96829718f9
commit
1e8b83ffbb
9 files changed
+555
-6
No files matched your search
@@ -41,12 +41,30 @@
|
||||
|
||||
Параметров у конвейера нет: объём он выводит сам из `world_position` и
|
||||
состояния слоёв. Сессии догоняют хвост — от последнего собранного дня до
|
||||
последнего прожитого; заказы пересобирают дышащее окно K от позиции; карта
|
||||
последнего прожитого; заказы пересобирают дышащее окно K и тот же хвост; карта
|
||||
идентичностей пересчитывается целиком; правила витрин — у
|
||||
[слоя DM](dm.md).
|
||||
Поэтому один и тот же запуск одинаково верен из триггера, с кнопки менти и
|
||||
после падения, а разгону на семь дней хватает одного прогона.
|
||||
|
||||
Окно заказов отсчитывается от **последнего дня, о котором рассказал сам
|
||||
источник**, — от позднейшего дня заказа в ODS. Рядом напрашиваются ещё два
|
||||
способа, и оба отвергнуты по одной причине: они отвечают не на тот вопрос.
|
||||
|
||||
Позиция мира говорит, какой день прожит, а не какой доехал: прогон дня D везёт
|
||||
слепок дня D − 1, да и позиция ставится раньше трансформации, так что после
|
||||
неудачного приёма она обгоняет хранилище. Слой обязан считать объём по тому,
|
||||
что у него есть, а не по тому, что мир о себе заявляет.
|
||||
|
||||
Ярлыки доставки — `snapshot_date`, `_load_id`, `_load_ts` — не отвечают тоже, и
|
||||
это свойство ODS, а не недосмотр: слой схлопывает доставки, сохраняя деловые
|
||||
версии, поэтому у заказа, который не менялся, ярлык выжившей строки достаётся
|
||||
жребием ([приём заказов](orders/ingestion.md), «Роль ODS»). Замер на стартовом
|
||||
мире: последний слепок нёс все 1662 заказа, а его ярлык сохранили 1188 —
|
||||
отбор «дни, у которых `snapshot_date` равен последнему» потерял бы четверть
|
||||
заказов и молча пропустил бы дальний край окна, где заказы давно замёрзли.
|
||||
К времени создания это не относится: у всех версий заказа оно одно.
|
||||
|
||||
## Готовность источника
|
||||
|
||||
Первый шаг `dds_transform` — дождаться, пока хвост топика `hits` дожёван:
|
||||
@@ -67,6 +85,31 @@
|
||||
замены: полная перезапись в `ReplacingMergeTree` — так живут карта
|
||||
идентичностей и витрина `daily_traffic` ([слой DM](dm.md)).
|
||||
|
||||
## Чего конвейер о себе не знает
|
||||
|
||||
Трансформация собирает слой по правилу, а не сверяет его с источником. Отсюда
|
||||
четыре вещи, которых она не заметит, и у всех четырёх один адрес — контур DQ.
|
||||
|
||||
- **Окно K разъехалось с источником.** Число живёт двумя копиями: у генератора
|
||||
в `world.py` и у стенда в `compose.yaml`. Разойдутся — слой начнёт
|
||||
недобирать край окна или переписывать замёрзший день, и оба прогона будут
|
||||
зелёными.
|
||||
- **Пустой свежий день сдвигает край окна.** Если в позднейший день источника
|
||||
не родилось ни одного заказа, окно съезжает на день назад
|
||||
([`sql/dds/order_scope.sql`](../../sql/dds/order_scope.sql)).
|
||||
- **Замена партиции прошла на одном шарде из двух.** День в слое есть — значит,
|
||||
объём следующего прогона его не тронет, а слой останется собранным наполовину
|
||||
из старого, наполовину из нового.
|
||||
- **Брак внутри `items` становится нулями.** Строгий приём внутрь позиций не
|
||||
смотрит, разбор по типу молчит, и цена без числа доедет до витрины выручки
|
||||
нулём ([`sql/dds/order_rebuild.sql`](../../sql/dds/order_rebuild.sql)).
|
||||
|
||||
Общее у них одно, и оно же подсказывает лекарство: ответить может только
|
||||
сверка слоя с источником по дню — счёт заказов и позднейший `updated_at` с
|
||||
обеих сторон. Трансформации такую сверку не поручают намеренно: она собирает,
|
||||
а проверяет другой. Так две роли остаются различимы, и ни одна не
|
||||
притворяется другой.
|
||||
|
||||
## Не входит
|
||||
|
||||
- Партиционная переобработка — переиграть произвольный прошлый день или
|
||||
|
||||
@@ -15,7 +15,9 @@ keeper, Kafka, каркас сервисов. Этап 2 собран: в `sql/d
|
||||
Тот же этап принёс первый справочник — словарь товаров из общего с генератором
|
||||
CSV-файла. Живёт он вне цепочки слоёв, в зоне `dic`, и читает не файл, а
|
||||
подложку над ним ([ADR 0012](../adr/0012-dictionary-home.md)). Этап 4 начат
|
||||
публичным договором события `dds.event_v` над точным состоянием ODS.
|
||||
публичным договором события `dds.event_v` над точным состоянием ODS, а следом
|
||||
получил первую физическую модель слоя — заказ: его пересобирает заменой
|
||||
партиций даг `dds_transform`.
|
||||
Дальше по тексту устройство описано так, как оно проектируется; построенное от
|
||||
заложенного отличает карта таблиц в конце.
|
||||
|
||||
@@ -129,6 +131,14 @@ FROM источник_dist`. Заводское значение `parallel_distr
|
||||
разложена по шардам по тому же ключу. Форму донора следует решать вместе с таким
|
||||
объектом, а не переносить на ODS заранее.
|
||||
|
||||
Первым такой донор понадобился заказу DDS, и правило на нём видно целиком:
|
||||
`dds.order_stage_rep` объявлен через `AS dds.order_rep` и шардируется тем же
|
||||
`cityHash64(order_id)`. Совпадение структуры требует сама операция, а совпадение
|
||||
ключа — устройство замены: она идёт по локальным таблицам, шард за шардом, и
|
||||
строка, попавшая в доноре не на свой шард, при подмене пропала бы с одного шарда
|
||||
и задвоилась на другом. Доводы целиком — в
|
||||
[`sql/ddl/45-dds-tables.sql`](../../sql/ddl/45-dds-tables.sql).
|
||||
|
||||
## Служебные колонки
|
||||
|
||||
Собственные колонки не повторяют имён виртуальных. Виртуальные даёт движок:
|
||||
@@ -508,7 +518,8 @@ SQL-файлы исполняет штатный `SQLExecuteQueryOperator`. Пр
|
||||
| `20-ods-tables.sql` | типизированное событие, версии заказа и обе таблицы ошибок |
|
||||
| `30-ods-views.sql` | актуальные события, текущие заказы и матвью разбора в ODS |
|
||||
| `40-stg-views.sql` | матвью приёма: чтец в сырьё |
|
||||
| `50-dds-views.sql` | публичный договор события на языке модели DDS |
|
||||
| `45-dds-tables.sql` | физическая модель заказа и таблица-двойник, донор её партиций |
|
||||
| `50-dds-views.sql` | публичные договоры события и заказа на языке модели DDS |
|
||||
|
||||
Справочники идут сразу за базами: зона `dic` не зависит ни от одного слоя, и
|
||||
правила порядка слоёв к ней не применяются.
|
||||
@@ -567,6 +578,9 @@ SQL-файлы исполняет штатный `SQLExecuteQueryOperator`. Пр
|
||||
| `ods` | `ods.order_v` | текущая версия заказа на языке источника |
|
||||
| `ods` | `ods.order_errors_rep` / `_dist` | строки слепка, не прошедшие строгий приём |
|
||||
| `dds` | `dds.event_v` | событие на языке бизнес-модели без физической копии |
|
||||
| `dds` | `dds.order_rep` / `_dist` | заказ в текущем состоянии, партиция — день заказа |
|
||||
| `dds` | `dds.order_stage_rep` / `_dist` | двойник заказа: в нём день собирается перед заменой партиции |
|
||||
| `dds` | `dds.order_v` | публичный договор заказа |
|
||||
| `dic` | `dic.products_file` | чтец CSV-каталога, общего с генератором |
|
||||
| `dic` | `dic.products` | словарь товаров поверх подложки |
|
||||
|
||||
@@ -574,9 +588,10 @@ SQL-файлы исполняет штатный `SQLExecuteQueryOperator`. Пр
|
||||
`INSERT SELECT` одной задачи `parse_batch` из файла
|
||||
`sql/ods/order_parse.sql`.
|
||||
|
||||
Слой DDS начат представлением `dds.event_v`; остальные его сущности и слой DM
|
||||
появятся в следующих тикетах этапа. Их состав задан разделом 7 мастер-спеки и
|
||||
переносится сюда по мере постройки.
|
||||
В слое DDS построены событие и заказ, и устроены они намеренно по-разному:
|
||||
событие остаётся представлением над ODS, заказ получил физическую модель.
|
||||
Сессии, карту идентичностей и слой DM принесут следующие тикеты этапа. Их
|
||||
состав задан разделом 7 мастер-спеки и переносится сюда по мере постройки.
|
||||
|
||||
## Что проверено
|
||||
|
||||
@@ -788,5 +803,32 @@ Kafka с пустым значением (ноль байт) и запись-н
|
||||
таблице. Это вычитано, а не измерено. На устройство приёма оговорка не влияет:
|
||||
служебные колонки мы заполняем выражением при любом ответе.
|
||||
|
||||
**Проверка замены партиций 26 августа 2026 года (#151).** Context7 подтвердил
|
||||
договор `REPLACE PARTITION`: `ON CLUSTER` в синтаксисе есть, а источник и цель
|
||||
обязаны совпадать структурой, ключом партиции, ключом сортировки, первичным
|
||||
ключом и политикой хранения. На закреплённом ClickHouse 26.3 замена по локальным
|
||||
таблицам с `ON CLUSTER` прошла на обеих нодах, а сборка стартового мира в
|
||||
двойник разложила 1662 заказа строго по ключу: шард 1 получил только чётные
|
||||
`cityHash64(order_id)`, шард 2 — только нечётные.
|
||||
|
||||
Тем же днём измерены ещё три поведения ClickHouse.
|
||||
|
||||
- **Параметр в `ALTER … PARTITION` не подставляется.** Запрос с `{day:Date}` в
|
||||
выражении партиции отвечает кодом 62, синтаксической ошибкой; литерал
|
||||
проходит. Отсюда единственное место в репозитории, где значение попадает в
|
||||
запрос подстановкой Jinja, а не параметром ClickHouse
|
||||
([`sql/dds/order_replace.sql`](../../sql/dds/order_replace.sql)).
|
||||
- **`TRUNCATE` — отдельное право, в `ALTER` оно не входит.** Роль `etl`, у
|
||||
которой есть `ALTER ON dds.*`, получает на `TRUNCATE` код 497, а
|
||||
`ALTER TABLE … DROP PARTITION ALL` на той же таблице проходит. Двойник
|
||||
поэтому чистится снятием партиций: у операции нашлась форма, на которую право
|
||||
уже выдано, и расширять доступ не понадобилось.
|
||||
- **`JSONExtract` читает `Decimal` прямо из строки JSON.** Цена на проводе —
|
||||
строка с двумя знаками (`"1289.99"`), и разбор в
|
||||
`Array(Tuple(sku String, qty UInt32, price Decimal(18, 2)))` даёт число без
|
||||
промежуточного `toDecimal64`. Проверено на живом слепке; честность разбора
|
||||
сторожит инвариант бэкенда — сумма «цена × количество» совпала с `items_total`
|
||||
у всех 1662 заказов.
|
||||
|
||||
**Сказано по памяти, проверки нет.** Группа пуста: оба утверждения, ждавшие
|
||||
матвью разбора, закрыты опытами при исполнении #43 и переехали выше.
|
||||
Reference in new issue
Block a user