Files
clickstream-data-platform/docs/research/2026-08-31-labs-inventory.md
T
ddadminandClaude Fable 5 502d705ca1 docs(research): инвентаризация сюжетов лаб по слоям стенда
- Зачем:
  - спеке программы лаб (#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>
2026-08-31 11:38:54 -04:00

44 KiB
Raw Blame History

Инвентаризация сюжетов лаб по слоям стенда

Это материал для спеки программы лаб (#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() должен входить в ТЗ каждого вычитания отдельной строкой.