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

dim_project (ClickHouse dimension)

Реплика-измерение «Проект» (ЖК / апарт-отель) в БД dev-stats. JOIN-free справочник для резолва имени/типа/класса/статуса проекта и для фильтрации метрик по уровню Project — минимальному обязательному уровню детализации в ЛК застройщика (ADR-0057). Дом полей источника — chessboard: ProjectEntity; здесь — только колонки, нужные dev-stats.

Назначение

dim_project хранит текущий снапшот проекта (одна логическая строка на ProjectId), реплицируемый из pin.chessboard.project-upserted.v1. Факты уже несут денормализованный ProjectId/DeveloperId; это измерение даёт подпись графика (Name) и атрибуты для разбивки/фильтра просмотров по проекту (Type, Class, ConstructionStatus, RegionId+RegionName). Не факт, а стабильное измерение (ReplacingMergeTree, последняя версия перезаписывает предыдущую).

Table

ПолеЗначение
Databasepin_dev_stats
Tabledim_project
EngineReplacingMergeTree(UpsertedAt)
Owner servicedev-stats (DevStats.Background пишет, DevStats.Api читает; DDL — DevStats.Migrator)
Source eventspin.chessboard.project-upserted.v1
Partition / Orderбез партиционирования (тысячи строк); ORDER BY (ProjectId)
Freshness SLA≤ 5 минут от события до видимости

Стабильное измерение, не append-only факт: последняя версия перезаписывает предыдущую. Онлайн-чтение — дубль-устойчивое, без FINAL: актуальная строка берётся argMax(col, UpsertedAt) c GROUP BY ProjectId.

Columns

ColumnTypeNullableSource (поле ProjectEntity)Partition/OrderPII
ProjectIdUUIDнетIdorder (единственный ключ)нет
DeveloperIdUUIDнетDeveloperId (= TenantId) — родитель вверх по иерархииnoneнет
NameStringнетNamenoneнет
StatusLowCardinality(String)нетStatusDraft / Published / Hidden / Archivednoneнет
TypeLowCardinality(String)нетTypeResidentialComplex / ApartHotel / Hotel / Villanoneнет
ClassLowCardinality(String)нетClassStandard / Comfort / Business / Premium / Elitenoneнет
ConstructionStatusLowCardinality(String)нетConstructionStatusPlanned / UnderConstruction / Commissioned / Deliverednoneнет
RegionIdInt64да (пусто ⇔ гео не резолвлено в источнике; у Published заполнен)RegionId — ссылка на справочник handbook, ключ разбивки/фильтраnoneнет
RegionNameLowCardinality(String)да (заполнен ⇔ RegionId задан)RegionSnapshot.Name — денормализованное имя региона на момент публикацииnoneнет
SourceVersionInt64нетVersion источника — монотонный guardnoneнет
UpsertedAtDateTime64(3)нетвремя события (CreatedAt)none (версия движка)нет

Минимальный набор для фильтрации/подписи. Денормализованные цены/счётчики (PriceMin/PropertyObjectsFreeCount) и jsonb-вкладки в dev-stats не нужны — не реплицируются (метрики считаются по фактам просмотров, а не по атрибутам продаж).

Инварианты

  • INV-DIM-PRJ-1 (единственность): одна логическая строка на ProjectId (единственный компонент ORDER BY); merge оставляет строку с максимальным UpsertedAt.
  • INV-DIM-PRJ-2 (родитель денормализован): DeveloperId реплицируется в строку проекта, чтобы восхождение по иерархии и проверка scope шли без обращения к dim_developer.
  • INV-DIM-PRJ-3 (не удаляется): строка живёт, пока существует проект; Status = Archived/Hidden помечает архив/скрытость, но не удаляет (3-летние факты должны резолвить подпись/фильтр).
  • INV-DIM-PRJ-4 (precedence): актуальность — по UpsertedAt (max побеждает); SourceVersion монотонна per-key, служит анти-out-of-order-диагностикой; устаревшая доставка не понижает строку.
  • INV-DIM-PRJ-5 (минимальность): реплицируется только набор для подписи/разбивки (Name/Type/Class/ ConstructionStatus/RegionId+RegionName); цены/счётчики/валюта/страна/город/jsonb-вкладки не хранятся; PII нет (проект — объект).
  • INV-DIM-PRJ-6 (гео-денормализация): RegionName денормализуется из RegionSnapshot.Name события и не «догоняется» самостоятельно — переименование региона в справочнике доезжает переопубликацией события источником (HR-6 chessboard); RegionId остаётся ключом разбивки при устаревшем имени.

Кросс-сервисный контракт

  • Ожидает от chessboard (project-upserted): Id, DeveloperId, Name, Status, Type, Class, ConstructionStatus, RegionId + RegionSnapshot (гео-ссылка + display-снапшот региона), Version; key = Id (порядок per-project), at-least-once, атомарная публикация снапшота, additive-only для minor; порядок между агрегатами разных уровней не гарантирован. Страна/город и валюта событием несутся, но в измерение не реплицируются.
  • Предоставляет downstream (в пределах dev-stats): JOIN-free подпись (Name) и атрибуты разбивки/фильтра метрик по уровню Project. Измерение не на критическом пути обогащения факта — факт несёт денормализованный ProjectId; dim_project нужен для подписи/фильтра при чтении.
  • При отсутствии/устаревании: строки ещё нет → метрика резолвится по ProjectId, подпись деградирует до id без ошибки; устаревшая доставка → берётся max(UpsertedAt).

Deduplication

  • Dedup key: ProjectId (единственный компонент ORDER BY); ReplacingMergeTree(UpsertedAt) оставляет строку с максимальным UpsertedAt при merge.
  • Replay behavior: идемпотентный upsert; повтор топика логических дублей не даёт, до merge — дубль-устойчивое чтение argMax(…, UpsertedAt) … GROUP BY ProjectId (FINAL в online запрещён).
  • Version guard: событие применяется только при event.Version > SourceVersion (anti-out-of-order); иначе идемпотентный пропуск.

Partition И TTL

  • Partition key: нет — измерение мало (проектов тысячи).
  • Order key: (ProjectId) — точечный резолв и фильтр WHERE DeveloperId = актор AND ProjectId = ….
  • TTL/retention: нет — измерение живёт, пока существует проект; Status = Archived/Hidden не удаляет строку (3-летние факты должны резолвиться), а помечает архив/скрытость.

Consumers И Backfill

ProcessНазначениеCheckpoint
Приём pin.chessboard.project-upserted.v1 (фоновый ingest)Проверка версии, маппинг snapshot → строка измерения, batch-upsert (≤ 1000 / 5 с)Kafka consumer offset (commit после вставки батча)
Backfill измеренийПовтор топика project-upserted / перезаливка при вводе сервиса (идемпотентно)Kafka offset / диапазон ProjectId

Security

  • Raw PII не хранится (проект — объект, не пользователь).
  • Доступ — роль застройщика, permission dev-stats.view, scope строго = свой DeveloperId; каждый запрос фильтрует DeveloperId = актор, чужие проекты недоступны.
  • ClickHouse не operational source of truth: источник проекта — chessboard.

Acceptance criteria

  • Happy upsert. Given project-upserted с непустыми Id/DeveloperId, When обработан батч, Then в dim_project есть строка с Name/Status/Type/Class/ConstructionStatus/RegionId/RegionName и UpsertedAt = время события; чтение argMax(…, UpsertedAt) GROUP BY ProjectId (без FINAL) отдаёт значения.
  • Идемпотентный повтор / устаревшая доставка. Given то же/старое событие доставлено повторно, When обработано, Then после merge логически одна строка; побеждает max(UpsertedAt).
  • Archived/Hidden не удаляет. Given проект переведён в Archived/Hidden, When событие обработано, Then строка остаётся, факты 3-летней глубины резолвят подпись/фильтр.
  • Деградация подписи. Given строки ещё нет при чтении метрики, Then метрика резолвится по ProjectId, подпись показывает id без ошибки.
  • Scope. Given запрос застройщика, When фильтруется dim_project, Then только строки с DeveloperId = актор; чужой проект недоступен.

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

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

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