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
+26
-84
@@ -1,8 +1,6 @@
|
||||
-- ODS: каноническое чтение актуальных событий.
|
||||
--
|
||||
-- Представление сохраняет поля и зерно ods.event_dist, но скрывает FINAL от
|
||||
-- потребителя. Поэтому физическая таблица остаётся местом диагностики
|
||||
-- доставленных версий, а точное чтение получает одно имя.
|
||||
-- Актуальные события: FINAL выбирает последнюю доставку по _load_ts,
|
||||
-- не дожидаясь фоновых слияний. Поля и типы остаются как в ods.event_dist.
|
||||
|
||||
CREATE VIEW IF NOT EXISTS ods.event_v ON CLUSTER clickstream_cluster
|
||||
AS
|
||||
SELECT *
|
||||
@@ -25,69 +23,24 @@ AS
|
||||
SELECT *
|
||||
FROM ods.order_dist FINAL;
|
||||
|
||||
-- ODS: разбор STG в событие и в таблицу ошибок.
|
||||
-- Разбор STG в события и ошибки. Два представления используют одинаковое
|
||||
-- условие годности; второе берет его отрицание. Условие дает только 0 или 1:
|
||||
-- NULL не выбрала бы ни одна ветвь. Функции разбора не вызывают исключений
|
||||
-- на некорректном raw, чтобы одно сообщение не прервало прием.
|
||||
--
|
||||
-- Матвью две, и вместе они обязаны делить поток без зазора и без нахлёста:
|
||||
-- строка, подошедшая обеим, задвоится, а не подошедшая ни одной — исчезнет
|
||||
-- молча. Держится это формой: годность считает один и тот же предикат из трёх
|
||||
-- частей, а вторая матвью берёт его буквальное отрицание.
|
||||
-- Оба представления срабатывают на вставку в stg.hits_raw_dist,
|
||||
-- до распределения по шардам. Порядок SELECT совпадает с целевой таблицей.
|
||||
--
|
||||
-- Отсюда два запрета на выражения предиката, и оба серьёзные.
|
||||
-- Прием проверяет объект JSON, точный набор ключей и разбор пяти полей:
|
||||
-- WatchID, VisitID, ClientID, EventDate, UTCEventTime. Остальные поля
|
||||
-- извлекаются по формату без отдельной проверки. Граница — ADR 0005.
|
||||
-- arraySort с обеих сторон позволяет сравнить ключи независимо от порядка.
|
||||
--
|
||||
-- Первый: ни одна его часть не возвращает NULL. Трёхзначная логика дала бы
|
||||
-- строку, которую не берёт ни «условие», ни «NOT условие», — и потерялась бы
|
||||
-- она бесшумно. Поэтому сравнения здесь дают 0 или 1, а разбор в Nullable
|
||||
-- заканчивается IS NOT NULL.
|
||||
--
|
||||
-- Второй: ни одна его часть не бросает исключений. Упавшая матвью роняет
|
||||
-- вставку, офсеты не коммитятся, и потребление топика встаёт до починки —
|
||||
-- это единственное, на чём сейчас держится правило репозитория «грязные
|
||||
-- записи не валят пайплайн». Поэтому приведение Nullable к необнуляемому типу
|
||||
-- (assumeNotNull) стоит только в списке колонок, за предикатом, который NULL
|
||||
-- уже отсёк, а в самом предикате живут только функции семейства
|
||||
-- JSONExtract/*OrNull. Довод целиком — ADR 0005.
|
||||
--
|
||||
-- Источник у обеих — stg.hits_raw_dist, лицо слоя, а не локальная таблица:
|
||||
-- потребитель цепляется к лицу, а матвью с источником-Distributed срабатывает
|
||||
-- на вставку именно в распределённую таблицу, до раскладки по шардам.
|
||||
--
|
||||
-- Порядок колонок в SELECT совпадает с порядком в целевой таблице.
|
||||
|
||||
-- Годное событие: строка, прошедшая строгий приём.
|
||||
--
|
||||
-- Строгий приём — это три вопроса: объект ли это JSON, тот ли набор ключей,
|
||||
-- разбираются ли в свой тип пять опорных колонок. Почему именно так — почему
|
||||
-- объект, а не валидность; почему сверка ключей заменяет сорок семь проверок
|
||||
-- на присутствие; почему опорных пять, а не сорок семь — ADR 0005, «Решение».
|
||||
--
|
||||
-- arraySort ниже стоит с обеих сторон, и убирать его нельзя: без обёртки сорок
|
||||
-- семь CamelCase-имён пришлось бы держать ровно в байтовом порядке, а сбой
|
||||
-- порядка увёл бы в брак вообще всё, и молча (ADR 0005).
|
||||
--
|
||||
-- Метка времени — единственная из пяти, кого разбирает не JSONExtract, и это
|
||||
-- измеренная необходимость, а не вкус. На проводе UTCEventTime уезжает в
|
||||
-- ISO-8601 с суффиксом зоны — «2026-06-01T12:34:56Z» (спека генератора,
|
||||
-- раздел 4), а JSONExtract с типом DateTime такую строку не берёт и отдаёт
|
||||
-- NULL. Оставь его здесь — и в брак уехали бы все события до единого.
|
||||
--
|
||||
-- Формат назван буквально, а не отдан parseDateTimeBestEffort, и вот почему.
|
||||
-- Best-effort понимает десяток записей и на непонятной не краснеет, а
|
||||
-- достраивает недостающее: обрезанное «20:00:21» он превращает в первое
|
||||
-- января текущего года. Такая строка прошла бы строгий приём с тихо неверным
|
||||
-- временем — ровно с той порчей, ради которой класс key_field_unparsed и
|
||||
-- заведён. Источник у топика один и шлёт одну запись, так что широта здесь не
|
||||
-- нужна вовсе, а стоит она отключённой проверкой. Замеры — ADR 0005,
|
||||
-- «Что проверено».
|
||||
--
|
||||
-- Третьим аргументом назван пояс — 'UTC'. Суффикс Z маска сверяет как букву и
|
||||
-- выбрасывает, зоны из строки не берёт вовсе, поэтому без имени функция читала
|
||||
-- бы показания часов по поясу сессии, а тот по умолчанию серверный. Тип
|
||||
-- колонки этого не чинит: разбор отдаёт готовое число, и колонка кладёт его
|
||||
-- как есть — пояс приёмника решал бы судьбу строки, а не числа.
|
||||
-- Правило и замер — docs/architecture/storage.md, «Часовые пояса».
|
||||
--
|
||||
-- EventDate в такой подпорке не нуждается: дата уезжает как «2026-06-01», и
|
||||
-- JSONExtract её берёт.
|
||||
-- UTCEventTime приходит строкой вида "2026-06-01T12:34:56Z". Ее разбирает
|
||||
-- parseDateTimeOrNull с явной маской: JSONExtract в DateTime такую форму
|
||||
-- не принимает, а BestEffort допускает неполные даты. Пояс UTC указан
|
||||
-- отдельно, потому что Z в маске проверяется как буква. EventDate приходит
|
||||
-- как "2026-06-01" и разбирается через JSONExtract.
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS ods.event_mv ON CLUSTER clickstream_cluster
|
||||
TO ods.event_dist
|
||||
@@ -154,10 +107,8 @@ SELECT
|
||||
JSONExtract(raw, 'GoalsReached', 'Array(UInt32)') AS GoalsReached,
|
||||
JSONExtract(raw, 'ParsedParamsKey1', 'Array(String)') AS ParsedParamsKey1,
|
||||
JSONExtract(raw, 'purchaseID', 'Array(String)') AS purchaseID,
|
||||
-- Выгрузка несет JSON-числа типа Float64, но деньги хранятся в Decimal.
|
||||
-- JSONExtract разбирает их сразу, без промежуточного двоичного числа:
|
||||
-- синтаксис подтвержден документацией ClickHouse через Context7 и
|
||||
-- запросом к стендовому ClickHouse 26.3 при исполнении #155.
|
||||
-- purchaseRevenue приходит числом рублей и разбирается сразу в Decimal.
|
||||
-- Прямой разбор сверяли через Context7 и на ClickHouse 26.3 (#155).
|
||||
JSONExtract(raw, 'purchaseRevenue', 'Array(Decimal(18, 2))') AS purchaseRevenue,
|
||||
JSONExtract(raw, 'purchaseCurrency', 'Array(String)') AS purchaseCurrency,
|
||||
JSONExtract(raw, 'purchaseCoupon', 'Array(String)') AS purchaseCoupon,
|
||||
@@ -168,25 +119,16 @@ SELECT
|
||||
JSONExtract(raw, 'productQuantity', 'Array(UInt64)') AS productQuantity,
|
||||
JSONExtract(raw, 'productEventType', 'Array(String)') AS productEventType,
|
||||
JSONExtract(raw, 'ecommerce', 'String') AS ecommerce,
|
||||
-- Метка загрузки переносится из STG как есть, а не ставится заново:
|
||||
-- она отвечает на вопрос «когда строка приехала в хранилище». Поставь
|
||||
-- здесь now64(3) — и колонка версии молча ответила бы на другой вопрос.
|
||||
-- _load_ts остается временем исходной доставки и версией события.
|
||||
_load_ts
|
||||
FROM stg.hits_raw_dist
|
||||
WHERE is_object AND keys_match AND key_fields_parsed;
|
||||
|
||||
-- Брак: буквальное отрицание того же предиката.
|
||||
--
|
||||
-- Классы брака пересекаются, поэтому проверяются по порядку, а в error_class
|
||||
-- пишется первый совпавший. Скаляр проваливает и проверку на объект, и сверку
|
||||
-- ключей — JSONExtractKeys от него даёт пустой массив (измерено на стенде
|
||||
-- 7 августа 2026 года), — и без объявленного порядка попал бы то в один
|
||||
-- класс, то в другой.
|
||||
--
|
||||
-- Предикат повторён здесь дословно, и это выбор, а не безвыходность: назвать
|
||||
-- его один раз на две матвью позволил бы CREATE FUNCTION. Отвергнуто — условие
|
||||
-- разбора ушло бы за имя, в отдельный объект со своей жизнью, и слой перестал
|
||||
-- бы читаться по своему же DDL.
|
||||
-- Ошибки: отрицание того же условия годности.
|
||||
-- Класс определяется первым нарушенным условием. Например, JSON-строка
|
||||
-- не проходит и проверку объекта, и проверку ключей, но получает not_an_object.
|
||||
-- Условие повторено рядом с запросом, без отдельной функции ClickHouse.
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS ods.event_errors_mv ON CLUSTER clickstream_cluster
|
||||
TO ods.event_errors_dist
|
||||
AS
|
||||
|
||||
Reference in new issue
Block a user