property_object_view_daily (ClickHouse rollup / materialized view)
Дневной агрегат просмотров по иерархии застройщика × дата × тип зрителя. Нужен, чтобы графики за
месяцы/годы (DevStats.Api) не сканировали сырые property_object_view_facts. ADR-0057 требует rollup-ы (в отличие
от invest-analytics, где считают прямыми запросами). Онлайн-чтение — через *Merge-функции,
без FINAL.
Назначение
property_object_view_daily — AggregatingMergeTree-таблица, наполняемая materialized view property_object_view_daily_mv
над property_object_view_facts: на каждый вставленный факт MV эмитит частичные агрегатные состояния в целевую
таблицу, где они схлопываются по ключу (иерархия, EventDate, ViewerKind). Хранит меры: число просмотров,
уникальных зрителей, суммарную/среднюю длительность и повторные просмотры. Дедуп-устойчивость обеспечивают
uniqState-меры (склейка open/closed строк одного просмотра по PropertyObjectViewId) и идемпотентность консюмера по
FactId (MV не получает повтор одного факта).
Классификация
| Поле | Значение |
|---|---|
| Контекст | dev-stats (ADR-0057) |
| Kind | rollup — AggregatingMergeTree + materialized view над property_object_view_facts |
| Источник | таблица pin_dev_stats.property_object_view_facts (см. property-object-view-facts.md) |
| Хранилище | ClickHouse, database pin_dev_stats, table property_object_view_daily, MV property_object_view_daily_mv |
| Tenant isolation | DeveloperId — головной компонент ключа; фильтр обязателен и = актор |
Table
| Поле | Значение |
|---|---|
| Database | pin_dev_stats |
| Table | property_object_view_daily (+ MV property_object_view_daily_mv) |
| Engine | AggregatingMergeTree |
| Owner service | dev-stats (DevStats.Migrator создаёт таблицу и MV; наполнение — автоматически при вставке фактов) |
| Source | property_object_view_facts (триггер MV на INSERT) |
| Transform version | 1 |
| Freshness SLA | ≤ 5 минут (синхронно со вставкой фактов) |
Columns
Ключ группировки (dimensions) + агрегатные состояния (measures). EventDate = toDate(ViewedAt).
| Column | Type | Роль | Partition/Order |
|---|---|---|---|
DeveloperId | UUID | dimension | order (1) |
ProjectId | UUID | dimension | order (2) |
BuildingId | UUID | dimension | order (3) |
LayoutGroupId | UUID | dimension | order (4) |
LayoutId | UUID | dimension | order (5) |
PropertyObjectId | UUID | dimension | order (6) |
EventDate | Date | dimension = toDate(ViewedAt) | PARTITION BY toYYYYMM(EventDate), order (7) |
ViewerKind | LowCardinality(String) | dimension (Guest/Agent/Investor) | order (8) |
Views | AggregateFunction(uniq, UUID) | мера: число просмотров = uniqState(PropertyObjectViewId) | none |
UniqViewers | AggregateFunction(uniq, UUID) | мера: uniqState(coalesce(UserId, AnonymousId)) | none |
DurationSum | AggregateFunction(sum, Int32) | мера: sumState(DurationSeconds) | none |
AvgDuration | AggregateFunction(avg, Int32) | мера: avgState(DurationSeconds) | none |
RepeatViews не хранится отдельной колонкой, а выводится при чтении: uniqMerge(Views) − uniqMerge(UniqViewers) — просмотры сверх первого на каждого уникального зрителя (частота повторных
заходов). Это устойчиво к дублям и не требует оконных функций в MV.
Определение measures (dedup-tolerant)
Views = uniqState(PropertyObjectViewId)— «число просмотров» (канон: count) реализовано какuniqпоPropertyObjectViewId, чтобы open + closed строки одного просмотра и at-least-once дубли считались один раз.UniqViewers = uniqState(coalesce(UserId, AnonymousId))— уникальные зрители (гости — поAnonymousId).DurationSum = sumState(DurationSeconds)— open-строка вносит0, поэтому сумма = реальному суммарному времени просмотров (консюмер идемпотентен поFactId→ повтор closed не задваивается).AvgDuration: хранитсяavgState(DurationSeconds); так как open-строки добавляют нули, API для показа берётsumMerge(DurationSum) / uniqMerge(Views)(средняя длительность на просмотр), аavgMerge— как вспомогательный ряд. Оба варианта задокументированы, чтобы не путать нуль-смещение.
Инварианты
| Код | Инвариант | Смысл |
|---|---|---|
| INV-RD-1 DerivedFromFacts | Rollup детерминированно выводится из property_object_view_facts через MV; пересборка по диапазону партиций даёт тот же результат | rollup не source of truth — восстановим из фактов |
| INV-RD-2 DedupTolerantMeasures | Все меры устойчивы к дублям и паре open/closed (склейка по PropertyObjectViewId); at-least-once не задваивает | нет расхождения с фактами |
| INV-RD-3 KeyComplete | Ключ (иерархия, EventDate, ViewerKind) заполнен полностью (наследуется из факта, INV-LV-3) | нет строк с «дырявой» иерархией |
| INV-RD-4 RepeatNonNegative | RepeatViews = uniqMerge(Views) − uniqMerge(UniqViewers) ≥ 0 (просмотров всегда ≥ уникальных зрителей) | производная неотрицательна по определению |
| INV-RD-5 OpenZeroDuration | Open-строка вносит 0 в DurationSum; средняя длительность на показ = DurationSum / Views | нуль-смещение open-строк не искажает суммарное/среднее время |
| INV-RD-6 SyncTtl | TTL и глубина синхронны с фактами (EventDate + 3 года) | rollup не переживает и не отстаёт от источника |
Согласованность и промежуточное состояние
- Rollup наполняется синхронно со вставкой фактов (MV на INSERT) — окно freshness то же, что у фактов (≤ 5 мин). Отдельного лага rollup над фактами нет.
- Текущий (сегодняшний) день может «доуточняться»: по мере прихода closed-фактов растут длительности, по мере late-dimension enrichment — счётчики прошедших дней (идемпотентно). Ряд за завершённые прошлые дни стабилен.
- Rollup не потребляет события напрямую — только производен от
property_object_view_facts; все кросс-сервисные ожидания (иерархия изdim_*, поля просмотра) замкнуты на уровне факта.
Deduplication
AggregatingMergeTreeсхлопывает частичные состояния по ключуORDER BY; финальные значения даютuniqMerge/sumMerge/avgMerge.uniq-меры делают счётчики устойчивыми к дублированию (open/closed, at-least-once).FINALв online-запросах запрещён — вместо него всегда*Merge+GROUP BYпо нужному срезу ключа (правило аналитического чтения).
Partition И TTL
- Partition key:
toYYYYMM(EventDate). - Order key (подсказка по индексам):
(DeveloperId, ProjectId, BuildingId, LayoutGroupId, LayoutId, PropertyObjectId, EventDate, ViewerKind)— та же иерархия фильтров, что и вproperty_object_view_facts. Свёртка «по проекту за месяц» = префикс-скан(DeveloperId, ProjectId)+ партиции; детализация по зданию/группе/планировке/объекту недвижимости — удлинение префикса; разбивка по типу зрителя — хвостовойViewerKind.EventDateпередViewerKindдаёт временные ряды по дням внутри выбранного уровня иерархии. - TTL:
EventDate + INTERVAL 3 YEAR(синхронно с фактами).
Consumers И Backfill
| Process | Назначение | Checkpoint |
|---|---|---|
MV property_object_view_daily_mv | Автонаполнение при каждом INSERT в property_object_view_facts (частичные агрегатные состояния) | — (детерминированно от фактов) |
| Rebuild (будущий ../background/facts-backfill.md) | INSERT INTO property_object_view_daily SELECT ...State... FROM property_object_view_facts по диапазону партиций после backfill/схемных правок | Диапазон toYYYYMM(EventDate) |
Security
- Доступ — роль застройщика + permission
dev-stats.view; фильтрDeveloperIdобязателен и = актор (fail-closed). Агрегаты не раскрывают персональные данные — только счётчики и типы зрителей. - Прямой доступ к ClickHouse отсутствует; чтение — через
DevStats.Api.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (3): Классификация зрителя и источника (ViewerKind / ViewSource), Модели ответа dev-stats (типизированные ряды / точки / сводки), DevStatsFilterModel (общий вход метрик dev-stats)
- API (7): GET /api/v1/dev-stats/duration/histogram — Гистограмма длительностей просмотра, GET /api/v1/dev-stats/repeat-frequency — Повторные просмотры и частота возврата на объект, GET /api/v1/dev-stats/top-objects — Топ объектов по просмотрам и времени, GET /api/v1/dev-stats/viewers/unique — Уникальные зрители (гость/агент/инвестор), GET /api/v1/dev-stats/views/hierarchy-funnel — Воронка просмотров по иерархии, GET /api/v1/dev-stats/views/summary — Сводка просмотров (кто/сколько/как долго/как часто), GET /api/v1/dev-stats/views/trend — Динамика просмотров + сравнение периодов
- Use Cases (4): UC-STA-001. Застройщик смотрит статистику проекта (период + проект), UC-STA-002. Детализация просмотров до здания / группы планировок / планировки, UC-STA-003. Сравнение периодов и тренд просмотров, UC-STA-005. Экспорт отчёта статистики (Excel / PDF) с аудитом
- Консюмеры (1): Consumer: DevStatsPropertyObjectViewEndedConsumer
- Фоновые задачи (1): Background Job: FactsBackfillJob
- Отчёты (1): Отчет/Export: dev-stats-export (выгрузка аналитики застройщика)
- Дашборды (1): Панель Метрик Застройщика: dev-stats-overview (аналитика внимания)
- index.md (1): DevStats Service (Pin.DevStats)
- facts (3): funnel_daily (ClickHouse rollup / воронка), PropertyObjectViewFact (ClickHouse), SessionFact (ClickHouse)
- registries (1): Реестр сущностей PIN
Связанные документы
- Источник: property-object-view-facts.md; воронка: funnel-daily-rollup.md.
- Шаблон: ../../../99-templates/clickhouse-fact-template.md.
- ADR-0057 (rollup-ы обязательны), ADR-0056 (simplicity-first).