dim_property_object (ClickHouse dimension)
Реплика-измерение «Объект недвижимости» в БД dev-stats — ключевое измерение обогащения. Событие
pin.user-activity.property-object-view-ended.v1 несёт только PropertyObjectId, но НЕ несёт вышестоящую иерархию; консюмер
факта property_object_view_facts резолвит DeveloperId → ProjectId → BuildingId → LayoutGroupId → LayoutId из этой таблицы (dim_property_object → вверх) перед вставкой факта. Дом полей источника —
chessboard: PropertyObjectEntity; здесь — только колонки, нужные dev-stats.
Назначение
dim_property_object хранит текущий снапшот объекта недвижимости (одна логическая строка на PropertyObjectId), реплицируемый из
pin.chessboard.property-object-upserted.v1. Две роли:
- Обогащение фактов иерархией — консюмер
property-object-view-endedберёт изdim_property_objectвесь путь вверх (DeveloperId,ProjectId,BuildingId,LayoutGroupId,LayoutId), чтобы факт просмотра лёг вproperty_object_view_factsуже денормализованным (JOIN-free аналитика по любому уровню, ADR-0057). - Фильтрация/подпись по конкретному объекту —
Number(«квартира 145»),Status,Floor,Rooms.
Не факт, а стабильное измерение (ReplacingMergeTree, последняя версия перезаписывает предыдущую). Событие
property-object-upserted уже несёт денормализованный LayoutGroupId (через привязанную планировку), поэтому вся
иерархия доступна из одной строки dim_property_object — вторичный резолв через dim_layout не требуется.
Table
| Поле | Значение |
|---|---|
| Database | pin_dev_stats |
| Table | dim_property_object |
| Engine | ReplacingMergeTree(UpsertedAt) |
| Owner service | dev-stats (DevStats.Background пишет и читает при обогащении, DevStats.Api читает; DDL — DevStats.Migrator) |
| Source events | pin.chessboard.property-object-upserted.v1 |
| Partition / Order | без партиционирования (сотни тысяч строк, ADR-0056); ORDER BY (PropertyObjectId) |
| Freshness SLA | ≤ 5 минут от события до видимости |
Стабильное измерение, не append-only факт: последняя версия перезаписывает предыдущую. Онлайн-чтение
(в т.ч. lookup при обогащении факта) — дубль-устойчивое, без FINAL: актуальная строка берётся
argMax(col, UpsertedAt) c GROUP BY PropertyObjectId.
Columns
| Column | Type | Nullable | Source (поле PropertyObjectEntity / payload property-object-upserted) | Partition/Order | PII |
|---|---|---|---|---|---|
PropertyObjectId | UUID | нет | Id | order (единственный ключ) | нет |
BuildingId | UUID | нет | BuildingId — родитель | none | нет |
LayoutId | Nullable(UUID) | да | LayoutId (может отсутствовать) | none | нет |
LayoutGroupId | Nullable(UUID) | да | LayoutGroupId (денормализация из планировки в payload) | none | нет |
ProjectId | UUID | нет | ProjectId — вверх по иерархии | none | нет |
DeveloperId | UUID | нет | TenantId (= DeveloperId проекта) — вершина иерархии | none | нет |
Status | LowCardinality(String) | нет | Status — Free / Reserved / Booked / Sold / Hidden | none | нет |
Number | String | нет | Number («квартира 145», «номер 12-А») | none | нет |
Floor | Int16 | нет | Floor (ось Y шахматки) | none | нет |
Rooms | Int16 | нет | Rooms (0 = студия) — атрибут разбивки | none | нет |
PublishedAt | Nullable(DateTime) | да | CreatedAt объекта недвижимости из payload property-object-upserted — момент публикации объекта; иммутабелен, переживает upsert-ы | none | нет |
SourceVersion | Int64 | нет | Version источника — монотонный guard | none | нет |
UpsertedAt | DateTime64(3) | нет | время события (CreatedAt) | none (версия движка) | нет |
Минимальный набор: вся иерархия вверх (для обогащения) + атрибуты фильтрации/подписи объекта. Цена/площади/
виды из окон в dev-stats не нужны — не реплицируются. Status меняется событием property-object-upserted и не
обязан быть свежим до секунды: аналитика внимания к объекту не зависит от live-статуса продажи.
PublishedAt (иммутабельный момент публикации объекта недвижимости) нужен метрике
time-to-first-view — время от публикации до первого просмотра; так как
поле неизменно, ReplacingMergeTree-upsert его не портит (каждый snapshot несёт то же значение).
Инварианты
- INV-DIM-PO-1 (единственность): одна логическая строка на
PropertyObjectId(единственный компонентORDER BY); merge оставляет строку с максимальнымUpsertedAt. - INV-DIM-PO-2 (полная иерархия вверх): каждая строка несёт весь путь
DeveloperId → ProjectId → BuildingId → LayoutGroupId? → LayoutId?(DeveloperId = TenantId);LayoutGroupIdденормализован из планировки в payload, поэтому вторичный резолв черезdim_layoutне нужен — это инвариант самодостаточности обогащения. - INV-DIM-PO-3 (не удаляется): строка живёт, пока существует объект;
Status = Sold/Hiddenне удаляет строку, иначе 3-летние факты просмотров проданных/скрытых объектов перестали бы резолвиться в иерархию. - INV-DIM-PO-4 (precedence): актуальность — по
UpsertedAt(max побеждает);SourceVersionмонотонна per-key, служит анти-out-of-order-диагностикой. - INV-DIM-PO-5 (
PublishedAtиммутабелен): момент публикации неизменен — каждый snapshot несёт то же значение,ReplacingMergeTree-upsert его не портит (вход метрики time-to-first-view).
Кросс-сервисный контракт
- Ожидает от
chessboard(property-object-upserted):PropertyObjectId,BuildingId,LayoutId?,LayoutGroupId?(денормализован из планировки),ProjectId,TenantId(=DeveloperId),Status,Number,Floor,Rooms,CreatedAt(→PublishedAt),Version;key = PropertyObjectId(6 партиций, порядок per-object), at-least-once, атомарная публикация снапшота, additive-only для minor. - Предоставляет downstream — критический контракт обогащения (в отличие от прочих
dim_*): это единственный источник, из которого консюмер фактаproperty-object-view-endedрезолвит всю иерархию по одномуPropertyObjectIdперед вставкой факта (событие просмотра иерархию не несёт). Гарантия: после upsert весь путь вверх доступен из одной строки; lookup —argMax(…, UpsertedAt) GROUP BY PropertyObjectIdбезFINAL; свежесть ≤ 5 мин. Плюс подпись/фильтр по уровнюPropertyObject(Number/Status/Floor/Rooms). - При отсутствии/устаревании: строки ещё нет, а факт просмотра уже пришёл (гонка «факт раньше
property-object-upserted») → консюмер факта паркует сообщение (deferred re-enrich, при устойчивом промахе → DLQ + алерт «dimension gap»), просмотр не теряется. Поэтому измерение обязано наполняться до включения ingest фактов (backfill при вводе). Устаревшая доставка → берётся max(UpsertedAt).
Deduplication
- Dedup key:
PropertyObjectId(единственный компонентORDER BY);ReplacingMergeTree(UpsertedAt)при merge оставляет строку с максимальнымUpsertedAt. - Replay behavior: идемпотентный upsert; повтор топика логических дублей не даёт. До merge lookup при
обогащении факта дубль-устойчив:
argMax(DeveloperId, UpsertedAt), argMax(ProjectId, UpsertedAt), … FROM dim_property_object WHERE PropertyObjectId = ? GROUP BY PropertyObjectId—FINALв online запрещён (правило SDK). - Version guard: событие применяется только при
event.Version > SourceVersion; иначе идемпотентный пропуск. - Late-dimension / отсутствие строки: если факт просмотра пришёл раньше
property-object-upserted(гонка), консюмер факта не находитdim_property_object— стратегия обработки (parking/DLQ/deferred re-enrich) описана вproperty_object_view_facts; измерение лишь гарантирует upsert по приходу события.
Partition И TTL
- Partition key: нет — измерение адресуется точечно по
PropertyObjectId, шардировать нечего (ADR-0056). - Order key:
(PropertyObjectId)— lookup при обогащении и фильтрWHERE DeveloperId = актор AND … AND PropertyObjectId = …. - TTL/retention: нет — измерение живёт, пока существует объект недвижимости;
Status = Sold/Hiddenне удаляет строку, иначе 3-летние факты просмотров проданных/скрытых объектов недвижимости перестали бы резолвиться в иерархию.
Consumers И Backfill
| Process | Назначение | Checkpoint |
|---|---|---|
Приём pin.chessboard.property-object-upserted.v1 (фоновый ingest) | Проверка версии, маппинг snapshot (иерархия + атрибуты) → строка, batch-upsert (≤ 1000 / 5 с) | Kafka consumer offset (commit после вставки батча) |
| Backfill измерений | Повтор топика property-object-upserted при вводе сервиса — обязателен до включения ingest фактов, иначе обогащение не найдёт иерархию (идемпотентно) | Kafka offset / диапазон PropertyObjectId |
Security
- Raw PII не хранится (объект недвижимости — объект недвижимости, участники сделок — контекст
crm). - Доступ — роль застройщика, permission
dev-stats.view, scope строго = свойDeveloperId; каждый запрос API фильтруетDeveloperId = актор. Обогащение фактов также клеймитDeveloperIdизdim_property_object, что замыкает tenant-scope на источнике данных. - ClickHouse не operational source of truth: источник объекта недвижимости (статус/цена/паспорт) —
chessboard.
Acceptance criteria
- Happy upsert. Given
property-object-upsertedс непустымиPropertyObjectId/BuildingId/ProjectId/TenantId, When обработан батч, Then вdim_property_objectесть строка с полным путём вверх и атрибутамиNumber/Status/Floor/Rooms/PublishedAt; чтениеargMax(…, UpsertedAt) GROUP BY PropertyObjectId(безFINAL) отдаёт значения. - Обогащение факта. Given строка есть, When консюмер факта резолвит
PropertyObjectId, Then получаетDeveloperId/ProjectId/BuildingId/LayoutGroupId/LayoutIdиз одной строки (JOIN-free), без резолва черезdim_layout. - Late-dimension. Given строки ещё нет, а факт пришёл раньше, When консюмер факта не нашёл иерархию, Then
сообщение факта паркуется (deferred re-enrich); после прихода
property-object-upsertedи replay факт обогащается; просмотр не теряется,dev_stats_dim_property_object_miss_totalрастёт. - Идемпотентный повтор / устаревшая доставка. Given то же/старое событие доставлено повторно, When обработано,
Then после merge логически одна строка; побеждает max(
UpsertedAt). - Sold/Hidden не удаляют. Given объект в
Sold/Hidden, When событие обработано, Then строка остаётся, 3-летние факты продолжают резолвить иерархию. PublishedAtиммутабелен. Given повторный upsert объекта, When обработан, ThenPublishedAtне меняется, метрика time-to-first-view стабильна.- Scope. Given запрос застройщика, When фильтруется
dim_property_object, Then только строкиDeveloperId = актор; обогащение факта клеймит тот жеDeveloperId(замыкание tenant-scope на источнике).
Обратные ссылки
Автоссылки: где используется этот документ.
- API (1): GET /api/v1/dev-stats/time-to-first-view — Время до первого просмотра (TTFV)
- Use Cases (2): UC-STA-002. Детализация просмотров до здания / группы планировок / планировки, UC-STA-004. Воронка конверсии «просмотр → сделка»
- Консюмеры (10): Consumer: DevStatsActionTrackedConsumer, Consumer: DevStatsFunnelBookingCreatedConsumer, Consumer: DevStatsFunnelBookingPaidConsumer, Consumer: DevStatsFunnelDealCompletedConsumer, Consumer: DevStatsFunnelDealCreatedConsumer, Consumer: DevStatsFunnelFavoriteAddedConsumer, Consumer: DevStatsFunnelLeadCreatedConsumer, Consumer: DevStatsFunnelSelectionItemAddedConsumer, Consumer: DevStatsPropertyObjectUpsertedConsumer, Consumer: DevStatsPropertyObjectViewEndedConsumer
- dimensions (5): dim_building (ClickHouse dimension), dim_developer (ClickHouse dimension), dim_layout (ClickHouse dimension), dim_layout_group (ClickHouse dimension), dim_project (ClickHouse dimension)
- facts (2): funnel_daily (ClickHouse rollup / воронка), PropertyObjectViewFact (ClickHouse)
- registries (1): Реестр сущностей PIN
Связанные документы
- Источник: chessboard:
PropertyObjectEntity, событие property-object-upserted. - Потребитель обогащения: facts/property-object-view-facts.md (обогащение просмотра иерархией).
- Соседние измерения: developer.md, project.md, building.md, layout-group.md, layout.md.
- Шаблон: clickhouse-fact-template.md; ADR-0057, ADR-0056.