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
+19
-42
@@ -1,21 +1,9 @@
|
||||
-- DDS: сборка дней заказа в таблицу-двойник перед заменой партиций.
|
||||
-- Сборка выбранных дней в донор перед заменой партиций DDS.
|
||||
-- Донор очищается в начале: иначе новые строки добавились бы к прежним.
|
||||
-- После сборки он остается для диагностики до следующего запуска.
|
||||
--
|
||||
-- Двойник очищается целиком в начале сборки, а не в конце: собранное остаётся
|
||||
-- лежать до следующего запуска, и разбор происшествия видит ровно то, чем
|
||||
-- подменили день. Очистка обязана быть первой ещё и по делу — вставка поверх
|
||||
-- прежних строк удвоила бы день в доноре, а REPLACE PARTITION перенёс бы
|
||||
-- удвоение в цель.
|
||||
--
|
||||
-- Дни собираются одной вставкой, а не по дню за раз: источник читается один
|
||||
-- раз на весь отрезок. Кто раскладывает строки по шардам — вставка через
|
||||
-- Distributed, ключ cityHash64(order_id) — разобрано в DDL двойника.
|
||||
--
|
||||
-- Чистит двойник DROP PARTITION ALL, а не TRUNCATE: последнее — отдельное
|
||||
-- право, роли etl оно не выдано, а снятие всех партиций
|
||||
-- входит в ALTER, который у неё есть (ADR 0007). Замерено: TRUNCATE отвечает
|
||||
-- ролью etl кодом 497, DROP PARTITION ALL проходит. Расширять права ради
|
||||
-- операции, для которой уже есть разрешённая форма, значило бы платить
|
||||
-- доступом за синоним.
|
||||
-- DROP PARTITION ALL доступен роли etl через ALTER; отдельного права
|
||||
-- TRUNCATE у нее нет (ADR 0007). Весь отрезок дней собирается одной вставкой.
|
||||
|
||||
ALTER TABLE dds.order_stage_rep ON CLUSTER clickstream_cluster DROP PARTITION ALL;
|
||||
|
||||
@@ -37,18 +25,13 @@ INSERT INTO dds.order_stage_dist
|
||||
_load_id,
|
||||
_load_ts
|
||||
)
|
||||
-- Позиции разбираются здесь один раз, на границе ODS → DDS. Цена на проводе —
|
||||
-- строка с двумя знаками, и JSONExtract читает её прямо в Decimal: замерено на
|
||||
-- живом слепке, промежуточный toDecimal64 не нужен.
|
||||
--
|
||||
-- Разбор по типу молчалив, и знать об этом надо заранее. Строгий приём ODS
|
||||
-- проверил только, что items — массив, внутрь он не смотрел; здесь тоже никто
|
||||
-- не смотрит. Замерено: qty вида "oops" даёт 0, пропущенная цена даёт 0,
|
||||
-- скаляр вместо объекта выпадает из массива целиком. На каноническом слепке
|
||||
-- генератора такого не бывает, и сумма «цена × количество» сходится с
|
||||
-- items_total у всех заказов, но сходится она по свойству источника, а не
|
||||
-- потому, что её здесь кто-то проверяет. Постоянное место такой проверки —
|
||||
-- контур DQ этапа 4; пока она делается руками при приёмке.
|
||||
-- Разбор позиций из JSON: цена в строке сразу превращается в Decimal.
|
||||
-- ODS проверил только тип массива items. Неверное содержимое здесь может
|
||||
-- дать значения по умолчанию: qty вида "oops" и пропущенная цена дают 0,
|
||||
-- скаляр вместо объекта выпадает из массива. Наблюдения —
|
||||
-- docs/architecture/storage.md, «Что проверено».
|
||||
-- Проверку суммы позиций участник добавляет в docs/labs/5-1-order-checks.md.
|
||||
|
||||
WITH JSONExtract(
|
||||
items, 'Array(Tuple(sku String, qty UInt32, price Decimal(18, 2)))'
|
||||
) AS lines
|
||||
@@ -56,8 +39,7 @@ SELECT
|
||||
order_id,
|
||||
user_id,
|
||||
status,
|
||||
-- День заказа и оба времени переезжают в пояс счётчика: DDS обслуживает
|
||||
-- человека с дашбордом, и пояс называется здесь в последний раз.
|
||||
-- День и время в Europe/Samara: витрины используют готовые колонки.
|
||||
toDate(toTimeZone(created_at, 'Europe/Samara')),
|
||||
toTimeZone(created_at, 'Europe/Samara'),
|
||||
toTimeZone(updated_at, 'Europe/Samara'),
|
||||
@@ -68,22 +50,17 @@ SELECT
|
||||
arrayMap(line -> line.sku, lines),
|
||||
arrayMap(line -> line.qty, lines),
|
||||
arrayMap(line -> line.price, lines),
|
||||
-- Координаты запуска, собравшего день: у всех строк дня они одни.
|
||||
-- Метки этой сборки DDS, общие для всех вставляемых строк.
|
||||
{load_id:String},
|
||||
now64(3, 'UTC')
|
||||
FROM ods.order_v
|
||||
WHERE toDate(toTimeZone(created_at, 'Europe/Samara'))
|
||||
BETWEEN {first_day:Date} AND {last_day:Date}
|
||||
-- Вторая граница не меняет отбор, а открывает источнику отсечение
|
||||
-- партиций: ODS нарезан по toDate(created_at) в UTC, и условие на
|
||||
-- вычисленный день заказа для этого ключа непрозрачно — сервер прочёл бы
|
||||
-- все партиции. Сутки пояса счётчика ложатся ровно на две даты UTC,
|
||||
-- поэтому день слева берётся с запасом.
|
||||
-- Дополнительный фильтр сокращает чтение партиций ODS по UTC.
|
||||
-- Модельные сутки Europe/Samara пересекают две даты UTC, поэтому
|
||||
-- левая граница берется на день раньше. Точный отбор — условием выше.
|
||||
AND toDate(created_at) BETWEEN {first_day:Date} - 1 AND {last_day:Date}
|
||||
-- Тот же ноль, что в приёме заказов: заводская двойка исполнила бы вставку
|
||||
-- локально на каждом шарде, минуя ключ шардирования цели, и строки унаследовали
|
||||
-- бы раскладку источника. Ключи ODS и DDS здесь совпадают, и раскладка вышла бы
|
||||
-- верной — но по совпадению, а не по контракту (docs/architecture/storage.md,
|
||||
-- «Раскладка по шардам», капкан #137).
|
||||
-- Ждем доставки на шарды и распределяем строки по ключу цели
|
||||
-- (docs/architecture/storage.md, «Раскладка по шардам»).
|
||||
SETTINGS distributed_foreground_insert = 1,
|
||||
parallel_distributed_insert_select = 0;
|
||||
+12
-24
@@ -1,28 +1,16 @@
|
||||
-- DDS: подмена одного дня заказов собранной партицией двойника.
|
||||
-- Замена одного дня DDS готовой партицией донора.
|
||||
-- Повтор при неизменном источнике сохраняет деловые данные без дублей.
|
||||
--
|
||||
-- Это и есть идемпотентность слоя. Повтор прогона безопасен, а «дыхание»
|
||||
-- статусов внутри окна выражается сменой всей партиции, а не правкой строк.
|
||||
-- ON CLUSTER выполняет замену на обеих нодах по локальным таблицам.
|
||||
-- Донор и цель имеют одинаковую структуру и ключ шардирования
|
||||
-- (sql/ddl/45-dds-tables.sql); REPLACE PARTITION строки не перераспределяет.
|
||||
-- Замена атомарна внутри шарда. Между заменами на разных шардах читатель
|
||||
-- может увидеть часть прежних и часть новых данных. После сбоя этот период
|
||||
-- длится до успешного повтора. Пустая партиция донора удаляет день из цели.
|
||||
--
|
||||
-- Замена идёт по локальным таблицам, шард за шардом: ON CLUSTER рассылает её
|
||||
-- на обе ноды, а по Distributed то же самое не работает — операции с
|
||||
-- партициями отвечают кодом 48 (docs/architecture/storage.md, «Что
|
||||
-- проверено»). Правильность пошардовой замены держится на том, что двойник
|
||||
-- шардирован тем же ключом, что цель, — см. sql/ddl/45-dds-tables.sql.
|
||||
--
|
||||
-- Отсюда граница обещания: атомарность здесь пошардовая. Внутри шарда подмена
|
||||
-- мгновенна — читатель видит либо прежний день целиком, либо новый; но две
|
||||
-- локальные замены независимы, и в зазоре между ними чтение через Distributed
|
||||
-- соберёт ответ из нового дня одного шарда и прежнего дня другого. Зазор
|
||||
-- короткий, а отказ одной из двух замен растягивает его до повторного прогона.
|
||||
--
|
||||
-- Пустая партиция двойника — законный случай, а не сбой: день, в котором
|
||||
-- заказов нет, снимает день из цели, и слой перестаёт показывать то, чего в
|
||||
-- источнике больше нет.
|
||||
--
|
||||
-- День подставляет Jinja, а не параметр ClickHouse: в ALTER … PARTITION
|
||||
-- сервер параметров не подставляет — замерено, отвечает синтаксической
|
||||
-- ошибкой. Общее правило репозитория «параметры передаются отдельно, а не
|
||||
-- строкой» здесь не нарушено по существу: значение пришло из ответа самого
|
||||
-- ClickHouse на запрос объёма, а не снаружи стенда.
|
||||
-- День подставляет Jinja: параметры ClickHouse в ALTER ... PARTITION
|
||||
-- не поддерживаются в закрепленной версии 26.3. Значение получено
|
||||
-- из результата order_scope.sql; замер — docs/architecture/storage.md.
|
||||
|
||||
ALTER TABLE dds.order_rep ON CLUSTER clickstream_cluster
|
||||
REPLACE PARTITION '{{ params.day }}' FROM dds.order_stage_rep;
|
||||
+16
-37
@@ -1,37 +1,17 @@
|
||||
-- DDS: объём пересборки заказов — с какого дня по какой собирает этот запуск.
|
||||
-- Дни пересборки заказов: first_day и last_day, обе границы включены.
|
||||
-- Текущее окно — дни оформления заказов из последнего snapshot_date.
|
||||
-- Если в DDS нет дня, который есть в ODS, отрезок расширяется до первого
|
||||
-- такого дня. Это восстанавливает пропуски даже за пределами текущего окна.
|
||||
--
|
||||
-- Параметров у дага нет: объём он выводит сам из состояния слоёв
|
||||
-- (docs/architecture/etl.md, «Объём прогона»). Ответ — две даты, границы
|
||||
-- непрерывного отрезка; список дней между ними разворачивает даг. Пустой
|
||||
-- источник даёт пустой ответ, и даг на нём останавливается с внятной ошибкой
|
||||
-- вместо зелёного прогона, собравшего ничего.
|
||||
-- Дата слепка у актуальной версии показывает последнее наблюдение заказа
|
||||
-- (ADR 0014). Поэтому окно берется из данных, без копирования константы
|
||||
-- генератора. FINAL здесь не нужен: created_at одинаков у всех версий,
|
||||
-- а строки последнего слепка сохраняются при слияниях.
|
||||
--
|
||||
-- Файл кончается без точки с запятой: даг читает его целиком и шлёт одним
|
||||
-- запросом, дописывая формат ответа, — точка с запятой сделала бы из этого
|
||||
-- мультизапрос, который ClickHouse не принимает.
|
||||
--
|
||||
-- Объём складывается из двух частей. Первая — дышащее окно: дни, которые нёс
|
||||
-- последний слепок, то есть те, чьё состояние источник ещё может изменить.
|
||||
-- Вторая — хвост от первого дня, которого в слое нет: первая сборка застаёт
|
||||
-- слой пустым и собирает весь стартовый мир, а после разгона мира пачкой слой
|
||||
-- догоняет уехавший вперёд источник.
|
||||
--
|
||||
-- **Хвост считается от первого недостающего дня, а не от последнего
|
||||
-- собранного.** Разница не косметическая: замены партиций идут отдельными
|
||||
-- задачами и падают порознь, поэтому упавший день оставляет дыру, пока
|
||||
-- соседние дни собираются дальше. Считай мы прогресс по максимуму собранного
|
||||
-- дня, дыра позади окна не заросла бы никогда, а прогоны продолжали бы
|
||||
-- зеленеть над щербатым слоем. Цена честного ответа — проход по дню заказа
|
||||
-- в ODS.
|
||||
--
|
||||
-- Окно читается из ярлыка слепка, а не вычисляется вычитанием окна
|
||||
-- изменяемости: у выжившей строки snapshot_date — последний слепок, который
|
||||
-- нёс этот заказ (ADR 0014). Поэтому оба края окна знает сам источник, а
|
||||
-- копии его константы на стороне стенда больше нет.
|
||||
--
|
||||
-- FINAL не нужен ни здесь, ни ниже: время рождения заказа у всех его версий
|
||||
-- одно, а строки последнего слепка несут максимум колонки версии — мерж
|
||||
-- оставляет как раз их.
|
||||
-- Пустой ODS не дает строки результата; даг завершится ошибкой.
|
||||
-- Точка с запятой в конце не нужна: клиент дописывает FORMAT ответа.
|
||||
-- Порядок пересборки — docs/architecture/etl.md, «Объём прогона».
|
||||
|
||||
WITH
|
||||
(SELECT max(snapshot_date) FROM ods.order_dist) AS last_snapshot,
|
||||
(
|
||||
@@ -44,10 +24,9 @@ WITH
|
||||
FROM ods.order_dist
|
||||
WHERE snapshot_date = last_snapshot
|
||||
) AS window_end,
|
||||
-- GLOBAL обязателен: обе таблицы распределённые, и обычный NOT IN сверял
|
||||
-- бы дни источника с локальной частью слоя на каждом шарде — день, чьи
|
||||
-- заказы легли на соседний шард, показался бы недостающим.
|
||||
-- minOrNull отличает «дыр нет» от даты по умолчанию.
|
||||
-- GLOBAL NOT IN сравнивает дни со всем DDS. Локальное сравнение могло бы
|
||||
-- счесть день пропущенным, если его заказы находятся на соседнем шарде.
|
||||
-- minOrNull возвращает NULL, когда пропущенных дней нет.
|
||||
(
|
||||
SELECT minOrNull(day)
|
||||
FROM (
|
||||
@@ -57,7 +36,7 @@ WITH
|
||||
WHERE day GLOBAL NOT IN (SELECT order_date FROM dds.order_dist)
|
||||
) AS first_missing_day
|
||||
SELECT
|
||||
-- Дыр нет — начинаем с окна; есть — с первой из них, если она старше окна.
|
||||
-- Начало окна или первый пропущенный день, если он раньше.
|
||||
least(window_start, coalesce(first_missing_day, window_start)) AS first_day,
|
||||
window_end AS last_day
|
||||
WHERE (SELECT count() FROM ods.order_dist) > 0
|
||||
Reference in new issue
Block a user