- Зачем: - спеке программы лаб (#202) нужен полный веер сюжетов вместо семи случайно накопленных идей; резолюция тикета #201 карты #182. - Что: - заметка docs/research/2026-08-31-labs-inventory.md: 32 сюжета по слоям STG, ODS, DDS, DM, BI и поперек слоев, три формы заданий, разбор гипотезы «вычитание + DQ» с таблицей по потокам. - Проверка: - фактура (пути, имена таблиц, номера тикетов) сверена с репозиторием при вычитке; кода изменение не касается. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
44 KiB
Инвентаризация сюжетов лаб по слоям стенда
Это материал для спеки программы лаб (#202): полный список сюжетов без отбора. Спека решит, что оставить и в каком порядке поставить. Здесь для каждого участка стенда записано, чему на нем научится менти. Тикет — #201, карта — #182.
Каждый сюжет описан четырьмя строками: урок, материал, форма и проверка. Возможны три формы:
- построить свое по ТЗ — как в пяти домашних заданиях DM: рядом есть образец, но нет готового ответа;
- вычитание — удалить SQL готовой трансформации и написать его заново; даги остаются без изменений (уточнение владельца в комментарии к #148);
- расследование — данные уже лежат на стенде или стенд намеренно ломается, а менти ищет причину и объясняет ее.
Существующие шесть заданий из docs/labs/ входят в список как готовое ядро слоя DM. Идеи из тикетов #143, #145, #148, #149, #186, #187, #188 разнесены по подходящим участкам. В конце отдельно проверяется гипотеза «вычитание + DQ» для каждого потока нижних слоев.
STG — прием и сырье
1. Push против pull: два способа принять топик.
Урок: способ приема зависит от источника. Поток постоянно читает подписанная матвью, а слепок забирает отдельный запрос по команде.
Материал: оба чтеца в sql/ddl/10-stg-tables.sql (у orders_raw_kafka нет ни матвью, ни ON CLUSTER, зато есть kafka_commit_on_select), матвью приема sql/ddl/40-stg-views.sql, забор sql/stg/orders_raw_load.sql, dags/orders_ingest.py; сравнение цены и наблюдаемости — заготовка спеки (docs/specs/2026-07-30-stand-v2-realism.md, раздел 12).
Форма: расследование двух живых путей на одном стенде.
Проверка: офсеты групп clickstream_hits и clickstream_orders, счет строк сырья по _load_id.
2. Кто читал топик и куда легли данные.
Урок: нода, которая читает топик, может меняться от прогона к прогону. Раскладка данных не меняется: ее определяет ключ шардирования — свойство самих данных.
Материал: колонка consumer_host в stg.hits_raw_dist; docs/architecture/storage.md, «Раскладка по шардам».
Форма: расследование данных.
Проверка: пересчет ключа шардирования по hostName() — строк на чужом шарде нет.
3. Вторая consumer group — дубли.
Урок: на кластер нужна одна группа потребителей. Если завести вторую, каждая нода получит полную копию топика.
Материал: kafka_group_name в sql/ddl/10-stg-tables.sql; сцена названа спекой (раздел 12).
Форма: расследование-опыт «сломай — увидь — верни» на переигранном дне.
Проверка: счет событий против описи мира (make check-clickhouse); уборка — снятием партиции _load_ts (правило из docs/architecture/testing.md).
4. Дырка в офсетах.
Урок: сырье может сохранить не все сообщения, которые пришли из источника. Пустое сообщение или зазор после пересоздания матвью видны только по разрыву в kafka_offset, поэтому пропажу не нужно искать глазами.
Материал: docs/architecture/storage.md, «Что проверено» (опыты 5 и 7 августа); колонка kafka_offset в сырье.
Форма: расследование.
Проверка: непрерывность офсетов внутри партиции.
5. Две оси времени.
Урок: переобработка данных и очистка диска живут по разным часам. Сырье разбивается на части и удаляется по _load_ts, а модельный день появляется только в ODS. Поэтому TTL по модельной дате работал бы неверно.
Материал: sql/ddl/10-stg-tables.sql (TTL трое суток, ttl_only_drop_parts), docs/architecture/storage.md, «Срок жизни сырья».
Форма: расследование-вопрос (почему нельзя нарезать сырье по EventDate).
Проверка: ответ сверяется с доводом доки; счетного инварианта нет.
ODS — типизация и брак
6. Предикат и его буквальное отрицание.
Урок: годные строки и брак должны делить поток без пропусков и пересечений. NULL в предикате незаметно теряет строку, а исключение роняет вставку и останавливает чтение топика.
Материал: sql/ddl/30-ods-views.sql (пара матвью событий), sql/ods/order_parse.sql (макрос order_wire_contract), ADR 0005.
Форма: расследование — подложить в топик по строке каждого класса брака и найти их в ods.event_errors_dist / ods.order_errors_dist.
Проверка: «сырье среза = годные + брак» на управляемой пачке; строки опыта убрать за собой.
7. Три времени и версия-наблюдение.
Урок: у строки заказа есть четыре временные координаты. Версию сущности задает дата слепка источника, а не время загрузки. FINAL живет ровно в одном месте — за представлением _v.
Материал: sql/ddl/20-ods-tables.sql, sql/ddl/30-ods-views.sql, ADR 0010 и 0014, docs/architecture/orders/ingestion.md («Роль ODS»).
Форма: расследование — физические версии ods.order_dist против ods.order_v.
Проверка: эталон argMax по updated_at прямо из сырья (прием #165).
8. Часовой пояс как способ смотреть на время.
Урок: у ночных событий toDate(UTCEventTime) может не совпадать с EventDate. Это нормальное свойство данных. День нужно считать по готовой колонке и явно называть часовой пояс, в котором его рассматривают.
Материал: docs/architecture/storage.md, «Часовые пояса»; ADR 0015.
Форма: расследование данных.
Проверка: менти сам находит ночные события и объясняет расхождение.
9. Сырое против разобранного: ecommerce.
Урок: типизированный разбор JSON может молча подменить ошибку значением. Например, qty со значением «oops» превратится в 0. Поэтому правильность разбора нужно подтверждать инвариантом источника.
Материал: колонка ecommerce рядом с массивами purchase_* / product_* в ods.event; разбор items в sql/dds/order_rebuild.sql; спека, раздел 1.2 («кандидат будущей лабы»).
Форма: построить свое по ТЗ — разобрать ecommerce запросом и сверить с готовыми массивами того же события.
Проверка: построчная сверка с колонками-массивами.
DDS — модель
10. Лаба сессий: своя нарезка против эталона.
Урок: сессия — расчетная сущность, границы которой определяет логика в хранилище. Самостоятельная нарезка может разойтись с эталоном. Если расхождение невозможно, задание сводится к GROUP BY по готовому полю.
Материал: docs/architecture/dds/session.md, sql/dds/session_rebuild.sql (два окна, полный порядок (event_time, event_id), детерминированный чекан), эталон VisitID в ods.event_v.
Форма: построить свое — менти собирает нарезку сам до знакомства со стендовым кодом; либо вычитание session_rebuild.sql (раздел о гипотезе ниже).
Проверка: dq_check / sessions_vs_reference (зерно кука × день против uniqExact(VisitID)); затем сравнение со стендовой нарезкой по session_id.
11. Поштучный разбор расхождения нарезки.
Урок: счетная сверка показывает, сколько сессий разошлось. Чтобы понять, где именно отличаются границы, нужно вручную соединить свою сессию с эталонной по времени.
Материал: sql/dq/sessions_vs_reference.sql, docs/architecture/dds/session.md, «Сверка с эталоном» («поштучное сопоставление границ — материал лабы»).
Форма: расследование.
Проверка: найденная кука, день и пауза, на которой решения разошлись.
12. Склейка идентичностей во времени (#145).
Урок: текущее знание о пользователе и знание на выбранную дату дают разные отчеты. Если использовать сведения из будущего, исторический график известной аудитории изменится.
Материал: dds.identity_map_v (пары без временной колонки — решение docs/architecture/dds/identity.md), момент связи выводится как минимум времени purchase-события, соединенного с заказом.
Форма: построить свое (запрос, без изменения стенда).
Проверка: два способа на одном наборе данных; расхождение объясняется письменно.
13. Справочник регионов (#143).
Урок: справочник содержит данные, которыми источник владеет вне хранилища. Если собрать словарь руками, можно разобраться в механике, которую готовый каталог стенда позволяет только использовать.
Материал: образец sql/ddl/05-dic-catalog.sql и ADR 0012; зацепка в контракте — region_city_id / region_country_id отдаются как есть (dds.event_v).
Форма: построить свое по образцу (свой CSV, подложка, словарь, dictGet в отчете).
Проверка: обогащенный отчет; неатомарность обновления словаря на двух нодах — дополнительное наблюдение (спека, раздел 12).
14. Инвариант позиций: недостающая проверка.
Урок: равенство «цена × количество = items_total» проверяет, не исказил ли молчаливый разбор данные. Сейчас это равенство проверяют вручную, хотя ему нужно постоянное место в контуре DQ.
Материал: комментарий в sql/dds/order_rebuild.sql («постоянное место такой проверки — контур DQ этапа 4; пока она делается руками»), образцы sql/dq/*_vs_dds.sql, dags/dq_check.py.
Форма: построить свое — новая проверка по образцу существующих пяти.
Проверка: проверка обязана уметь краснеть — испортить позицию руками и увидеть красное (docs/architecture/testing.md).
15. Сверка слоя с источником по дню.
Урок: трансформация выполняет заданное правило, но сама не замечает, что часть данных не приехала. Это обнаруживает отдельная сверка слоя с источником.
Материал: docs/architecture/etl.md, «Чего конвейер о себе не знает» (три слепых пятна и лекарство: счет заказов и поздний updated_at с обеих сторон).
Форма: построить свое — вторая самодельная DQ-проверка.
Проверка: снести часть дня в ODS-окне — проверка краснеет, конвейер сам не замечал.
DM — витрины
16. Пять домашек DM (готовое ядро).
Урок: готовые ТЗ уже содержат пять отдельных уроков (docs/labs/README.md): events_enriched_v показывает, что слой задает контракт, а не копирует данные; top_pages_daily меняет зерно, материализует витрину и включает ее в конвейер; session_overview учит хранить слагаемые вместо средних и не складывать уникальные значения, даже если на текущих данных результат совпадает; dq_errors_daily показывает, что у брака есть собственный день и срок жизни, а витрина может пережить источник; utm_effectiveness объясняет, зачем в имени нужно предупреждение declared_.
Материал: пять готовых ТЗ в docs/labs/README.md.
Форма: построить свое по ТЗ.
Проверка: dags/homework_check.py + sql/dq/*_vs_dds.sql (полное внешнее соединение с независимым ответом).
17. Свой дашборд в Superset (готовая необязательная лаба).
Урок: пользовательские объекты BI хранятся отдельно от стендовых и переживают перезапуск. Стендовые объекты при импорте перезаписываются.
Материал: docs/labs/superset-dashboard.md, docs/architecture/superset.md.
Форма: построить свое.
Проверка: глазами — счетного инварианта у раскладки панелей нет.
18. Инкремент против пересборки (#149, повышенная сложность).
Урок: чтобы обновлять меньше данных, приходится хранить историю изменений. Полная перезагрузка эту историю стирает.
Материал: sql/dm/daily_traffic_rebuild.sql (полный пересчет), docs/architecture/dm.md, «Ритм пересборки» и отклоненный вариант; момент first_matched_at — из сюжета 12.
Форма: построить свое (инкрементальная альтернатива рядом со стендовой).
Проверка: бесплатный контроль — результат обязан совпасть с эталонной витриной dm.daily_traffic_v.
19. Вычитание стендовых витрин.
Урок: при сверке двух источников нужен GLOBAL FULL OUTER JOIN с подзапросами с обеих сторон, join_use_nulls и явный приоритет классов. Значение с отсутствующей стороны должно остаться NULL, а не превратиться в ноль.
Материал: sql/dm/purchase_vs_orders_rebuild.sql (и соседние revenue_daily_*, daily_traffic_*), docs/architecture/dm.md.
Форма: вычитание — с оговоркой: запросы sql/dq/*_vs_dds.sql лежат открыто и почти дословно повторяют рецепт витрины. Предупреждение из docs/labs/README.md («открыв их до того, как задание сделано, ты обменяешь урок на готовый SQL») здесь особенно важно.
Проверка: dq_check, группа marts_vs_dds + classes_vs_inventory.
BI — Superset (сюжеты сверх готовой лабы)
20. Ловушка ноды 2.
Урок: забытый ON CLUSTER и представление над локальной таблицей могут проявиться только в дашборде. Superset намеренно читает ноду 2, а DDL запускается с ноды 1.
Материал: docs/architecture/superset.md, «Устройство зоны»; спека, «Роли нод».
Форма: расследование-опыт (создать объект без ON CLUSTER, открыть дашборд).
Проверка: своя же находка и починка.
Поперек слоев
Airflow-переобработка по дневным партициям
21. Переиграть день X.
Урок: объем повторного прогона определяется состоянием хранилища. После падения конвейеру не нужно помнить параметры прошлого запуска: он ищет первую несобранную дату, а не последнюю собранную.
Материал: sql/dds/order_scope.sql и session_scope.sql (окно + хвост от первой дыры, GLOBAL NOT IN), dags/dds_transform.py (день — отдельная размноженная задача замены), docs/architecture/etl.md.
Форма: расследование-опыт — снести партицию дня в dds.session_rep, запустить etl_pipeline без параметров, увидеть, как объем сам нашел дыру.
Проверка: dq_check зеленый, день вернулся тем же (детерминированный чекан).
22. Атомарность — пошардовая.
Урок: REPLACE PARTITION заменяет данные в локальных таблицах по одному шарду. Если одна из двух замен завершится ошибкой, половина дня останется старой. Сам конвейер этого не заметит, поэтому нужна отдельная сверка.
Материал: sql/dds/order_replace.sql (комментарий о границе обещания), docs/architecture/etl.md, «Чего конвейер о себе не знает»; правило донора — sql/ddl/45-dds-tables.sql.
Форма: расследование (в паре с сюжетом 15).
Проверка: purchase_vs_orders_vs_dds или самодельная сверка по дню.
Сверка purchase vs orders (классы A–D)
23. Воронка подтверждений и недосчет трекера (#186, класс B).
Урок: со стороны трекера потерянное событие можно обнаружить только по разрыву воронки между /confirmation и purchase. Отсюда правило: поведение считаем по трекеру, деньги — по бэкенду.
Материал: lost_event в dm.purchase_vs_orders_v, события дня в dds.event_v; механика — docs/architecture/orders/fate.md («потеря точечная»).
Форма: расследование.
Проверка: оцененный недосчет сходится с долей lost_event закрытых дней.
24. Бизнес-дубль и наивная выручка (#187, класс D).
Урок: движок удаляет технические дубли, а бизнес-дубли приходится определять самим — по purchaseID, а не по совпадению строк. ReplacingMergeTree не удалит дубль с новым WatchID, и это правильное поведение.
Материал: duplicate_event, свертка по purchase_id[1] в sql/dm/purchase_vs_orders_rebuild.sql; fate.md («дубль сюжетный»).
Форма: расследование — наивный SUM против дедуплицированного.
Проверка: разница равна вкладу дублей; сверка с dm.revenue_daily_v.
25. Дельта суммы объясняется сравнением позиций (класс C).
Урок: amount_delta обозначает конкретное расхождение, а не случайный шум. Его можно до копейки объяснить вычеркнутой позицией, если разобрать вложенный JSON и применить ARRAY JOIN. Ради этого навыка позиции и разбираются.
Материал: amount_delta в сверке, массивы product_* события против item_* заказа; fate.md («дельта — вычеркнутая позиция»).
Форма: расследование.
Проверка: найденная позиция и ее полная стоимость равны дельте.
26. Две дороги отмены и история версий (класс A).
Урок: текущее состояние заказа не сохраняет его историю. Различить случаи «оплачен и отменен» и «отменен неоплаченным» можно только по следам: физическим версиям ODS до слияния и сырью STG, пока оно хранится трое суток.
Материал: fate.md, «Что из двух дорог видно»; ods.order_dist, stg.orders_raw_dist; выручка дня, которая выросла и убыла.
Форма: расследование с ограничением по времени — часть случаев неразличима принципиально, и признать это тоже урок.
Проверка: списки заказов по дорогам, сверенные с историей версий.
Опоздания и закрытие дня
27. Опоздавшие заказы и закрытое окно (#188).
Урок: отчет за день зависит от момента расчета. День можно считать закрытым, когда источник уже не способен его изменить. Поэтому опись хранит счетчики классов только для закрытых дней.
Материал: awaiting_order в dm.purchase_vs_orders_v, задержка слепков D+1/D+2 (fate.md, «Опоздание»), data/world-inventory.json (счетчики orders только у дней с закрытым окном), возврат awaiting_order-дней в объем — sql/dm/purchase_vs_orders_scope.sql.
Форма: расследование по ходу проживаемых дней.
Проверка: вчерашний отчет, не сошедшийся утром, сходится завтра; разность слепков называет опоздавших.
28. Дыхание выручки.
Урок: значение выручки за день меняется, пока день внутри окна изменяемости, а после выхода из окна фиксируется. Эти изменения видны как повторные пересборки партиции.
Материал: dm.revenue_daily_v по ходу дней, окно из ярлыка snapshot_date (sql/dm/revenue_daily_scope.sql), K = 7 — константа мира.
Форма: расследование-наблюдение (снять один день несколько раз).
Проверка: за окном значение больше не меняется.
Мониторинг и runbook
В репозитории пока нет runbook для случаев «keeper упал / DDL повис». Спека оставляет его в хвосте этапа 9 вместе с system.text_log (ADR 0002), потому что такой документ должен опираться на реальный опыт. Поэтому менти может написать runbook сам: по шагам сломать сценарий, записать наблюдения и оставить стенду документ, которого сейчас не хватает.
29. Keeper — общая память кластера.
Урок: без keeper останавливаются DDL и репликация. На дашборде это бывает видно раньше, чем в ошибках запросов. Читаемый путь в keeper помогает понять, какие данные он хранит и за что отвечает.
Материал: панель keeper и очередь DDL в docs/architecture/monitoring/cluster.md, system.zookeeper (читаемый путь — docs/architecture/storage.md, «Путь реплицированных таблиц»), make smoke / make check-clickhouse.
Форма: расследование-опыт «посмотрели — сломали — увидели — вернули» (остановить keeper), итог — свой runbook.
Проверка: возврат к зеленым целям make; шаги записаны.
30. Повисший DDL.
Урок: каждый запрос ON CLUSTER проходит через keeper. Очередь распределенных DDL показывает, дошел ли запрос до обеих нод и что ответила каждая из них.
Материал: system.distributed_ddl_queue, панель «Распределенный DDL» (cluster.md: единственная панель без clusterAllReplicas — и почему).
Форма: расследование-опыт (DDL при недоступной второй ноде, таймаут ON CLUSTER).
Проверка: запись в очереди со статусом по нодам; починка доводит до Finished.
31. Мониторинг сам стоит денег.
Урок: запросы Grafana попадают в system.query_log вместе с запросами людей. Самый частый клиент хранилища — сам мониторинг. Если отфильтровать его запросы, стоимость наблюдаемости станет невидимой.
Материал: docs/architecture/monitoring/queries.md (панель «Кто спрашивает», строки начала и конца, куски шардов в панели ошибок).
Форма: расследование.
Проверка: собственное объяснение верха списка тяжелых запросов.
32. Свежесть второго источника — пила, а не линия.
Урок: возраст заказов растет в течение дня и резко уменьшается после нового слепка. Так устроен источник; это не признак деградации. Для панелей свежести, потока и брака выбираются разные слои данных. Если выбрать неверный слой, панель даст правдоподобный, но ложный ответ.
Материал: docs/architecture/monitoring/data.md (почему свежесть — ODS, поток и брак — сырье, окно доли брака).
Форма: расследование.
Проверка: предсказать форму графика до взгляда на него.
За границей лаб — опорные точки лекций
Спека (раздел 12) оставляет несколько сюжетов для лекций и не включает их в лабораторные работы: Sign и CollapsingMergeTree (почему на стенде sum(Sign) равен count(), а в бою нет), «как выбирают топологии в бою», идентичность «как в бою» (setUserID, вероятностная склейка, identity graph), анти-паттерны ключа шардирования. Последний сюжет уже входит в домашние задания DM как требование защитить выбранный ключ (docs/labs/README.md).
Гипотеза «вычитание + DQ» для нижних слоев
В #148 предложено удалить SQL трансформаций потока и написать его заново. Владелец уточнил, что даги менять не нужно: это несущая инфраструктура. Обратную связь дают инварианты, которые не зависят от реализации слоя. Ниже каждый поток проверяется отдельно: что можно отдать на переписывание, что должно остаться, чем проверять результат и где заканчивается эталон.
На всю таблицу влияют два общих факта:
homework_checkпокрывает только пять витрин DM.dq_checkсодержит пять проверок, но внешний эталон есть лишь у двух:sessions_vs_referenceиспользует чужой чеканVisitIDизods.event_v, аclasses_vs_inventory— опись мира. Три проверки*_vs_ddsпересчитывают витрину из DDS по тому же рецепту. Они находят ошибку в сборке витрины, но не ошибку в DDS, потому что обе стороны читают один слой.- Все сверки читают таблицы
_dist, поэтому их агрегаты не зависят от раскладки (спека, раздел 6). Если строка попала не на свой шард, счетные проверки останутся зелеными. Это ловит только пересчет ключа шардирования поhostName()(прием изdocs/architecture/storage.md, замер #137). Такой пересчет нужно явно включать в ТЗ каждого вычитания.
По потокам
События STG (прием). На переписывание этот участок не отдается. Здесь почти нет трансформации: stg.hits_raw_mv перекладывает колонки (sql/ddl/40-stg-views.sql). При этом пересоздание матвью на работающем стенде незаметно пропускает ближайшие сообщения (замер в storage.md, «Что проверено»). Чтец, таблицы сырья и матвью остаются несущей стеной. Урок этого участка раскрывают расследования из сюжетов 1–5.
События STG → ODS (разбор). Вся логика — в DDL, в паре матвью sql/ddl/30-ods-views.sql; отдельных файлов событий в sql/ods/ нет. Удаление и пересоздание этих матвью остановит прием и снова создаст зазор с пропущенными сообщениями. Поэтому обычное вычитание здесь не подходит. Рабочая форма — «собери рядом»: выполнить пакетный INSERT SELECT из сырья в собственную таблицу по срезу _load_ts, а затем построчно сверить результат с нетронутым ods.event_v. Эталоном служат стендовый ODS и подневный счет против описи (make check-clickhouse). Однако опись проверяет только количество и только для покрытых ею дней. Содержимое неопорных колонок остается без независимой проверки: неверно разобранные UTM или браузер нигде не покраснеют, потому что витрины сверяются с DDS, а DDS читает ODS. Срок работы тоже ограничен: сырье хранится трое суток.
Заказы STG (забор, sql/stg/orders_raw_load.sql). Формально трансформация вынесена в отдельный файл, но ее учебный смысл сводится к перекладке колонок, а цена ошибки велика. Офсеты коммитятся при чтении, поэтому неверный повтор унесет слепок до следующего переигрыша дня через пульт. Этот участок остается стеной. Эталон описи — хеш байтов слепка — проверяет топик, но не сам забор.
Заказы STG → ODS (sql/ods/order_parse.sql). Вычитание возможно с оговоркой. Это пакетный шаг, идемпотентный по _load_id: повтор не меняет ods.order_v («Что проверено» в orders/ingestion.md). Брак обрабатывается отдельной целью, а слепок можно вернуть переигрышем дня. Обратную связь менти собирает сам: проверяет равенство «сырье среза = годные + брак» по _load_id, а текущее состояние сверяет с argMax по updated_at из сырья. Ниже по течению результат проверяет classes_vs_inventory через DDS и витрину. Слабое место — ветвь брака. Чистый мир не производит брак, поэтому отрицание предиката останется непроверенным, пока менти сам не подложит по строке каждого класса. Готовый рецепт есть в orders/ingestion.md, «Риски и проверка»; после опыта данные нужно обязательно убрать.
DDS сессия (session_scope / rebuild / replace). Это лучший поток для вычитания: здесь гипотеза подтверждается без дополнительных условий. Эталон VisitID внешний, а sessions_vs_reference.sql считает чужой чекан и не повторяет алгоритм нарезки. Ошибиться можно в важных местах: в полном порядке окон, границе суток в PARTITION BY, детерминированном чекане и расчете объема из позиции мира. Эталон заканчивается на атрибутах строки. Сверка работает на зерне кука × день и проверяет только число сессий. Поэтому неверные границы, длительность, вход, выход или UTM могут пройти зеленым. session_overview из homework_check тоже согласится, потому что сверяет витрину с тем же слоем. Вторая ступень обратной связи — поштучная сверка границ по VisitID из сюжета 11, которую менти пишет сам.
DDS заказ (order_scope / rebuild / replace). Вычитание возможно при условии: сначала нужно построить недостающую проверку. Косвенный эталон уже есть. classes_vs_inventory ловит потерянные заказы и неверные статусы закрытых дней, а purchase_vs_orders проверяет items_total. Но независимо не проверяются разбор массивов item_* и время в поясе Europe/Samara. Выручка сойдется сама с собой, а счетчиков выручки в описи намеренно нет (спека, раздел 8). Стенд уже подсказывает решение: до вычитания нужно реализовать инвариант «цена × количество = items_total» из сюжета 14. Последовательность «сначала сторож, потом ломать» сама становится частью урока.
DDS карта идентичностей (identity_map_rebuild.sql). Для чистого вычитания этот поток не подходит: независимого эталона нет. daily_traffic_vs_dds читает карту с обеих сторон, поэтому пустая или задвоенная карта пройдет проверку. Счетчики идентичности в опись не входят (спека, раздел 8). Менти может сам написать слабые инварианты: known_users <= visitors и «каждая пара подтверждена покупкой и заказом». Они проверят допустимость пар, но не их полноту. Подходящие формы — «собери рядом и сверь со стендовой картой» либо вычитание вместе с самодельной проверкой, которая честно названа неполной.
DM стендовые витрины. Формально это самый защищенный участок: dq_check независимо пересчитывает каждую витрину. На деле проверка содержит готовый ответ: revenue_daily_vs_dds.sql дословно повторяет рецепт витрины. Вычитание возможно как сюжет 19, но только с предупреждением из docs/labs/README.md о цене подглядывания.
Сводная таблица
| Поток | Отдать на переписывание | Несущая стена | Обратная связь | Где кончается эталон |
|---|---|---|---|---|
| События STG | ничего | чтец, сырье, hits_raw_mv |
опись (счет) | все, кроме счета |
| События STG→ODS | «собери рядом», не вычитание | обе матвью, *_errors |
стендовый ODS рядом, опись | содержимое неопорных колонок; окно сырья 3 суток |
| Заказы STG | ничего | чтец, забор | хеш слепка в описи | сам забор |
| Заказы STG→ODS | order_parse.sql |
*_errors, чтец |
баланс среза по _load_id, argMax(updated_at), опись классов |
ветвь брака — пока не подложен брак руками |
| DDS сессия | session_* (все три) |
донор, DDL, даг | sessions_vs_reference (внешний эталон) |
атрибуты строки — нужна поштучная сверка |
| DDS заказ | order_* (все три) |
донор, DDL, даг | опись классов, items_total в сверке |
разбор позиций и пояс — сначала сюжет 14 |
| DDS карта | только с оговорками | DDL, даг | нет независимой | почти сразу; форма — «собери рядом» |
| DM витрины | revenue/pvo/traffic |
доноры, DDL, даг | dq_check целиком |
ответ лежит в самой проверке |
Вывод
Гипотеза «вычитание + DQ» подходит не для всех нижних слоев. Без оговорок она работает только для сессий DDS: там есть внешний эталон, настоящие места для ошибок и проверка, которая не содержит готового решения.
Для заказов DDS и разбора заказов ODS нужна последовательность «сначала построй сторожа, потом ломай». Недостающие инварианты — сюжеты 14 и 15, а также баланс среза — сами входят в учебный материал. Их нужно построить до вычитания.
STG и прием событий нельзя отдавать в форме «удали и перепиши»: код там служит несущей частью стенда. Для этих участков подходят расследования и форма «собери рядом».
У всех счетных проверок остается общая дыра — раскладка по шардам. Поэтому пересчет ключа по hostName() должен входить в ТЗ каждого вычитания отдельной строкой.