docs(warehouse): объяснены расчет строк и замена дневных разделов
Зачем: выражения «собираются в донор» и «публикуются дни» требовали знать устройство конвейера до чтения объяснения. Что: описаны назначение промежуточных таблиц и последовательность расчета, записи и замены разделов. Перечитаны остальные объяснения: выбор дней, разворачивание позиций заказа, проверка сессий и повтор запуска. Проверка: make lint, git diff --check, локальные ссылки и якоря. SQL без комментариев и AST обоих дагов без строк документации не изменились. Refs: #249
This commit is contained in:
1 parent
5f317ba8d2
commit
086007fbd4
12 files changed
+216
-166
No files matched your search
+76
-61
@@ -13,9 +13,11 @@
|
||||
повторного расчета. Таблицу наполняет `dm_transform`.
|
||||
|
||||
У такой витрины есть локальная `_rep`, распределенная `_dist` и публичное
|
||||
представление `_v` с явным списком колонок. Для замены дневных партиций
|
||||
служат еще две таблицы: `_stage_rep` и `_stage_dist`. Это донор — место
|
||||
сборки результата перед публикацией. Подробности — в
|
||||
представление `_v` с явным списком колонок. Для обновления по дням
|
||||
нужна промежуточная таблица `_stage_rep`, в которую пишут через `_stage_dist`.
|
||||
В нее записывают рассчитанные строки, пока основная таблица хранит прежние.
|
||||
Затем дневные разделы основной таблицы заменяют готовыми разделами
|
||||
промежуточной. Такие разделы называются партициями. Подробности — в
|
||||
[справочнике хранилища](storage.md#имена-объектов).
|
||||
|
||||
Если запрос только переименовывает или дополняет поля и сохраняет одну
|
||||
@@ -54,23 +56,25 @@
|
||||
Дни для пересчета выбирают по тому, как меняется источник:
|
||||
|
||||
- События и сессии прожитого дня на стенде больше не меняются. Витрины
|
||||
по ним собирают диапазон от первого пропущенного дня до последнего
|
||||
по ним рассчитывают показатели от первого пропущенного дня до последнего
|
||||
прожитого и заменяют дневные партиции.
|
||||
- Заказы могут меняться в течение семи модельных дней. Последний слепок ODS
|
||||
задает диапазон дат для пересборки; пропущенные дни могут расширить его.
|
||||
- Новые связи карты идентичностей меняют трактовку прошлых визитов:
|
||||
сегодняшняя покупка позволяет узнать пользователя и в ранних днях.
|
||||
- Новая связь в карте идентичностей позволяет определить пользователя
|
||||
по его прежним визитам: один `client_id` мог встречаться и до покупки,
|
||||
которая связала его с `user_id`.
|
||||
Поэтому `daily_traffic`, как и карта, пересчитывается целиком.
|
||||
|
||||
`daily_traffic` каждый раз добавляет всю историю новой версией
|
||||
в `ReplacingMergeTree(_load_ts)`. Дневных партиций у нее нет, а текущие
|
||||
строки выбирает представление через `FINAL`.
|
||||
|
||||
У `purchase_vs_orders` два источника. Левая граница — самая ранняя
|
||||
из трех дат: начало окна последнего слепка, первый пропущенный день
|
||||
и первый день с `awaiting_order`. Последний нужен потому, что строка
|
||||
ожидания уже есть в витрине и поиском пропусков не обнаружится.
|
||||
Правая граница — более поздняя из конца окна заказов и последнего
|
||||
У `purchase_vs_orders` два источника. Начало диапазона — самая ранняя
|
||||
из трех дат: первая дата оформления заказа в последнем слепке, первый
|
||||
пропущенный день и первый день с `awaiting_order` — покупкой, для которой
|
||||
заказ пока не поступил. День ожидания проверяется отдельно: строки за него
|
||||
уже есть в витрине, поэтому он не считается пропущенным.
|
||||
Конец диапазона — более поздняя из последней даты заказов в слепке и последнего
|
||||
прожитого дня с покупкой. Один запуск пересобирает весь отрезок,
|
||||
даже если с предыдущего запуска прошло больше семи модельных дней.
|
||||
|
||||
@@ -81,22 +85,25 @@
|
||||
|
||||
## Витрины стенда
|
||||
|
||||
Публичные представления перечисляют деловые колонки и координаты запуска
|
||||
`_load_id`, `_load_ts` явно. Новая колонка физической таблицы сама в договор не
|
||||
попадет.
|
||||
Представления `_v` явно перечисляют поля для отчетов, а также `_load_id`
|
||||
и `_load_ts` — идентификатор запуска и время записи. Новая колонка
|
||||
физической таблицы появится в представлении только после изменения его SQL.
|
||||
|
||||
### `dm.revenue_daily_v`
|
||||
|
||||
Одна строка — день заказа и категория товара. Витрина читает только оплаченные
|
||||
заказы из `dds.order_v`, синхронно разворачивает три массива позиций и узнает
|
||||
категорию артикула через `dic.products`. Один заказ с несколькими позициями
|
||||
категории считается в `orders` один раз. `units` складывает количество единиц,
|
||||
`revenue` складывает цену позиции × количество. Скидка и доставка
|
||||
в выручку не входят; итог заказа между категориями не делится. `aov` равен `revenue / orders` в том же срезе и
|
||||
хранит два знака после запятой.
|
||||
Одна строка содержит итоги оплаченных заказов за день по категории товара.
|
||||
Запрос читает `dds.order_v` и разворачивает массивы артикулов, количеств
|
||||
и цен в строки позиций заказа. Артикул, количество и цена с одним индексом
|
||||
становятся одной строкой. Категория артикула берется из `dic.products`.
|
||||
|
||||
Первый запуск строит всю доступную историю. Следующие берут границы дней из
|
||||
последнего слепка ODS: только источник знает, какие даты еще может изменить.
|
||||
`orders` считает разные заказы: несколько позиций одной категории
|
||||
в заказе не увеличивают этот счетчик. `units` — сумма количества товаров,
|
||||
`revenue` — сумма произведений цены на количество. Скидка и доставка
|
||||
в нее не входят. `aov` равен `revenue / orders` для того же дня и категории
|
||||
с округлением до двух знаков после запятой.
|
||||
|
||||
Первый запуск рассчитывает выручку за всю доступную историю. Следующие
|
||||
берут диапазон дат оформления заказов из последнего слепка ODS.
|
||||
Если в DDS есть оплаченные заказы за день, отсутствующий в витрине,
|
||||
левая граница расширяется до него. Агрегат читает DDS; ODS отвечает
|
||||
только на вопрос об объеме.
|
||||
@@ -107,8 +114,9 @@
|
||||
возвращает `first_day` и `last_day`. Без заказов в ODS или DDS
|
||||
запрос не возвращает строк, и задача завершается ошибкой.
|
||||
2. [`revenue_daily_rebuild.sql`](../../sql/dm/revenue_daily_rebuild.sql)
|
||||
очищает донор и одной вставкой собирает весь диапазон в
|
||||
`dm.revenue_daily_stage_dist`. Вставка ждет доставки на шарды.
|
||||
очищает `dm.revenue_daily_stage_rep`, рассчитывает показатели за выбранные
|
||||
дни и записывает строки через `dm.revenue_daily_stage_dist`.
|
||||
Вставка ждет доставки строк на шарды.
|
||||
3. [`revenue_daily_replace.sql`](../../sql/dm/revenue_daily_replace.sql)
|
||||
заменяет в `dm.revenue_daily_rep` каждую дневную партицию `report_date`
|
||||
готовой партицией `dm.revenue_daily_stage_rep`.
|
||||
@@ -116,33 +124,38 @@
|
||||
Повтор замены не добавляет копий строк. Ограничения при частичном отказе
|
||||
описаны в [конвейере](etl.md#идемпотентность).
|
||||
|
||||
Цель и донор шардируются
|
||||
по `cityHash64(product_category)`. Кроме дня, у витрины только категория, а
|
||||
шесть категорий стартового мира близки по объему. Более сложный составной ключ
|
||||
распределил бы эту маленькую таблицу без нового урока.
|
||||
Основная и промежуточная таблицы распределяют строки по
|
||||
`cityHash64(product_category)`. На каждый день приходится не больше шести
|
||||
категорий стартового мира. Для такой маленькой таблицы достаточно простого
|
||||
ключа по категории.
|
||||
|
||||
### `dm.purchase_vs_orders_v`
|
||||
|
||||
Одна строка — один заказ. Покупки из `dds.event_v` сначала сворачиваются по
|
||||
`purchase_id[1]`: контракт источника обещает один номер и одну объявленную
|
||||
сумму. Отдельный счет событий позволяет обнаружить повтор покупки
|
||||
и присвоить класс `duplicate_event`. Заказы приходят из `dds.order_v`. Обе стороны записаны
|
||||
подзапросами, затем соединены `FULL OUTER GLOBAL JOIN`: левая
|
||||
Distributed-таблица без подзапроса вернула бы каждый непарный заказ
|
||||
по разу с каждого шарда
|
||||
Одна строка сопоставляет события покупки с заказом магазина. События
|
||||
из `dds.event_v` группируются по номеру заказа `purchase_id[1]`:
|
||||
по формату источника у покупки один номер и одна заявленная сумма.
|
||||
Отдельный счет событий позволяет обнаружить повтор покупки
|
||||
и присвоить класс `duplicate_event`. Заказы берутся из `dds.order_v`.
|
||||
|
||||
Полное соединение `FULL OUTER GLOBAL JOIN` сохраняет и покупки без заказа,
|
||||
и заказы без покупки. Обе стороны записаны подзапросами: при прямом чтении
|
||||
левой Distributed-таблицы каждый непарный заказ повторился бы по числу шардов
|
||||
([исследование](../research/2026-08-22-distributed-join.md)).
|
||||
|
||||
Непарная сторона остается `NULL` благодаря `join_use_nulls = 1`. При
|
||||
`join_use_nulls = 0` ClickHouse заполнил бы ее нулем или пустой строкой и скрыл
|
||||
отсутствие источника. Клиентская `declared_revenue` и серверная `items_total`
|
||||
сравниваются как `Decimal(18, 2)`. Классы проверяются по порядку:
|
||||
Если для строки нет пары, поля отсутствующей стороны получают `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)`. Диапазон собирается в доноре одной вставкой и
|
||||
заменяется по дням. Первичная сборка охватывает всю историю обоих источников;
|
||||
Партиции витрины разделены по `order_day`. Основная и промежуточная таблицы
|
||||
распределяют строки по `cityHash64(order_id)`. Запрос рассчитывает весь
|
||||
выбранный диапазон и одной вставкой записывает строки в промежуточную
|
||||
таблицу. Затем ее дневные разделы заменяют разделы основной таблицы.
|
||||
Первичная сборка охватывает всю историю обоих источников;
|
||||
следующие прогоны выбирают дни по правилам обоих источников, описанным выше.
|
||||
|
||||
### `dm.daily_traffic_v`
|
||||
@@ -150,8 +163,9 @@ Distributed-таблица без подзапроса вернула бы ка
|
||||
В строке один день. `visitors` считает разные `client_id` из
|
||||
`dds.session_v`; `known_users` считает разные `user_id` после левого соединения
|
||||
с полной текущей `dds.identity_map_v`. Анонимная сессия остается в витрине.
|
||||
Несколько кук одного человека увеличивают `visitors`, но дают одного
|
||||
известного пользователя, поэтому `known_users <= visitors`.
|
||||
Несколько кук, связанных с одним пользователем магазина, учитываются
|
||||
в `visitors` по отдельности, а в `known_users` дают одного пользователя.
|
||||
Поэтому `known_users <= visitors`.
|
||||
|
||||
Сессии и карта распределены по `client_id`. Однако на ClickHouse 26.3
|
||||
чтение через публичные `_v` с `distributed_product_mode = 'local'`
|
||||
@@ -176,8 +190,8 @@ Distributed-таблица без подзапроса вернула бы ка
|
||||
- `failed_rows` — число отсутствующих или различающихся ключей;
|
||||
- `status` — `pass` при нуле нарушений, иначе `fail`.
|
||||
|
||||
Служебные `_load_id` и `_load_ts` называют собравший запуск. Представление
|
||||
перечисляет колонки явно. Порогов и промежуточного состояния нет.
|
||||
В `_load_id` и `_load_ts` записаны идентификатор запуска и время расчета
|
||||
сводки. Даже одно расхождение дает `fail`.
|
||||
|
||||
## Звено DQ конвейера
|
||||
|
||||
@@ -201,29 +215,30 @@ Distributed-таблица без подзапроса вернула бы ка
|
||||
сравниваются с прямыми агрегатами `dds.session_v` и
|
||||
`dds.identity_map_v`.
|
||||
|
||||
Область каждой проверки выводится из объединения сторон. Полностью потерянный
|
||||
день или ключ поэтому не исчезает из результата. Разные ошибки тоже не могут
|
||||
скомпенсировать друг друга общим итогом: `failed_rows` считается после
|
||||
сопоставления деловых ключей.
|
||||
Каждая проверка берет дни и ключи из обоих сравниваемых наборов данных.
|
||||
Поэтому потерянная строка остается видна на другой стороне сравнения.
|
||||
Расхождения считаются по ключам: завышение одного показателя не скроет
|
||||
занижение другого в общей сумме за день.
|
||||
|
||||
`classes_vs_inventory` сравнивает результат с паспортом мира. Это техническая
|
||||
проверка доставки и обработки эталонных данных стенда. Она не проверяет
|
||||
сам генератор; у настоящего магазина такого эталона нет.
|
||||
|
||||
Четыре SQL-сверки считают дневные итоги в ClickHouse и возвращают Airflow только
|
||||
сводку и малый срез диагностики. Техническая сверка с паспортом читает небольшой
|
||||
файл JSON в Python. Все пять сборок пишут в общий донор. После их успеха даг
|
||||
заменяет проверенные дневные партиции. Затем именованные задачи читают
|
||||
опубликованную сводку и падают при `failed_rows > 0`. Расхождение успевает
|
||||
сохраниться до красного завершения, а ошибка сборки не затирает прежний
|
||||
результат. Красная задача печатает не больше двадцати нарушенных ключей за
|
||||
проверку со значениями сторон. Для полного списка итоговую агрегацию SQL-сверки
|
||||
сводку и несколько примеров расхождений. Техническая сверка с паспортом читает небольшой
|
||||
файл JSON в Python. Результаты всех пяти проверок записываются в общую
|
||||
промежуточную таблицу. Когда расчеты завершились, ее дневные разделы
|
||||
заменяют разделы `dm.dq_summary_rep`. Затем отдельные задачи читают
|
||||
сохраненную сводку и завершаются ошибкой при `failed_rows > 0`.
|
||||
Так расхождение сохраняется в таблице до отказа задачи. Если сам расчет
|
||||
не выполнен, прежняя сводка остается на месте. Задача с расхождениями
|
||||
выводит в журнал не больше двадцати ключей со значениями обеих сторон. Для полного списка итоговую агрегацию SQL-сверки
|
||||
заменяют на `SELECT * FROM comparison WHERE failed ORDER BY data_date,
|
||||
business_key`.
|
||||
|
||||
Если после починки день исчез с обеих сторон, его больше нет и в новом доноре.
|
||||
Такой день удаляется из сводки отдельно: старая красная строка не должна
|
||||
превращаться в историю попыток.
|
||||
Если день исчез из обоих сравниваемых наборов, новый расчет не создаст
|
||||
для него строк. Даг отдельно удаляет прежний результат за этот день
|
||||
из сводки.
|
||||
|
||||
## Даг самопроверки
|
||||
|
||||
|
||||
Reference in new issue
Block a user