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