Зачем: выражения «собираются в донор» и «публикуются дни» требовали знать устройство конвейера до чтения объяснения. Что: описаны назначение промежуточных таблиц и последовательность расчета, записи и замены разделов. Перечитаны остальные объяснения: выбор дней, разворачивание позиций заказа, проверка сессий и повтор запуска. Проверка: make lint, git diff --check, локальные ссылки и якоря. SQL без комментариев и AST обоих дагов без строк документации не изменились. Refs: #249
24 KiB
Слой 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 три шага:
revenue_daily_scope.sqlвозвращаетfirst_dayиlast_day. Без заказов в ODS или DDS запрос не возвращает строк, и задача завершается ошибкой.revenue_daily_rebuild.sqlочищаетdm.revenue_daily_stage_rep, рассчитывает показатели за выбранные дни и записывает строки черезdm.revenue_daily_stage_dist. Вставка ждет доставки строк на шарды.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. Стал лабой повышенной сложности — issue #149. - Superset поверх DDS без слоя DM. Терялся бы урок «витрина — контракт под потребителя», против раздела 7 спеки.
Связанные решения
- Мастер-спека, разделы 4 и 7, — состав колонок витрин и классы сверки.
- Дневной конвейер — запуск
dm_transform, объём прогона, замена партиций на кластере. - Дока хранилища — имена объектов, конвенция чтения через
_v, партиция-донор. - Карта модели DDS — источники витрин.
- Резолюция — тикет #135 карты #127.