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
@@ -17,10 +17,11 @@
|
||||
`EventDate` источника, дата по часовому поясу счетчика. У всех событий
|
||||
одной сессии она одинакова. Пересчитывать ее из времени UTC не нужно.
|
||||
|
||||
Оба окна нарезки используют порядок `(event_time, event_id)`: при равном
|
||||
времени события идут по идентификатору. Одинаковый порядок нужен, чтобы
|
||||
признак начала визита и накопленный номер визита относились к одним
|
||||
и тем же событиям.
|
||||
SQL рассчитывает границы визитов двумя оконными функциями: первая отмечает
|
||||
начало нового визита, вторая по этим отметкам присваивает событиям номер
|
||||
визита. Обе используют порядок `(event_time, event_id)`: при равном времени
|
||||
события идут по идентификатору. Иначе два расчета могли бы по-разному
|
||||
упорядочить события и отнести отметку начала к разным строкам.
|
||||
|
||||
## Идентификатор
|
||||
|
||||
@@ -73,24 +74,28 @@
|
||||
Три SQL-файла выполняются последовательно в группе `session` дага
|
||||
`dds_transform`:
|
||||
|
||||
1. `session_scope.sql` находит первый день событий, которого еще нет
|
||||
в таблице сессий. Последний день для сборки — первый день событий ODS
|
||||
плюс `world_position` минус один. Так в расчет попадают только полностью
|
||||
1. `session_scope.sql` находит первую дату событий, за которую еще нет
|
||||
сессий. Последний день для расчета — первый день событий ODS
|
||||
плюс `world_position` минус один; `world_position` — номер следующего
|
||||
дня для генерации. Так в расчет попадают только полностью
|
||||
прожитые дни. При пустом ODS запрос не возвращает строк и задача падает;
|
||||
если пропущенных прожитых дней нет, `first_day` равен `NULL` и группа
|
||||
пропускается.
|
||||
2. `session_rebuild.sql` очищает `dds.session_stage_rep` и собирает весь
|
||||
выбранный диапазон в `dds.session_stage_dist`. Это донор — служебная
|
||||
таблица для подготовки результата до публикации. Вставка ждет доставки
|
||||
2. `session_rebuild.sql` очищает промежуточную таблицу
|
||||
`dds.session_stage_rep`. Затем рассчитывает сессии выбранных дней
|
||||
и записывает строки через `dds.session_stage_dist`. Пока идет расчет,
|
||||
в основной таблице остаются прежние данные. Вставка ждет доставки
|
||||
строк на шарды благодаря `distributed_foreground_insert = 1`.
|
||||
3. `session_replace.sql` заменяет каждый день в `dds.session_rep` партицией
|
||||
донора. Для каждого дня Airflow создает отдельную задачу `replace`.
|
||||
3. `session_replace.sql` заменяет в `dds.session_rep` раздел за каждый
|
||||
выбранный день готовым разделом `dds.session_stage_rep`. Для каждого
|
||||
дня Airflow создает отдельную задачу `replace`.
|
||||
|
||||
В обычном ходе сессии прожитого дня больше не меняются. Если пропущен ранний
|
||||
Когда все события завершенного дня приняты в ODS, их набор на стенде
|
||||
больше не меняется. Поэтому готовые сессии обычно не пересчитываются. Если пропущен ранний
|
||||
день, диапазон включает и все последующие прожитые дни, даже уже собранные.
|
||||
Поиск по наличию даты не обнаружит неполный день, у которого часть строк
|
||||
сохранилась. Условия повторного запуска и границы атомарности замены
|
||||
описаны в [справочнике конвейера](../etl.md#идемпотентность).
|
||||
Если за день сохранилась хотя бы одна сессия, поиск пропущенных дней
|
||||
не обнаружит потерю остальных. Условия повторного запуска и замены
|
||||
разделов описаны в [справочнике конвейера](../etl.md#идемпотентность).
|
||||
|
||||
Сессий еще идущего дня в модели нет; его события доступны через
|
||||
`dds.event_v`. Перед выбором дней даг ждет завершения доставки событий
|
||||
@@ -103,10 +108,10 @@
|
||||
из `ods.event_v`. Эталон читается из ODS, потому что `dds.event_v`
|
||||
скрывает `VisitID`.
|
||||
|
||||
Нулевое число расхождений подтверждает совпадение счета, но не всех
|
||||
границ и атрибутов сессий. Две ошибки внутри одного посетителя и дня
|
||||
могут сохранить число визитов. Поэтому в заданиях вы дополнительно
|
||||
сопоставляете границы сессий по исходным событиям и проверяете страницы,
|
||||
Нулевое число расхождений подтверждает только число сессий каждого
|
||||
посетителя за день. Например, ошибочное объединение двух визитов
|
||||
и разделение другого на два сохранят их общее число. Поэтому в заданиях
|
||||
вы дополнительно сопоставляете границы сессий по исходным событиям и проверяете страницы,
|
||||
UTM, устройство и город.
|
||||
|
||||
## Читатели
|
||||
|
||||
+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`.
|
||||
|
||||
Если после починки день исчез с обеих сторон, его больше нет и в новом доноре.
|
||||
Такой день удаляется из сводки отдельно: старая красная строка не должна
|
||||
превращаться в историю попыток.
|
||||
Если день исчез из обоих сравниваемых наборов, новый расчет не создаст
|
||||
для него строк. Даг отдельно удаляет прежний результат за этот день
|
||||
из сводки.
|
||||
|
||||
## Даг самопроверки
|
||||
|
||||
|
||||
+39
-22
@@ -1,21 +1,23 @@
|
||||
# Дневной конвейер трансформаций
|
||||
|
||||
Даг `etl_pipeline` собирает модель DDS, обновляет витрины DM и проверяет
|
||||
результат. Здесь описаны запуск, выбор дней и публикация данных.
|
||||
результат. Здесь описаны запуск, выбор дней и запись результата в таблицы.
|
||||
Состав моделей — в [справочниках DDS](dds/README.md) и [DM](dm.md).
|
||||
|
||||
## Форма
|
||||
|
||||
`etl_pipeline` последовательно запускает `dds_transform`, `dm_transform`
|
||||
и `dq_check`. Каждый следующий даг начинается после успеха предыдущего.
|
||||
Для этого `TriggerDagRunOperator` ждет завершения созданного им запуска;
|
||||
его ошибка завершает ошибкой и соответствующую задачу конвейера.
|
||||
Для этого `TriggerDagRunOperator` ждет завершения запущенного им дага.
|
||||
Если тот завершился ошибкой, задача ожидания в конвейере тоже завершится
|
||||
ошибкой.
|
||||
Поведение проверено через Context7 по документации Airflow 3.x (3.1.6).
|
||||
|
||||
У этих дагов нет расписания. Их вызывает конвейер; для отдельного разбора
|
||||
можно запустить даг слоя вручную. У каждого разрешен один активный запуск:
|
||||
два запуска одного дага не должны одновременно заполнять общий донор.
|
||||
Внутри дага модели собраны в отдельные задачи и группы задач.
|
||||
два запуска одного дага используют одни промежуточные таблицы и могли бы
|
||||
стереть результаты друг друга. Расчет каждой модели выполняется отдельной
|
||||
задачей или группой задач.
|
||||
|
||||
## Три пути запуска
|
||||
|
||||
@@ -40,9 +42,11 @@ world_next_day ─────> источники без ETL
|
||||
|
||||
Объем прогона — дни, которые обрабатывает один запуск. Даты передавать
|
||||
не нужно: даги определяют их по состоянию таблиц и `world_position`.
|
||||
Например, после генерации семи дней без ETL один запуск соберет все семь.
|
||||
Например, после генерации семи дней без ETL один запуск рассчитает сессии
|
||||
за все семь дней.
|
||||
|
||||
Сессии собираются от первого отсутствующего дня до последнего прожитого.
|
||||
Для расчета сессий выбираются дни от первого, за который в DDS еще нет
|
||||
сессий, до последнего прожитого.
|
||||
`world_position` хранит номер следующего дня для генерации, поэтому
|
||||
последний прожитый день равен `min(EventDate) + world_position - 1`.
|
||||
Начало календаря берется из ODS; события ранних дней там не удаляются по TTL.
|
||||
@@ -52,9 +56,10 @@ world_next_day ─────> источники без ETL
|
||||
|
||||
Для заказов диапазон задает последний принятый слепок ODS. Среди строк
|
||||
с максимальной `snapshot_date` выбираются самая ранняя и самая поздняя
|
||||
даты оформления заказа. Эти дни еще могут измениться. Если в DDS
|
||||
отсутствует более ранний день, который есть в ODS, левая граница
|
||||
расширяется до него. Все дни отрезка пересобираются.
|
||||
даты оформления заказа. Состояние заказов за эти дни еще может измениться.
|
||||
Если в ODS есть заказы за более ранний день, а в DDS за него нет строк,
|
||||
начало диапазона сдвигается до этого дня. Запрос заново рассчитывает
|
||||
заказы за все дни выбранного отрезка.
|
||||
|
||||
Позиция мира для выбора дней заказов не подходит: с событиями дня D
|
||||
приходит слепок за D−1. Объем рассчитывается по принятым строкам ODS;
|
||||
@@ -62,8 +67,8 @@ world_next_day ─────> источники без ETL
|
||||
|
||||
Карта идентичностей пересчитывается целиком. Витрины выбирают дни
|
||||
по своим источникам — см. [ритм пересборки DM](dm.md#ритм-пересборки).
|
||||
Поиск отсутствующих дней проверяет только наличие даты: частично
|
||||
заполненный день требует отдельной проверки.
|
||||
Если за день сохранилась хотя бы одна строка, запрос не считает его
|
||||
пропущенным. Полноту данных проверяют отдельно.
|
||||
|
||||
## Готовность источника
|
||||
|
||||
@@ -96,17 +101,29 @@ world_next_day ─────> источники без ETL
|
||||
не добавляет копии деловых строк. Служебные `_load_id` и `_load_ts`
|
||||
при новой сборке меняются.
|
||||
|
||||
Для дневных таблиц порядок такой: `scope` выбирает даты, `rebuild` очищает
|
||||
донор и собирает в него весь диапазон, затем отдельные задачи `replace`
|
||||
публикуют дни через `REPLACE PARTITION`. Донор — служебная таблица с той же
|
||||
структурой и раскладкой по шардам, что у цели. Запись в `_stage_dist`
|
||||
с `distributed_foreground_insert = 1` дожидается доставки строк перед заменой.
|
||||
Результат дневного расчета сначала записывается в промежуточную таблицу.
|
||||
Она нужна, чтобы читатели продолжали видеть прежние данные, пока новые
|
||||
еще рассчитываются. Например, для сессий это `dds.session_stage_rep`,
|
||||
а основная таблица — `dds.session_rep`.
|
||||
|
||||
Замена выполняется на локальных `_rep` с `ON CLUSTER`. Она атомарна
|
||||
на каждом шарде: читатель видит старую или новую партицию. Единой
|
||||
транзакции для двух шардов и всех дней нет. Если замена прошла только
|
||||
на одном шарде, другой может сохранить прежние данные. Повтор задачи
|
||||
`replace` с тем же готовым донором повторит замену без добавления копий.
|
||||
Задачи выполняются в таком порядке:
|
||||
|
||||
1. `scope` выбирает даты для расчета.
|
||||
2. `rebuild` очищает промежуточную таблицу, рассчитывает строки за выбранные
|
||||
дни и записывает их через `_stage_dist`. Настройка
|
||||
`distributed_foreground_insert = 1` заставляет вставку дождаться
|
||||
доставки строк на шарды.
|
||||
3. `replace` заменяет в основной таблице раздел за каждый выбранный день
|
||||
готовым разделом промежуточной таблицы. Раздел таблицы называется
|
||||
партицией; для замены используется `REPLACE PARTITION`.
|
||||
|
||||
Структура и распределение строк по шардам у двух таблиц должны совпадать.
|
||||
Замена выполняется на локальных `_rep` с `ON CLUSTER`. На каждом шарде
|
||||
читатель видит либо старый, либо новый раздел целиком — это атомарность
|
||||
замены. Общей транзакции для двух шардов и всех дней нет: при сбое один
|
||||
шард может уже содержать новый результат, а другой — прежний. Повторить
|
||||
`replace` можно, пока промежуточная таблица хранит тот же результат;
|
||||
копии строк в основной таблице от этого не появятся.
|
||||
|
||||
Новый запуск всего конвейера заново выбирает дни. Он не обязан повторить
|
||||
все дни неудачного запуска: если дата уже есть в целевой таблице, поиск
|
||||
|
||||
@@ -95,20 +95,24 @@ DDS: представление читает события ODS.
|
||||
Само совпадение ключей еще не гарантирует локального выполнения запроса
|
||||
через представления — пример есть в [описании `daily_traffic`](dm.md#dmdaily_traffic_v).
|
||||
|
||||
Для дневной пересборки DDS и DM используют донор — служебную таблицу,
|
||||
в которой готовят результат перед заменой партиций. Например,
|
||||
`dds.order_stage_rep` объявлена через `AS dds.order_rep`, а запись
|
||||
в нее идет через `dds.order_stage_dist` с тем же `cityHash64(order_id)`.
|
||||
У цели и донора должны совпадать структура, ключи партиции и сортировки,
|
||||
первичный ключ и политика хранения. Эти требования сверены через Context7
|
||||
с [документацией ClickHouse](https://clickhouse.com/docs/sql-reference/statements/alter/partition#replace-partition)
|
||||
для используемой схемы на ClickHouse 26.3. Поэтому донор объявлен через
|
||||
`AS` цели с совпадающими ключами. DDL показан в
|
||||
Для дневной пересборки DDS и DM результат сначала записывают
|
||||
в промежуточную таблицу. Основная таблица в это время остается доступна
|
||||
для чтения с прежними данными. Например, заказы записываются через
|
||||
`dds.order_stage_dist` в `dds.order_stage_rep`, затем ее дневные разделы
|
||||
заменяют разделы основной `dds.order_rep`.
|
||||
|
||||
У промежуточной и основной таблиц должны совпадать структура, ключи
|
||||
партиции и сортировки, первичный ключ и политика хранения. Поэтому
|
||||
`dds.order_stage_rep` объявлена через `AS dds.order_rep` с совпадающими
|
||||
ключами. Требования сверены через Context7 с
|
||||
[документацией ClickHouse](https://clickhouse.com/docs/sql-reference/statements/alter/partition#replace-partition)
|
||||
для схемы на ClickHouse 26.3. DDL показан в
|
||||
[`sql/ddl/45-dds-tables.sql`](../../sql/ddl/45-dds-tables.sql).
|
||||
|
||||
`REPLACE PARTITION` копирует готовую партицию между локальными таблицами
|
||||
на каждом шарде; заново распределять строки по ключу цели эта операция
|
||||
не будет. Поэтому донор должен сразу иметь ту же раскладку, что и цель.
|
||||
Промежуточная таблица должна также распределять строки по тому же ключу,
|
||||
здесь — `cityHash64(order_id)`. `REPLACE PARTITION` копирует раздел между
|
||||
локальными таблицами каждого шарда и не перераспределяет строки.
|
||||
Поэтому уже в промежуточной таблице заказ должен находиться на нужном шарде.
|
||||
Перед заменой вставка с `distributed_foreground_insert = 1` дожидается
|
||||
доставки данных на шарды.
|
||||
|
||||
|
||||
Reference in new issue
Block a user