Files
ddadmin b839a671a1 docs(labs): добавлены история связей и инкрементальный пересчет
Зачем: дать практику учета новых связей пользователей при обновлении
прежних дней витрины.

Что: добавлены две связанные лабораторные с наблюдениями через Airflow,
собственными таблицами student и повтором частично записанной загрузки.
Обновлены программа, индекс и ссылки из справочников DDS и DM.

Проверка: DDL и SQL под student на ClickHouse 26.3.17.56, четыре модельных
дня через Airflow, полные сверки и повтор незавершенной загрузки;
config-test, lint, локальные ссылки и независимое ревью.
2026-09-22 05:57:11 -04:00

24 KiB
Raw Permalink Blame History

Слой DM: витрины

Витрины готовят данные DDS для отчетов и дашбордов. Здесь описаны форма хранения, правила обновления и проверки. Состав колонок задан в спецификации и учебных заданиях.

Таблица или представление

Зерно — то, чему соответствует одна строка. Например, в заказах это заказ, а в дневной выручке — день и категория товара. На стенде результат с новым зерном хранится в таблице: отчет читает готовые итоги без повторного расчета. Таблицу наполняет dm_transform.

У такой витрины есть локальная _rep, распределенная _dist и публичное представление _v с явным списком колонок. Для обновления по дням нужна промежуточная таблица _stage_rep, в которую пишут через _stage_dist. В нее записывают рассчитанные строки, пока основная таблица хранит прежние. Затем дневные разделы основной таблицы заменяют готовыми разделами промежуточной. Такие разделы называются партициями. Подробности — в справочнике хранилища.

Если запрос только переименовывает или дополняет поля и сохраняет одну строку на событие, достаточно обычного представления. Оно выполняет запрос при чтении, не хранит вторую копию событий и не требует пересборки.

На небольших данных стенда и агрегаты могли бы вычисляться при чтении (исследование). Готовые таблицы нужны в программе для практики загрузки, обновления и включения витрин в конвейер.

Что готово в стенде, а что остаётся заданием

Готовы три витрины дашборда и сводка качества:

  • revenue_daily — одна строка на день заказа и категорию товара; образец дневной пересборки для учебных заданий.
  • purchase_vs_orders — одна строка на заказ: сопоставление покупки из кликстрима с заказом магазина и класс расхождения.
  • daily_traffic — одна строка на день: число посетителей и известных пользователей с учетом карты идентичностей.
  • dq_summary — одна строка на день данных и проверку качества.

В заданиях ступени 1 вы строите events_enriched_v, top_pages_daily, session_overview, dq_errors_daily и utm_effectiveness. Для каждой физической витрины нужно добавить сборку в dm_transform. Представление events_enriched_v отдельной задачи не требует.

Стендовый дашборд читает готовые витрины и работает до выполнения заданий. Дашборд по своим витринам можно собрать в факультативе после ступени 2. Устройство стендового дашборда — в справочнике Superset.

Ритм пересборки

Дни для пересчета выбирают по тому, как меняется источник:

  • События и сессии прожитого дня на стенде больше не меняются. Витрины по ним рассчитывают показатели от первого пропущенного дня до последнего прожитого и заменяют дневные партиции.
  • Заказы могут меняться в течение семи модельных дней. Последний слепок ODS задает диапазон дат для пересборки; пропущенные дни могут расширить его.
  • Новая связь в карте идентичностей позволяет определить пользователя по его прежним визитам: один client_id мог встречаться и до покупки, которая связала его с user_id. Поэтому daily_traffic, как и карта, пересчитывается целиком.

daily_traffic каждый раз добавляет всю историю новой версией в ReplacingMergeTree(_load_ts). Дневных партиций у нее нет, а текущие строки выбирает представление через FINAL.

У purchase_vs_orders два источника. Начало диапазона — самая ранняя из трех дат: первая дата оформления заказа в последнем слепке, первый пропущенный день и первый день с awaiting_order — покупкой, для которой заказ пока не поступил. День ожидания проверяется отдельно: строки за него уже есть в витрине, поэтому он не считается пропущенным. Конец диапазона — более поздняя из последней даты заказов в слепке и последнего прожитого дня с покупкой. Один запуск пересобирает весь отрезок, даже если с предыдущего запуска прошло больше семи модельных дней.

Для учебной сводки брака действует отдельное правило: дни выбираются по времени загрузки, а прежние итоги сохраняются после удаления источника по TTL. Оно разобрано в задании dq_errors_daily.

Витрины стенда

Представления _v явно перечисляют поля для отчетов, а также _load_id и _load_ts — идентификатор запуска и время записи. Новая колонка физической таблицы появится в представлении только после изменения его SQL.

dm.revenue_daily_v

Одна строка содержит итоги оплаченных заказов за день по категории товара. Запрос читает dds.order_v и разворачивает массивы артикулов, количеств и цен в строки позиций заказа. Артикул, количество и цена с одним индексом становятся одной строкой. Категория артикула берется из dic.products.

orders считает разные заказы: несколько позиций одной категории в заказе не увеличивают этот счетчик. units — сумма количества товаров, revenue — сумма произведений цены на количество. Скидка и доставка в нее не входят. aov равен revenue / orders для того же дня и категории с округлением до двух знаков после запятой.

Первый запуск рассчитывает выручку за всю доступную историю. Следующие берут диапазон дат оформления заказов из последнего слепка ODS. Если в DDS есть оплаченные заказы за день, отсутствующий в витрине, левая граница расширяется до него. Агрегат читает DDS; ODS отвечает только на вопрос об объеме.

В группе revenue_daily дага dm_transform три шага:

  1. revenue_daily_scope.sql возвращает first_day и last_day. Без заказов в ODS или DDS запрос не возвращает строк, и задача завершается ошибкой.
  2. revenue_daily_rebuild.sql очищает dm.revenue_daily_stage_rep, рассчитывает показатели за выбранные дни и записывает строки через dm.revenue_daily_stage_dist. Вставка ждет доставки строк на шарды.
  3. revenue_daily_replace.sql заменяет в dm.revenue_daily_rep каждую дневную партицию report_date готовой партицией dm.revenue_daily_stage_rep.

Повтор замены не добавляет копий строк. Ограничения при частичном отказе описаны в конвейере.

Основная и промежуточная таблицы распределяют строки по cityHash64(product_category). На каждый день приходится не больше шести категорий стартового мира. Для такой маленькой таблицы достаточно простого ключа по категории.

dm.purchase_vs_orders_v

Одна строка сопоставляет события покупки с заказом магазина. События из dds.event_v группируются по номеру заказа purchase_id[1]: по формату источника у покупки один номер и одна заявленная сумма. Отдельный счет событий позволяет обнаружить повтор покупки и присвоить класс duplicate_event. Заказы берутся из dds.order_v.

Полное соединение FULL OUTER GLOBAL JOIN сохраняет и покупки без заказа, и заказы без покупки. Обе стороны записаны подзапросами: при прямом чтении левой Distributed-таблицы каждый непарный заказ повторился бы по числу шардов (исследование).

Если для строки нет пары, поля отсутствующей стороны получают NULL благодаря join_use_nulls = 1. При join_use_nulls = 0 они заполнились бы нулями или пустыми строками, которые сложнее отличить от данных источника. Клиентская declared_revenue и серверная items_total сравниваются как Decimal(18, 2). Классы проверяются по порядку: awaiting_order для покупки без заказа, затем cancelled, lost_event, duplicate_event, amount_delta, match. Поэтому отмененный заказ без события остается отмененным, а не потерянным.

Партиции витрины разделены по order_day. Основная и промежуточная таблицы распределяют строки по cityHash64(order_id). Запрос рассчитывает весь выбранный диапазон и одной вставкой записывает строки в промежуточную таблицу. Затем ее дневные разделы заменяют разделы основной таблицы. Первичная сборка охватывает всю историю обоих источников; следующие прогоны выбирают дни по правилам обоих источников, описанным выше.

dm.daily_traffic_v

В строке один день. visitors считает разные client_id из dds.session_v; known_users считает разные user_id после левого соединения с полной текущей dds.identity_map_v. Анонимная сессия остается в витрине. Несколько кук, связанных с одним пользователем магазина, учитываются в visitors по отдельности, а в known_users дают одного пользователя. Поэтому known_users <= visitors.

Сессии и карта распределены по client_id. Однако на ClickHouse 26.3 чтение через публичные _v с distributed_product_mode = 'local' не дало локального соединения. Поэтому в запросе явно указан GLOBAL LEFT JOIN. Он сохраняет сессии без известного пользователя; в known_users учитываются только ненулевые user_id. Обновление всей истории новой версией описано в разделе «Ритм пересборки».

dm.dq_summary

Одна строка dm.dq_summary_v — день данных и одна проверка. Строка отвечает, каково качество дня сейчас; историю запусков хранит Airflow. Партиция совпадает с днём данных и заменяется через REPLACE PARTITION.

Деловые колонки сводки:

  • data_date — проверяемый день;
  • check_name — имя проверяемого правила;
  • reference_rows — число деловых ключей на эталонной стороне;
  • actual_rows — число деловых ключей в проверяемом объекте;
  • failed_rows — число отсутствующих или различающихся ключей;
  • status — pass при нуле нарушений, иначе fail.

В _load_id и _load_ts записаны идентификатор запуска и время расчета сводки. Даже одно расхождение дает fail.

Звено DQ конвейера

Даг dq_check стоит в etl_pipeline после dm_transform и проверяет пять правил:

  • sessions_vs_reference: ключ — client_id внутри дня, эталон — uniqExact(VisitID) из ods.event_v, объект — число строк dds.session_v;
  • classes_vs_inventory: ключ — mismatch_class, эталон — счётчик из orders в паспорте мира, объект — число заказов класса в dm.purchase_vs_orders_v. Проверяются match, cancelled, lost_event, duplicate_event и amount_delta только для покрытых паспортом закрытых дней;
  • revenue_daily_vs_dds: ключ — product_category внутри дня. Сравниваются orders, units, revenue и aov с прямым агрегатом dds.order_v;
  • purchase_vs_orders_vs_dds: ключ — order_id внутри дня. Все деловые поля сверки сравниваются с независимой сборкой из dds.event_v и dds.order_v;
  • daily_traffic_vs_dds: ключ — день. visitors и known_users сравниваются с прямыми агрегатами dds.session_v и dds.identity_map_v.

Каждая проверка берет дни и ключи из обоих сравниваемых наборов данных. Поэтому потерянная строка остается видна на другой стороне сравнения. Расхождения считаются по ключам: завышение одного показателя не скроет занижение другого в общей сумме за день.

classes_vs_inventory сравнивает результат с паспортом мира. Это техническая проверка доставки и обработки эталонных данных стенда. Она не проверяет сам генератор; у настоящего магазина такого эталона нет.

Четыре SQL-сверки считают дневные итоги в ClickHouse и возвращают Airflow только сводку и несколько примеров расхождений. Техническая сверка с паспортом читает небольшой файл JSON в Python. Результаты всех пяти проверок записываются в общую промежуточную таблицу. Когда расчеты завершились, ее дневные разделы заменяют разделы dm.dq_summary_rep. Затем отдельные задачи читают сохраненную сводку и завершаются ошибкой при failed_rows > 0. Так расхождение сохраняется в таблице до отказа задачи. Если сам расчет не выполнен, прежняя сводка остается на месте. Задача с расхождениями выводит в журнал не больше двадцати ключей со значениями обеих сторон. Для полного списка итоговую агрегацию SQL-сверки заменяют на SELECT * FROM comparison WHERE failed ORDER BY data_date, business_key.

Если день исчез из обоих сравниваемых наборов, новый расчет не создаст для него строк. Даг отдельно удаляет прежний результат за этот день из сводки.

Даг самопроверки

homework_check запускается вручную и содержит задачу для каждого из пяти заданий по витринам. Если витрины нет или значения расходятся с источником, задача завершается ошибкой. Журнал показывает причину и примеры несовпадений. Успех означает, что проверяемые значения совпали; остальные требования задания вы проверяете сами.

Ожидаемые значения рассчитываются по исходным данным текущего стенда. Для четырех заданий это события или сессии DDS, для сводки брака — таблицы ошибок ODS. Сравнение по деловому ключу учитывает отсутствующие строки с обеих сторон. Результаты не пишутся в dq_summary: самопроверка учебных витрин отделена от качества готового конвейера.

Готовые SQL-проверки открывайте после собственного решения: они содержат расчет существенной части задания.

Отклонённые варианты

  • Весь слой представлениями. Дёшево и всегда свежо, но главный производственный паттерн слоя — пересборка агрегатов — остался бы невидим, а dm_transform — без работы.
  • Материализовать всё. Копия событий ради переименования колонок — лишняя; такой пример приучал бы без нужды дублировать исходные данные.
  • Гибрид по цене запроса. Выбор формы по стоимости, которой на объёмах стенда нет: сторож без настоящей опасности.
  • Проверки домашек в конвейерном звене DQ. Конвейер стоял бы красным до выполнения лаб, а проверка, которая всегда красная, приучает себя игнорировать.
  • «Жёлтый» статус недостроенной витрины. У задачи Airflow нет такого состояния; изображать его логами — семантика, которую пришлось бы расшифровывать. Красное «ещё не построено» честнее.
  • Самопроверка пишет в dq_summary. Результаты учебных попыток смешались бы с историей качества конвейера.
  • Окно заказов для daily_traffic — прошлое замерзает. Дни за окном хранили бы число известных на момент закрытия — витрина тихо расходится с картой и протаскивает обратно «знание на дату», отвергнутое развилкой карты (#133).
  • Инкрементальный пересчёт затронутых дней по first_matched_at. Производственный ход, но на объёмах стенда его экономия неизмерима, а след новых связок требует вернуть колонку, выкинутую в #133. Стал лабой повышенной сложности — «Как обновлять витрину без полной пересборки». В ней новые связи находятся сравнением наблюдений: время покупки не заменяет момент обнаружения связи.
  • Superset поверх DDS без слоя DM. Терялся бы урок «витрина — контракт под потребителя», против раздела 7 спеки.

Связанные решения