0030. Правила объединения данных РНП и модель витрины по артикулу
Копия ADR из репозитория:
backend/docs/adr/0030-rnp-data-merge-rules.md. Канонический текст — в репозитории; портал обновляется командойnpm run sync:adr.
- Статус: accepted (согласовано 2026-06-29 — приняты рекомендованные ответы на 4 вопроса: финансы по дню заказа, день в МСК, только дневная витрина с агрегацией на лету, состав метрик ниже)
- Дата: 2026-06-29
- Связанные задачи: OP-86 (Spike · 5.6 · РНП), потребитель — OP-82 (витрина), OP-83 (API)
- Связанные ADR: ADR-0008 (хранение временных рядов), ADR-0009 (реестр расписаний);
реализуется поверх OP-91 (партиции) и OP-92 (re-aggregate
*_daily)
Контекст и проблема
ТЗ 5.6 прямо помечает «структура хранения данных» и «правила объединения данных из разных
источников» как уточняется. К моменту спайка все источники РНП уже собираются в raw
fact-таблицы по артикулу (nm_id):
| Источник | Таблица | Ключ идемпотентности | Период |
|---|---|---|---|
| Заказы (OP-54/79) | wb_orders | (wb_account_id, srid) | date (timestamptz) |
| Продажи/возвраты (OP-54/79) | wb_sales | (wb_account_id, sale_id) | date (timestamptz) |
| Реклама (OP-65/80) | ad_campaign_stats | (advert_id, wb_account_id, date, nm_id) | date |
| Финансы: комиссии/логистика/штрафы (OP-55) | wb_report_lines | (wb_account_id, rrd_id) | период отчёта (rr_dt) |
| Воронка/конверсии (OP-78) | rnp_funnel_stats | (wb_account_id, nm_id, date) | date |
Нужно: единый ключ сопоставления, правила разрешения дублей/конфликтов, гранулярность периодов и итоговая схема витрины — чтобы OP-82 строил её детерминированно.
Предлагаемое решение
1. Ключ сопоставления — (wb_account_id, nm_id, day)
nm_id(артикул WB) присутствует во всех источниках и стабилен — базовая размерность. Бренд/магазин/списки накладываются поверх через справочники (OP-38), не входят в ключ.day— календарный день в московском времени (WB statistics/analytics ведут учёт по МСК;rnp_funnel_stats/ad_campaign_statsуже в днях МСК).wb_orders/wb_sales(timestamptz) приводятся к дню МСК.
2. Трёхслойная модель (поверх ADR-0008)
- Raw fact-таблицы (как есть, по источникам) — не меняются.
- Дневные роллапы
<domain>_daily(механизм OP-92 re-aggregate): на источник — идемпотентный агрегат по(wb_account_id, nm_id, day)(sales_daily,ads_daily,finance_daily,funnel_daily). Партиционируются помесячно (OP-91), пересчитываются по событию late-arriving (OP-92). - Витрина
rnp_daily—FULL/LEFT JOINвсех<domain>_dailyпо(wb_account_id, nm_id, day)в одну строку. Отсутствие источника за день → нули (домен не прислал данных), а не пропуск строки.
3. Правила объединения и разрешения конфликтов
- Дубли — сняты на уровне raw идемпотентным upsert (уникальные ключи выше); дневной
роллап детерминирован (
SUMколичеств/сумм, конверсии — из воронки как есть). - Late-arriving — re-aggregate (OP-92) пересчитывает затронутый
(account, nm_id, day); витрина пересобирается из*_dailyидемпотентно. - Привязка финансов к периоду (ключевое правило, на согласование).
wb_report_linesимеют период отчёта реализации, а не день заказа. Предлагается привязывать затраты к дню исходного заказа (srid→ заказ →nm_id+ день заказа), чтобы расходы стояли в одной строке с продажами/рекламой/воронкой того же дня. Альтернатива — день отчёта (проще, но рассинхрон с воронкой/рекламой). Рекомендация: день заказа. - Бренд — из любого источника с непустым
brand/brand_id(nm_id→ один бренд, конфликт маловероятен; при расхождении — приоритетwb_report_lines>wb_sales> каталог OP-38). - Валюта — рубли (WB отдаёт RUB;
currency-поля проверяются, не-RUB логируется и пропускается до отдельного решения по мультивалютности).
4. Схема витрины rnp_daily (предлагаемая)
Ключ: wb_account_id, nm_id, day, brand. Метрики (нули при отсутствии источника):
- Продажи:
orders_count,orders_sum,buyouts_count,buyouts_sum,returns_count,returns_sum. - Реклама:
ad_views,ad_clicks,ad_spend,ad_orders. - Финансы:
commission,logistics,penalties,to_pay(к перечислению). - Воронка:
open_card_count,add_to_cart_count,add_to_cart_conversion,cart_to_order_conversion,buyout_percent. - Служебное:
recomputed_at.
Производные (ДРР = ad_spend/orders_sum, маржа, CTR, CR) — не хранятся, считаются в
API аналитики (OP-83) на лету / в агрегатах за период. Партиционирование rnp_daily —
помесячно по day (OP-91); агрегация в неделю/месяц — поверх (on-the-fly в OP-83,
материализация недельных роллапов — отдельный тикет при потребности).
Вопросы на согласование (заказчик/руководитель)
- Привязка финансов: день заказа (
srid) — рекомендуется — vs день отчёта реализации. - Часовой пояс дня: МСК — рекомендуется.
- Гранулярность хранения: только дневная
rnp_daily+ агрегация за период на лету (рекомендуется) vs материализовать недельные/месячные роллапы сразу. - Состав метрик витрины — достаточен ли список выше для отчётов 5.6, или добавить (напр. остатки/оборачиваемость из раздела «Остатки», упущенные продажи).
Последствия / готовность к OP-82
- Реализация витрины (OP-82) = per-domain re-aggregate-роллаперы (контракт OP-92) →
*_daily→ ETL объединения вrnp_daily; всё идемпотентно и пересчитываемо. - Решение согласовано (2026-06-29): приняты рекомендованные ответы на все 4 вопроса. OP-82 реализует схему витрины один-в-один по этому ADR.
Реализация (OP-82) — уточнения и ограничения
Реализовано в модуле rnp: роллаперы rnp-sales/rnp-ads/rnp-finance
(ReAggregateHandler, OP-92) → sales_daily/ads_daily/finance_daily, merge
rnp → rnp_daily; плановый ETL rnp-merge-sync пересчитывает окно последних дней.
funnel_dailyотдельной таблицей не заводится — её роль исполняет существующаяrnp_funnel_stats(OP-78), уже дневной агрегат по(account, nm_id, date)от WB Analytics. Merge читает воронку прямо из неё.- Ключ — составной PK
(wb_account_id, nm_id, day)(безbrand): PostgreSQL требует колонку партиции (day) в каждом UNIQUE/PK и не допускает NULL в PK, аbrand— производный атрибут (однаnm_id→ один бренд). Бренд хранится как обычная колонка. - Идемпотентность включает удаления: пересчёт дня заменяет строки роллапа
транзакцией
DELETE (account, day) + INSERT, иначе выбывший из источника артикул (например, единственный заказ дня отменён) завис бы устаревшим агрегатом. - Бутстрап партиций (миграция) пред-создаёт прошлый + текущий + 3 будущих месяца — окно пересчёта ETL заходит в прошлый месяц в его первые дни, а менеджер OP-91 создаёт партиции только вперёд.
Известные ограничения (приняты осознанно):
- Строки
wb_report_linesбез сопоставимого заказа и безrr_dt(или с заказом, у которогоdate IS NULL) в витрину не попадают — крайне редкий случай битых дат. - Артикул, у которого за день была только реклама (нет продаж/финансов/воронки),
получает
brand = NULL(вad_campaign_statsбренда нет) — бренд добирается из других дней/каталога (OP-38). - Дедуп merge-джобы по
jobId(OP-92): при concurrency воркера > 1 merge может снять снапшот*_dailyдо завершения последнего домена; расхождение сходится следующим событием/прогоном ETL (документированное поведение OP-92, не специфично для OP-82). - Дневная агрегация выполняется в приложении (чистые функции — ради тестируемости на
эталонных наборах, AC OP-90); объёмы ограничены одним
(кабинет, день)на джобу.