Перейти к основному содержимому

funnel_daily (ClickHouse rollup / воронка)

Дневная воронка конверсии по иерархии застройщика × дата: views → favorites → selections → leads → bookings → deals. Показывает застройщику, как внимание к его недвижимости превращается в лиды и сделки. ADR-0057. Онлайн-чтение — через *Merge, без FINAL.

Назначение

funnel_dailyAggregatingMergeTree-таблица, где на каждый уровень иерархии и дату хранятся уникальные счётчики этапов воронки. Наполняется из разных источников:

  • этап views — materialized view funnel_views_mv над property_object_view_facts (uniqState(PropertyObjectViewId));
  • этапы favorites / selections / leads / bookings / bookingsPaid / deals / dealsCompleted — консюмерами DevStats.Background, которые слушают доменные события favorites/selections/crm, обогащают иерархию из dim_property_object (событие несёт PropertyObjectId, но не иерархию) и вставляют частичную строку агрегата.

Каждый этап — uniqState по своему бизнес-ключу (id избранного, лида, брони, сделки), поэтому воронка идемпотентна к at-least-once: повтор события не увеличивает счётчик. Конверсии между этапами считаются при чтении отношением uniqMerge соседних этапов.

Классификация

ПолеЗначение
Контекстdev-stats (ADR-0057)
Kindrollup-воронка — AggregatingMergeTree + MV funnel_views_mv + ingest-консюмеры
Источникиproperty_object_view_facts; события favorites/selections/crm (через dim_property_object)
ХранилищеClickHouse, database pin_dev_stats, table funnel_daily
Tenant isolationDeveloperId — головной компонент ключа; фильтр обязателен и = актор

Table

ПолеЗначение
Databasepin_dev_stats
Tablefunnel_daily (+ MV funnel_views_mv для этапа views)
EngineAggregatingMergeTree (уникальные счётчики этапов; идемпотентность по бизнес-ключам)
Owner servicedev-stats (DevStats.Migrator — DDL/MV; DevStats.Background — ingest этапов)
Source eventspin.favorites.favorite-added.v1, pin.selections.selection-item-added.v1, pin.crm.lead-created.v1, pin.crm.booking-created.v1, pin.crm.booking-paid.v1, pin.crm.deal-created.v1, pin.crm.deal-completed.v1; этап views — из property_object_view_facts
Transform version1
Freshness SLA≤ 5 минут (batch-вставка / MV)

Columns

Ключ группировки (иерархия × дата) + агрегатные состояния этапов. EventDate = toDate(<время события>).

ColumnTypeРольPartition/Order
DeveloperIdUUIDdimensionorder (1)
ProjectIdUUIDdimensionorder (2)
BuildingIdUUIDdimensionorder (3)
LayoutGroupIdUUIDdimensionorder (4)
LayoutIdUUIDdimensionorder (5)
PropertyObjectIdUUIDdimensionorder (6)
EventDateDatedimensionPARTITION BY toYYYYMM(EventDate), order (7)
ViewsAggregateFunction(uniq, UUID)этап 1: uniqState(PropertyObjectViewId) (из property_object_view_facts)none
FavoritesAggregateFunction(uniq, UUID)этап 2: uniqState(FavoriteId)none
SelectionsAggregateFunction(uniq, UUID)этап 3: uniqState(SelectionItemId)none
LeadsAggregateFunction(uniq, UUID)этап 4: uniqState(LeadId)none
BookingsAggregateFunction(uniq, UUID)этап 5: uniqState(BookingId)none
BookingsPaidAggregateFunction(uniq, UUID)этап 5а: uniqState(BookingId) по booking-paidnone
DealsAggregateFunction(uniq, UUID)этап 6: uniqState(DealId)none
DealsCompletedAggregateFunction(uniq, UUID)этап 6а: uniqState(DealId) по deal-completednone

Каждый источник вставляет частичную строку: заполнено состояние своего этапа, остальные — пустые *State() (сливаются при merge). Конверсии (favorites/views, leads/favorites, deals/leads и т.д.) — производные при чтении, отдельными колонками не хранятся.

Инварианты

КодИнвариантСмысл
INV-FN-1 StageIdempotentКаждый этап — uniqState по своему бизнес-ключу (PropertyObjectViewId/FavoriteId/SelectionItemId/LeadId/BookingId/DealId)повтор события (at-least-once) не увеличивает счётчик этапа
INV-FN-2 PartialRowsMergeИсточник вставляет частичную строку только со своим этапом; AggregatingMergeTree сливает по ключупорядок доставки этапов не важен; этапы наполняются независимо
INV-FN-3 ConversionsDerivedКонверсии считаются при чтении как stage / prevStage; prevStage = 0nullотдельного хранения конверсий нет; нет деления на ноль
INV-FN-4 KeyCompleteКлюч (иерархия, EventDate) заполнен обогащением из dim_property_object (событие несёт только PropertyObjectId)воронка режется по любому уровню иерархии
INV-FN-5 NoViewerSplitВоронка не разбивается по ViewerKind (она про конверсию объектов/иерархии, а не сегмент аудитории)сегмент зрителя — в property_object_view_daily, не в воронке

Ожидание монотонности (не жёсткий инвариант): в устоявшемся состоянии верхние этапы ≥ нижних (views ≥ favorites ≥ … ≥ deals). При лаге источника нижнего этапа это временно нарушается — свойство freshness, не порча данных (см. ниже).

Согласованность, кросс-сервис и промежуточное состояние

  • Что ожидается от продюсеров этапов (favorites/selections/crm): бизнес-ключ этапа + PropertyObjectId
    • OccurredAt; доставка at-least-once; иерархию dev-stats добавляет сам из dim_property_object. Контракты входных событий — events/README.md.
  • Разные окна свежести этапов. Этап views (из фактов) обычно свежее нижних; crm-этапы могут догонять свою очередь. Верхние этапы считаются независимо от нижних (частичные состояния сливаются по ключу) — отстающий этап отдаётся текущим (частичным) счётчиком, конверсия к нему помечается «предварительно».
  • Feature-flag dev-stats.funnel.enabled гейтит незапущенные этапы (источник ещё не готов к проду): выключенные этапы отдаются с признаком Enabled = false, деградация — код FUNNEL_DISABLED (см. UC-STA-004).
  • Late-dimension (нет строки dim_property_object): этап паркуется (deferred re-enrich), затем ложится в партицию своего EventDate — идемпотентно (INV-FN-1).

Deduplication

  • Идемпотентность: каждый этап — uniqState по бизнес-ключу этапа (FavoriteId, LeadId, BookingId, DealId, PropertyObjectViewId). Повторная доставка события даёт тот же id → uniq не растёт. Дополнительно консюмер отсекает повтор по SourceEventId в батче.
  • AggregatingMergeTree схлопывает частичные состояния по ключу; чтение — uniqMerge + GROUP BY по срезу иерархии. FINAL в online-запросах запрещён (правило аналитического чтения).

Partition И TTL

  • Partition key: toYYYYMM(EventDate).
  • Order key (подсказка по индексам): (DeveloperId, ProjectId, BuildingId, LayoutGroupId, LayoutId, PropertyObjectId, EventDate) — иерархия фильтров застройщика префиксом. Воронка «по проекту за период» = префикс-скан (DeveloperId, ProjectId) + партиции; детализация до здания/группы/планировки/объекта недвижимости — удлинение префикса. EventDate в хвосте — дневной ряд внутри выбранного уровня. Разбивки по типу зрителя в воронке нет (она про конверсию объектов недвижимости/иерархии, не про сегмент аудитории — для этого property_object_view_daily).
  • TTL: EventDate + INTERVAL 3 YEAR.

Consumers И Backfill

ProcessИсточникЭтапОбогащение
MV funnel_views_mvproperty_object_view_facts (INSERT)Viewsиерархия уже в факте
favorites-added consumer (../consumers/favorite-added.md)pin.favorites.favorite-added.v1Favoritesdim_property_object по PropertyObjectId
selection-item-added consumer (../consumers/selection-item-added.md)pin.selections.selection-item-added.v1Selectionsdim_property_object
crm-lead consumer (../consumers/lead-created.md)pin.crm.lead-created.v1Leadsdim_property_object
crm-booking consumerpin.crm.booking-created.v1, booking-paid.v1Bookings, BookingsPaiddim_property_object
crm-deal consumerpin.crm.deal-created.v1, deal-completed.v1Deals, DealsCompleteddim_property_object
Rebuild (будущий)пересбор из фактов/событий по диапазонувсе

Checkpoint каждого консюмера — Kafka offset (commit после подтверждённой batch-вставки). Все консюмеры пишут в одну таблицу funnel_daily частичными строками; порядок доставки этапов роли не играет — AggregatingMergeTree сольёт состояния по ключу.

Security

  • Доступ — роль застройщика + permission dev-stats.view; фильтр DeveloperId обязателен и = актор (fail-closed). Воронка агрегатна — персональные данные лидов/клиентов не раскрываются.
  • Прямой доступ к ClickHouse отсутствует; чтение — через DevStats.Api. Не source of truth (операционные данные — в favorites/selections/crm).

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы