docs(labs): уточнено ТЗ разбора событий по ревью

- Зачем:
  - сделать постановку ступени 6 понятнее после редакторского ревью, сохранив её учебный договор и границы.
- Что:
  - оставлена свобода выбрать форму разбора без навязывания Nullable и готовой схемы 0/1.
  - уточнены пакетная обработка, баланс годных строк, модельное и ручное время и убрана лишняя персонификация.
- Проверка:
  - проверены SHA256, `git diff --check` и изменение только `docs/labs/6-1-event-parsing.md`.
This commit is contained in:
ddadmin committed 2026-09-07 03:19:58 -04:00
1 parent 7918eeed25
commit 6e4680d12d
1 file changed
+42 -43
+42 -43
View File
@@ -49,9 +49,12 @@ ORDER BY minute DESC;
Выберите короткий интервал `[slice_from, slice_to)`, который уже закончился и Выберите короткий интервал `[slice_from, slice_to)`, который уже закончился и
дает удобное для построчной проверки число строк. Если одна минута велика, дает удобное для построчной проверки число строк. Если одна минута велика,
сузьте ее точными границами `_load_ts`. Если недавних строк нет, запустите сузьте ее точными границами `_load_ts`. Если недавних строк нет, откройте
`world_live_day`: он распределит события модельного дня по реальным минутам. Airflow. При включенном `world_live` посмотрите его текущий запуск и дочерний
Затем повторите поиск. Службы для задания останавливать не нужно. `world_live_day`: дождитесь свежих строк от текущего или следующего планового
запуска, а при ошибке сначала разберите ее причину. Запустите `world_live_day`
вручную, только если `world_live` стоит на паузе и активного `world_live_day`
нет. Успешный прогон распределяет события модельного дня по реальным минутам.
Зафиксируйте до разбора: Зафиксируйте до разбора:
@@ -60,9 +63,8 @@ ORDER BY minute DESC;
`kafka_topic`, `kafka_partition`. `kafka_topic`, `kafka_partition`.
Во всех дальнейших запросах читайте `stg.hits_raw_dist` только с этими Во всех дальнейших запросах читайте `stg.hits_raw_dist` только с этими
границами. Зафиксированная верхняя граница защищает срез от нового потока. границами. Новые строки с более поздним `_load_ts` в срез не попадут. Повторите
Повторить опыт можно, пока строки не удалил TTL; после этого прежний срез уже опыт до очистки по TTL; после нее прежний срез уже не восстановить из ODS.
не восстановить из ODS.
## Соберите собственную таблицу ## Соберите собственную таблицу
@@ -70,47 +72,45 @@ ORDER BY minute DESC;
`ods.event_rep`, а также `_load_ts`. Тип `purchaseRevenue` возьмите из ODS: `ods.event_rep`, а также `_load_ts`. Тип `purchaseRevenue` возьмите из ODS:
`Array(Decimal(18, 2))`. `Array(Decimal(18, 2))`.
Повтор доставки должен схлопываться так же, как в ODS. Возьмите Схлопните повтор доставки по договору ODS. Возьмите
`ReplacingMergeTree(_load_ts)`, партицию `EventDate` и полный ключ замены: `ReplacingMergeTree(_load_ts)`, партицию `EventDate` и полный ключ замены:
`(CounterID, EventDate, intHash32(ClientID), WatchID)`. Достаточно одной `(CounterID, EventDate, intHash32(ClientID), WatchID)`. Достаточно одной
локальной таблицы: отдельная Distributed-таблица не добавит урока к пакетному локальной таблицы: отдельная Distributed-таблица не добавит урока к пакетному
опыту на одной ноде. опыту на одной ноде.
`WatchID` - логический идентификатор события, уникальный по `WatchID` — логический идентификатор события, уникальный по
[договору генератора](../specs/2026-08-01-generator.md). Физический ключ ODS [договору генератора](../specs/2026-08-01-generator.md). Физический ключ ODS
шире: `ReplacingMergeTree` считает версиями строки с одинаковым полным ключом шире: в `ReplacingMergeTree` версиями считаются строки с одинаковым полным
сортировки. Используйте его при загрузке и сверке. ключом сортировки. Используйте его при загрузке и сверке.
## Разберите годные строки ## Разберите годные строки
Для каждой строки среза примите решение по тому же строгому входу, что и Для каждой строки среза примите решение по тому же строгому входу, что и
стенд: стенд:
1. корень `raw` - JSON-объект; 1. корень `raw` — JSON-объект;
2. набор верхнеуровневых ключей в точности равен 47 ключам формата; 2. набор верхнеуровневых ключей в точности равен 47 ключам формата;
3. `WatchID`, `VisitID`, `ClientID`, `EventDate` и `UTCEventTime` разбираются 3. `WatchID`, `VisitID`, `ClientID`, `EventDate` и `UTCEventTime` разбираются
в ожидаемые типы. в ожидаемые типы.
Пять опорных полей сначала разбирайте в `Nullable`. К преобразованию в Напишите разбор, который не прерывает пакет из-за одной грязной строки. Сами
необнуляемый тип переходите после фильтра, чтобы грязная строка не остановила выберите способ безопасного извлечения пяти опорных полей, форму общих выражений
весь пакет исключением. Остальные поля извлеките по договору формата. Выберите и место фильтра. Остальные поля извлеките по договору формата.
сами форму общих выражений и способ не повторять их внутри запроса. Итог
предиката всегда должен быть равен 0 или 1: `NULL` не должен потеряться между
предикатом и его отрицанием.
Перенесите `_load_ts` из STG без пересчета: это время исходной доставки и Перенесите `_load_ts` из STG без пересчета: это время исходной доставки и
колонка версии. Время запуска вашего запроса сюда не входит. Вставьте в свою колонка версии. Время запуска вашего запроса сюда не входит. Вставьте в свою
таблицу только годные строки выбранного среза. таблицу только годные строки выбранного среза.
Отдельную таблицу брака строить не нужно. Но решение строгого входа должно Считайте отвергнутые строки прямо в исходном срезе: отдельная таблица брака в
быть наблюдаемым. Одним запросом посчитайте для исходного среза: задание не входит. Проверьте работу строгого входа одним запросом. Посчитайте:
- все доставки; - все доставки;
- строки, прошедшие ваш предикат; - строки, прошедшие ваш предикат;
- строки, отвергнутые его отрицанием. - строки, отвергнутые его отрицанием.
Первая величина должна равняться сумме двух следующих. Если отвергнутые строки Проверьте равенство общего числа строк сумме прошедших и отвергнутых. Так каждая
есть, сохраните их `raw`, `_load_ts` и координаты Kafka. Найдите те же исходная строка учитывается ровно в одной группе. Если отвергнутые строки есть,
сохраните их `raw`, `_load_ts` и координаты Kafka. Найдите те же
координаты в `ods.event_errors_dist` и запишите стендовый `error_class`. Если координаты в `ods.event_errors_dist` и запишите стендовый `error_class`. Если
брака нет, так и зафиксируйте: опыт проверил успешную ветвь и выполнение брака нет, так и зафиксируйте: опыт проверил успешную ветвь и выполнение
предиката на этом срезе, но не классификатор ошибок. Подкладывать грязные предиката на этом срезе, но не классификатор ошибок. Подкладывать грязные
@@ -127,24 +127,25 @@ ORDER BY minute DESC;
счетом показывает повторы в исходном срезе. счетом показывает повторы в исходном срезе.
Область эталона постройте независимо от своего предиката и результата вставки. Область эталона постройте независимо от своего предиката и результата вставки.
Снова возьмите весь исходный STG-срез. По координатам Kafka отделите доставки, Снова возьмите весь исходный STG-срез. По координатам Kafka исключите доставки,
которые стенд записал в `ods.event_errors_dist`. У оставшихся доставок извлеките которые есть в `ods.event_errors_dist`. У оставшихся доставок извлеките
`WatchID` в `Nullable` и выберите эти события из свежего `ods.event_v`. `WatchID` без риска прервать запрос и выберите эти события из свежего
`ods.event_v`.
Здесь `WatchID` служит мостом между сырой доставкой и ODS: по договору Свяжите сырую доставку с ODS по `WatchID`: по договору генератора это уникальный
генератора это уникальный идентификатор события. Он задает только область идентификатор события. Он задает только область эталона. Дедупликация и
эталона. Дедупликация и последующее соединение по-прежнему используют полный последующее соединение используют полный физический ключ. Не берите область из
физический ключ. Не берите область из своей таблицы и не применяйте к ней свой своей таблицы и не применяйте к ней свой предикат. Иначе ошибочно отвергнутая
предикат. Иначе ошибочно отвергнутая строка исчезнет с обеих сторон сверки. строка исчезнет с обеих сторон сверки.
## Сверьте 47 полей ## Сверьте 47 полей
Соедините свою таблицу и выбранные строки `ods.event_v` через `FULL OUTER Соедините свою таблицу и выбранные строки `ods.event_v` через `FULL OUTER
JOIN` по полному ключу замены. Полное соединение должно сохранить строки, JOIN` по полному ключу замены. Полное соединение сохраняет строки,
которых нет на одной из сторон. Разделите результат на три исхода: которых нет на одной из сторон. Разделите результат на три исхода:
- строка есть только в ODS - вы ее потеряли; - строка есть только в ODS — вы ее потеряли;
- строка есть только у вас - вы приняли лишнее или иначе разобрали часть - строка есть только у вас — вы приняли лишнее или иначе разобрали часть
ключа; ключа;
- ключ есть с обеих сторон, но различается хотя бы одно из 47 полей. - ключ есть с обеих сторон, но различается хотя бы одно из 47 полей.
@@ -154,16 +155,14 @@ ClickHouse по умолчанию заполняет отсутствующую
Сравните все поля явно, включая массивы, `ecommerce` и точное десятичное Сравните все поля явно, включая массивы, `ecommerce` и точное десятичное
`purchaseRevenue`. Удобный вид диагностики выберите сами: важно, чтобы по `purchaseRevenue`. Удобный вид диагностики выберите сами: важно, чтобы по
результату можно было назвать отличающиеся колонки одной строки. Итоговая результату можно было назвать отличающиеся колонки одной строки. Выведите число
сводка должна показать число строк каждого из трех исходов. В верном разборе строк каждого из трех исходов. В верном разборе все три числа равны нулю.
все три числа равны нулю.
`_load_ts` не входит в 47 полей источника и не проверяет JSON-разбор. `_load_ts` не входит в 47 полей источника и не проверяет JSON-разбор.
Проверьте его отдельно: у каждой строки вашей таблицы метка должна быть меткой Проверьте его отдельно. Свяжите каждую строку своей таблицы с меткой одной из
одной из доставок того же полного ключа в исходном срезе, а после `FINAL` доставок того же полного ключа в исходном срезе. После `FINAL` остается самая
должна остаться самая поздняя метка этого ключа внутри среза. С ODS ее не поздняя метка этого ключа внутри среза. С ODS ее не сравнивайте: там может
сравнивайте: там могла победить более поздняя доставка того же события из-за храниться более поздняя доставка того же события из-за границ среза.
границ среза.
## Расследуйте расхождение ## Расследуйте расхождение
@@ -172,8 +171,8 @@ ClickHouse по умолчанию заполняет отсутствующую
Для измененной строки назовите поля, которые расходятся, и сравните каждое с Для измененной строки назовите поля, которые расходятся, и сравните каждое с
сырой строкой. Для пропавшей или лишней строки проверьте пять опорных полей и сырой строкой. Для пропавшей или лишней строки проверьте пять опорных полей и
решение строгого предиката. Если строку отверг стенд, найдите ее в решение строгого предиката. Если координаты доставки есть в
`ods.event_errors_dist` по координатам доставки и посмотрите `error_class`. `ods.event_errors_dist`, посмотрите `error_class`.
Исправьте разбор, удалите и заново создайте только собственную таблицу, затем Исправьте разбор, удалите и заново создайте только собственную таблицу, затем
повторите вставку того же среза. Остановитесь, когда счет исходных доставок по повторите вставку того же среза. Остановитесь, когда счет исходных доставок по
@@ -182,7 +181,7 @@ ClickHouse по умолчанию заполняет отсутствующую
## Где заканчивается проверка ## Где заканчивается проверка
Стендовый ODS дает образец для сравнения. Оба разбора могли повторить одну `ods.event_v` — образец для сравнения. Оба разбора могли повторить одну
ошибку. Для пяти опорных полей у вас есть проверка разбираемости и исходный ошибку. Для пяти опорных полей у вас есть проверка разбираемости и исходный
JSON. Остальные 42 поля можно вручную сопоставить с `raw`, однако второго JSON. Остальные 42 поля можно вручную сопоставить с `raw`, однако второго
независимого источника для всех значений на стенде нет. независимого источника для всех значений на стенде нет.