diff --git a/docs/labs/6-1-event-parsing.md b/docs/labs/6-1-event-parsing.md new file mode 100644 index 0000000..cd0ab8b --- /dev/null +++ b/docs/labs/6-1-event-parsing.md @@ -0,0 +1,215 @@ +# Разберите сырые события рядом со стендом + +**Чему учит:** пакетно разобрать сообщения отдельно от живого приема и +построчно проверить свой результат по нетронутому ODS. + +## Сначала прочитать + +- [формат события](../formats/clickstream-event.md); +- [справочник хранилища](../architecture/storage.md), разделы про имена + объектов, актуальные события, прием потока и срок жизни сырых данных; +- [ADR о приеме событий](../adr/0005-event-ingestion.md), раздел с принятым + решением; +- схему [`ods.event_rep`](../../sql/ddl/20-ods-tables.sql). + +Готовый стендовый разбор находится в `sql/ddl/30-ods-views.sql`. Откройте его +после собственной сверки: в нем лежит ответ на основную часть задания. + +## Граница задания + +Создайте в базе `default` одну свою таблицу годных событий и наполните ее одним +пакетным `INSERT SELECT` из `stg.hits_raw_dist`. Работайте с первой нодой под +пользователем `etl`. + +Матвью `ods.event_mv` и `ods.event_errors_mv` продолжают принимать живой +поток. Оставьте их, STG, ODS, Kafka и даги без изменений. Пересоздание матвью +на работающем стенде оставило бы зазор в приеме, а для пакетного опыта оно не +нужно. + +Дайте своей таблице имя, которое не пересечется с чужой работой, и запишите +его вместе с запросами. В конце вы удалите только эту таблицу. + +## Выберите сырой срез + +STG хранит строки примерно три дня по `_load_ts`. Эта колонка отмечает время +поступления в хранилище. Модельный день события лежит в `EventDate`. Сначала +найдите среди недавних данных завершившийся интервал со строками. Посмотрите +счет по минутам: + +```sql +SELECT + toStartOfMinute(_load_ts) AS minute, + count() AS rows +FROM stg.hits_raw_dist +WHERE _load_ts >= now() - INTERVAL 30 MINUTE + AND _load_ts < toStartOfMinute(now()) +GROUP BY minute +ORDER BY minute DESC; +``` + +Выберите короткий интервал `[slice_from, slice_to)`, который уже закончился и +дает удобное для построчной проверки число строк. Если одна минута велика, +сузьте ее точными границами `_load_ts`. Если недавних строк нет, запустите +`world_live_day`: он распределит события модельного дня по реальным минутам. +Затем повторите поиск. Службы для задания останавливать не нужно. + +Зафиксируйте до разбора: + +- обе границы с миллисекундами и число строк; +- минимальный и максимальный `kafka_offset` по каждой паре + `kafka_topic`, `kafka_partition`. + +Во всех дальнейших запросах читайте `stg.hits_raw_dist` только с этими +границами. Зафиксированная верхняя граница защищает срез от нового потока. +Повторить опыт можно, пока строки не удалил TTL; после этого прежний срез уже +не восстановить из ODS. + +## Соберите собственную таблицу + +Таблица содержит все 47 полей источника с именами и типами +`ods.event_rep`, а также `_load_ts`. Тип `purchaseRevenue` возьмите из ODS: +`Array(Decimal(18, 2))`. + +Повтор доставки должен схлопываться так же, как в ODS. Возьмите +`ReplacingMergeTree(_load_ts)`, партицию `EventDate` и полный ключ замены: +`(CounterID, EventDate, intHash32(ClientID), WatchID)`. Достаточно одной +локальной таблицы: отдельная Distributed-таблица не добавит урока к пакетному +опыту на одной ноде. + +`WatchID` - логический идентификатор события, уникальный по +[договору генератора](../specs/2026-08-01-generator.md). Физический ключ ODS +шире: `ReplacingMergeTree` считает версиями строки с одинаковым полным ключом +сортировки. Используйте его при загрузке и сверке. + +## Разберите годные строки + +Для каждой строки среза примите решение по тому же строгому входу, что и +стенд: + +1. корень `raw` - JSON-объект; +2. набор верхнеуровневых ключей в точности равен 47 ключам формата; +3. `WatchID`, `VisitID`, `ClientID`, `EventDate` и `UTCEventTime` разбираются + в ожидаемые типы. + +Пять опорных полей сначала разбирайте в `Nullable`. К преобразованию в +необнуляемый тип переходите после фильтра, чтобы грязная строка не остановила +весь пакет исключением. Остальные поля извлеките по договору формата. Выберите +сами форму общих выражений и способ не повторять их внутри запроса. Итог +предиката всегда должен быть равен 0 или 1: `NULL` не должен потеряться между +предикатом и его отрицанием. + +Перенесите `_load_ts` из STG без пересчета: это время исходной доставки и +колонка версии. Время запуска вашего запроса сюда не входит. Вставьте в свою +таблицу только годные строки выбранного среза. + +Отдельную таблицу брака строить не нужно. Но решение строгого входа должно +быть наблюдаемым. Одним запросом посчитайте для исходного среза: + +- все доставки; +- строки, прошедшие ваш предикат; +- строки, отвергнутые его отрицанием. + +Первая величина должна равняться сумме двух следующих. Если отвергнутые строки +есть, сохраните их `raw`, `_load_ts` и координаты Kafka. Найдите те же +координаты в `ods.event_errors_dist` и запишите стендовый `error_class`. Если +брака нет, так и зафиксируйте: опыт проверил успешную ветвь и выполнение +предиката на этом срезе, но не классификатор ошибок. Подкладывать грязные +сообщения ради задания не требуется. + +## Приведите стороны к одному зерну + +В STG одна строка представляет доставку. Повтор сообщения остается второй +строкой. В `ods.event_v` одна строка представляет актуальное событие, потому +что представление читает физическую таблицу с `FINAL`. + +Свою сторону тоже читайте с `FINAL`. Ожидаемый счет равен числу различных +полных ключей среди годных доставок. Разница между числом доставок и этим +счетом показывает повторы в исходном срезе. + +Область эталона постройте независимо от своего предиката и результата вставки. +Снова возьмите весь исходный STG-срез. По координатам Kafka отделите доставки, +которые стенд записал в `ods.event_errors_dist`. У оставшихся доставок извлеките +`WatchID` в `Nullable` и выберите эти события из свежего `ods.event_v`. + +Здесь `WatchID` служит мостом между сырой доставкой и ODS: по договору +генератора это уникальный идентификатор события. Он задает только область +эталона. Дедупликация и последующее соединение по-прежнему используют полный +физический ключ. Не берите область из своей таблицы и не применяйте к ней свой +предикат. Иначе ошибочно отвергнутая строка исчезнет с обеих сторон сверки. + +## Сверьте 47 полей + +Соедините свою таблицу и выбранные строки `ods.event_v` через `FULL OUTER +JOIN` по полному ключу замены. Полное соединение должно сохранить строки, +которых нет на одной из сторон. Разделите результат на три исхода: + +- строка есть только в ODS - вы ее потеряли; +- строка есть только у вас - вы приняли лишнее или иначе разобрали часть + ключа; +- ключ есть с обеих сторон, но различается хотя бы одно из 47 полей. + +ClickHouse по умолчанию заполняет отсутствующую сторону значениями типов. +Включите для сверки `join_use_nulls = 1` или добавьте явный признак присутствия +в каждую сторону. Не определяйте отсутствие по значениям полей события. + +Сравните все поля явно, включая массивы, `ecommerce` и точное десятичное +`purchaseRevenue`. Удобный вид диагностики выберите сами: важно, чтобы по +результату можно было назвать отличающиеся колонки одной строки. Итоговая +сводка должна показать число строк каждого из трех исходов. В верном разборе +все три числа равны нулю. + +`_load_ts` не входит в 47 полей источника и не проверяет JSON-разбор. +Проверьте его отдельно: у каждой строки вашей таблицы метка должна быть меткой +одной из доставок того же полного ключа в исходном срезе, а после `FINAL` +должна остаться самая поздняя метка этого ключа внутри среза. С ODS ее не +сравнивайте: там могла победить более поздняя доставка того же события из-за +границ среза. + +## Расследуйте расхождение + +Если сверка не пуста, возьмите одну строку. Вернитесь к независимому списку +ключей и исходному `raw`, выведите рядом координаты Kafka и `_load_ts`. + +Для измененной строки назовите поля, которые расходятся, и сравните каждое с +сырой строкой. Для пропавшей или лишней строки проверьте пять опорных полей и +решение строгого предиката. Если строку отверг стенд, найдите ее в +`ods.event_errors_dist` по координатам доставки и посмотрите `error_class`. + +Исправьте разбор, удалите и заново создайте только собственную таблицу, затем +повторите вставку того же среза. Остановитесь, когда счет исходных доставок по +прежним границам не изменился, баланс строгого входа сошелся, а построчная +сверка пуста. + +## Где заканчивается проверка + +Стендовый ODS дает образец для сравнения. Оба разбора могли повторить одну +ошибку. Для пяти опорных полей у вас есть проверка разбираемости и исходный +JSON. Остальные 42 поля можно вручную сопоставить с `raw`, однако второго +независимого источника для всех значений на стенде нет. + +## Уберите свой объект + +Сохраните DDL таблицы, запрос разбора, границы среза, баланс строгого входа и +пустую итоговую сверку. Затем удалите свою таблицу командой `DROP TABLE` с ее +точным именем. Убедитесь через `system.tables`, что объекта больше нет. +Прикладные строки стенда вы не меняли, поэтому удалять данные из STG, ODS или +Kafka после опыта не нужно. + +## Приемка + +- [ ] записаны границы малого завершенного среза, его счет и координаты + доставки по партициям Kafka; +- [ ] создана одна собственная таблица со всеми 47 полями, перенесенным + `_load_ts` и физическим договором дедупликации ODS; +- [ ] пакетный разбор принимает только JSON-объекты с точным набором ключей и + пятью разбираемыми опорными полями; +- [ ] счет исходных строк равен сумме годных и отвергнутых, а найденный брак + рассмотрен по `raw`, координатам и стендовому классу; +- [ ] область ODS получена из всех координат исходного среза и стендовой + таблицы ошибок, независимо от собственного предиката и таблицы; +- [ ] обе стороны приведены к зерну полного физического ключа; +- [ ] `FULL OUTER JOIN` сравнивает все 47 полей, а числа пропавших, лишних и + измененных строк равны нулю; +- [ ] отдельно подтвержден перенос `_load_ts` и учтены повторы доставки; +- [ ] границы стендового оракула и TTL записаны вместе с результатом; +- [ ] собственная таблица удалена, объекты и данные стенда не изменены. diff --git a/docs/labs/README.md b/docs/labs/README.md index 4aaaf29..39d4c8a 100644 --- a/docs/labs/README.md +++ b/docs/labs/README.md @@ -13,10 +13,10 @@ | 3 | [Нарежь сессии сам](3-1-sessions-by-hand.md) | сверить число сессий в своей нарезке с эталоном и найти события, из-за которых оно расходится | | 4 | [Перепиши нарезку стенда](4-1-session-pipeline.md) | изменить расчетную сущность внутри готового конвейера и проверить ее внешним эталоном | | 5 | [Сначала сторож, потом ломай: заказы](5-1-order-checks.md)
[Перепишите разбор заказов в ODS](5-2-order-parsing.md)
[Перепишите сборку заказов в DDS](5-3-order-pipeline.md) | написать проверки качества до рискованного изменения, затем переписать разбор и сборку заказов | -| 6 | Разбери сырые события и сверь результат со стендом | изучить разбор потока отдельно от живого приема и сверить его со стендовым результатом | +| 6 | [Разбери сырые события и сверь результат со стендом](6-1-event-parsing.md) | изучить разбор потока отдельно от живого приема и сверить его со стендовым результатом | | 7 | Мониторинг: куда смотреть | выбрать, куда и на что смотреть в живой системе | -ТЗ для ступеней 6-7 появятся по мере продвижения программы. +ТЗ для ступени 7 появится по мере продвижения программы. ## Ступень 1: пять витрин слоя DM