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
@@ -0,0 +1,95 @@
|
||||
-- DDS: физическая модель заказа и донор её партиций.
|
||||
--
|
||||
-- Зерно — заказ в текущем состоянии, ключ order_id. Историю версий слой не
|
||||
-- хранит: у неё нет читателя ни в одной витрине, а ODS её и не обещает
|
||||
-- (docs/architecture/dds/order.md, «Зерно»).
|
||||
--
|
||||
-- Идемпотентность здесь устроена иначе, чем в приёме, и это главный урок
|
||||
-- сущности. В ODS повтор гасит колонка версии: ReplacingMergeTree(updated_at)
|
||||
-- оставляет позднюю строку. Здесь колонки версии нет вовсе — повтор гасит
|
||||
-- замена партиции целиком: день собирается заново и подменяется одним
|
||||
-- атомарным движением. Поэтому каждая строка таблицы — ровно один заказ, а
|
||||
-- чтению не нужен FINAL.
|
||||
|
||||
-- Локальная таблица шарда.
|
||||
--
|
||||
-- Партиция — order_date, день заказа в поясе счётчика. Партиция дневная не
|
||||
-- ради размера куска, а потому что день — единица пересборки: пока заказ
|
||||
-- внутри окна изменяемости, его день «дышит» и собирается заново, за окном
|
||||
-- замерзает. Ключ партиции и есть контракт трансформации с хранилищем.
|
||||
--
|
||||
-- Ключ сортировки — order_id: единственный устойчивый ключ сущности, он же
|
||||
-- ключ шардирования ниже. Колонки версии в движке нет: см. шапку файла.
|
||||
CREATE TABLE IF NOT EXISTS dds.order_rep ON CLUSTER clickstream_cluster
|
||||
(
|
||||
order_id String,
|
||||
user_id UInt64,
|
||||
status LowCardinality(String),
|
||||
-- День заказа посчитан один раз при загрузке, по образцу EventDate у
|
||||
-- события: суточные срезы группируются по готовой дате и пояса не
|
||||
-- упоминают (docs/architecture/storage.md, «Часовые пояса»).
|
||||
order_date Date,
|
||||
-- Времена лежат местными: DDS обслуживает человека с дашбордом, и пояс
|
||||
-- пересчитан один раз здесь, а не в каждом отчёте. Линза названа в типе,
|
||||
-- поэтому пояс сервера на эти колонки не влияет.
|
||||
created_at DateTime64(3, 'Europe/Samara'),
|
||||
updated_at DateTime64(3, 'Europe/Samara'),
|
||||
items_total Decimal(18, 2),
|
||||
discount Decimal(18, 2),
|
||||
delivery Decimal(18, 2),
|
||||
total Decimal(18, 2),
|
||||
-- Позиции — три массива одной длины: артикул, количество, цена. Родная
|
||||
-- для колоночного хранилища денормализация: позиция не живёт без заказа и
|
||||
-- своего ключа не имеет, а разворачивает массивы читатель — ARRAY JOIN в
|
||||
-- витрине выручки. Форма записи выбрана по дому: широкое событие хранит
|
||||
-- повторяющиеся группы Метрики такими же параллельными колонками
|
||||
-- (productID, productPrice, productQuantity в ods.event_rep), а не Nested.
|
||||
item_sku Array(String),
|
||||
item_quantity Array(UInt32),
|
||||
item_price Array(Decimal(18, 2)),
|
||||
-- Координаты собственной загрузки слоя: каким запуском и когда собран
|
||||
-- день. При замене партиции они одинаковы у всех строк дня. Ярлыки ODS
|
||||
-- сюда не переезжают — почему, разобрано в доке модели.
|
||||
_load_id String,
|
||||
_load_ts DateTime64(3, 'UTC')
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/{database}/{table}', '{replica}')
|
||||
PARTITION BY order_date
|
||||
ORDER BY order_id;
|
||||
|
||||
-- Лицо загрузки и диагностики. Ключ шардирования — cityHash64(order_id), тот
|
||||
-- же, что у ods.order_dist: заказ остаётся на одном шарде на всём пути через
|
||||
-- слои, и соединения по зерну заказа обходятся без GLOBAL
|
||||
-- (docs/architecture/storage.md, «Раскладка по шардам»).
|
||||
CREATE TABLE IF NOT EXISTS dds.order_dist ON CLUSTER clickstream_cluster
|
||||
AS dds.order_rep
|
||||
ENGINE = Distributed('clickstream_cluster', 'dds', 'order_rep', cityHash64(order_id));
|
||||
|
||||
-- Донор партиций: таблица-двойник, в которой день собирается перед подменой.
|
||||
--
|
||||
-- Двойник нужен потому, что REPLACE PARTITION берёт готовый кусок из другой
|
||||
-- таблицы, а не из результата запроса. Собрать день прямо в цель нельзя:
|
||||
-- вставка добавила бы строки к уже лежащим, и повтор прогона удвоил бы день.
|
||||
--
|
||||
-- Форма двойника задана правилом донора: структура, ключ партиции, ключ
|
||||
-- сортировки, первичный ключ и политика хранения обязаны совпадать с целью —
|
||||
-- это требование самой операции (проверено по документации ClickHouse,
|
||||
-- sql-reference/statements/alter/partition). Отсюда AS dds.order_rep вместо
|
||||
-- повторённого списка колонок: расхождение становится невозможным по
|
||||
-- построению и не держится на внимательности того, кто правит.
|
||||
--
|
||||
-- Своё имя в keeper двойник получает от макроса {table}, поэтому две таблицы
|
||||
-- не делят путь репликации.
|
||||
CREATE TABLE IF NOT EXISTS dds.order_stage_rep ON CLUSTER clickstream_cluster
|
||||
AS dds.order_rep
|
||||
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/{database}/{table}', '{replica}')
|
||||
PARTITION BY order_date
|
||||
ORDER BY order_id;
|
||||
|
||||
-- Двойник шардируется тем же ключом, что цель, и это не украшение: REPLACE
|
||||
-- PARTITION идёт по локальным таблицам, шард за шардом. Ляг строка заказа в
|
||||
-- доноре на другой шард, чем её место в цели, — подмена перенесла бы её не
|
||||
-- туда, и заказ пропал бы с одного шарда и задвоился на другом.
|
||||
CREATE TABLE IF NOT EXISTS dds.order_stage_dist ON CLUSTER clickstream_cluster
|
||||
AS dds.order_stage_rep
|
||||
ENGINE = Distributed('clickstream_cluster', 'dds', 'order_stage_rep', cityHash64(order_id));
|
||||
@@ -65,3 +65,37 @@ SELECT
|
||||
productQuantity AS product_quantity,
|
||||
productEventType AS product_event_type
|
||||
FROM ods.event_v;
|
||||
|
||||
-- DDS: публичный договор заказа на языке бизнес-модели.
|
||||
--
|
||||
-- В отличие от события, у заказа под представлением лежит физическая модель:
|
||||
-- разбор позиций и пересчёт пояса стоят денег, и платить их при каждом чтении
|
||||
-- значило бы собирать модель заново на лету (docs/architecture/dds/order.md,
|
||||
-- «Отклонённые варианты»). Представление здесь ничего не вычисляет — оно
|
||||
-- только называет колонки, которые слой обещает читателю.
|
||||
--
|
||||
-- FINAL нет и не будет: строка на заказ одна по построению, повтор гасит
|
||||
-- замена партиции, а не колонка версии.
|
||||
--
|
||||
-- Служебные колонки перечислены наравне с деловыми — это координаты
|
||||
-- собственной загрузки слоя, и расследование происшествия начинается с них:
|
||||
-- по _load_id видно, какой запуск собрал день.
|
||||
CREATE VIEW IF NOT EXISTS dds.order_v ON CLUSTER clickstream_cluster
|
||||
AS
|
||||
SELECT
|
||||
order_id,
|
||||
user_id,
|
||||
status,
|
||||
order_date,
|
||||
created_at,
|
||||
updated_at,
|
||||
items_total,
|
||||
discount,
|
||||
delivery,
|
||||
total,
|
||||
item_sku,
|
||||
item_quantity,
|
||||
item_price,
|
||||
_load_id,
|
||||
_load_ts
|
||||
FROM dds.order_dist;
|
||||
Reference in new issue
Block a user