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:
ddadminandClaude Opus 5 committed 2026-08-26 05:20:39 -04:00
1 parent 96829718f9
commit 1e8b83ffbb
9 files changed
+555 -6

No files matched your search

+65
View File
@@ -0,0 +1,65 @@
-- DDS: объём пересборки заказов — с какого дня по какой собирает этот запуск.
--
-- Параметров у дага нет: объём он выводит сам из состояния слоёв
-- (docs/architecture/etl.md, «Объём прогона»). Ответ — две даты, границы
-- непрерывного отрезка; список дней между ними разворачивает даг. Пустой
-- источник даёт пустой ответ, и даг на нём останавливается с внятной ошибкой
-- вместо зелёного прогона, собравшего ничего.
--
-- Файл кончается без точки с запятой: даг читает его целиком и шлёт одним
-- запросом, дописывая формат ответа, — точка с запятой сделала бы из этого
-- мультизапрос, который ClickHouse не принимает.
--
-- Объём складывается из двух частей. Первая — дышащее окно: K последних дней
-- источника, состояние которых слепок ещё может изменить. Вторая — хвост от
-- первого дня, которого в слое нет: первая сборка застаёт слой пустым и
-- собирает весь стартовый мир, а после разгона мира пачкой слой догоняет
-- уехавший вперёд источник.
--
-- **Хвост считается от первого недостающего дня, а не от последнего
-- собранного.** Разница не косметическая: замены партиций идут отдельными
-- задачами и падают порознь, поэтому упавший день оставляет дыру, пока
-- соседние дни собираются дальше. Считай мы прогресс по максимуму собранного
-- дня, дыра позади окна не заросла бы никогда, а прогоны продолжали бы
-- зеленеть над щербатым слоем. Цена честного ответа — проход по дню заказа в
-- ODS; она того стоит.
--
-- Край окна берётся из делового поля, а не из ярлыков доставки: время
-- создания заказа одинаково у всех его версий, а `snapshot_date` и `_load_id`
-- выжившей строки достаются жребием (docs/architecture/etl.md, «Объём
-- прогона», и docs/architecture/orders/ingestion.md, «Роль ODS»).
--
-- Известная неточность края: если в самый свежий день источника не родилось
-- ни одного заказа, окно съезжает на день назад — лишний замёрзший день
-- пересоберётся впустую, а пустой день не тронется. Данные от этого не
-- страдают, меняется только ярлык загрузки лишнего дня. Точный ответ здесь
-- дала бы дата слепка, но она и есть тот самый жребий.
WITH
-- FINAL не нужен: время рождения заказа версии не различают.
(SELECT max(toDate(toTimeZone(created_at, 'Europe/Samara')))
FROM ods.order_dist) AS last_source_day,
(SELECT min(toDate(toTimeZone(created_at, 'Europe/Samara')))
FROM ods.order_dist) AS first_source_day,
-- GLOBAL обязателен: обе таблицы распределённые, и обычный NOT IN сверял
-- бы дни источника с локальной частью слоя на каждом шарде — день, чьи
-- заказы легли на соседний шард, показался бы недостающим.
-- minOrNull отличает «дыр нет» от даты по умолчанию.
(
SELECT minOrNull(day)
FROM (
SELECT DISTINCT toDate(toTimeZone(created_at, 'Europe/Samara')) AS day
FROM ods.order_dist
)
WHERE day GLOBAL NOT IN (SELECT order_date FROM dds.order_dist)
) AS first_missing_day,
last_source_day - ({window_days:UInt16} - 1) AS window_start
SELECT
-- Дыр нет — начинаем с окна; есть — с первой из них, если она старше окна.
-- greatest держит левый край не раньше первого дня, известного источнику:
-- у молодого мира окно шире самого мира.
greatest(
least(window_start, coalesce(first_missing_day, window_start)),
first_source_day
) AS first_day,
last_source_day AS last_day
WHERE (SELECT count() FROM ods.order_dist) > 0