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

dim_layout (ClickHouse dimension)

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

Назначение

dim_layout хранит текущий снапшот планировки (одна логическая строка на LayoutId), реплицируемый из pin.chessboard.layout-upserted.v1. Даёт подпись графика (Name — «1К-А 32.5»), атрибуты для разбивки (Rooms, TotalArea) и родительские id (в т.ч. группу планировок) для восхождения по иерархии/фильтрации. Не факт, а стабильное измерение (ReplacingMergeTree).

Table

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

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

Columns

ColumnTypeNullableSource (поле LayoutEntity)Partition/OrderPII
LayoutIdUUIDнетIdorder (единственный ключ)нет
LayoutGroupIdNullable(UUID)даLayoutGroupId — родитель (может отсутствовать)noneнет
ProjectIdUUIDнетProjectId — планировки типовые в рамках проектаnoneнет
DeveloperIdUUIDнетTenantId (= DeveloperId проекта) — вверх по иерархииnoneнет
NameStringнетName («1К-А 32.5», «Студия S»)noneнет
RoomsInt16нетRooms (0 = студия) — атрибут разбивкиnoneнет
TotalAreaDecimal(8,2)нетTotalArea (типовая общая площадь, м²)noneнет
IsArchivedUInt8нетIsArchived (soft-delete планировки) — статус-эквивалентnoneнет
SourceVersionInt64нетVersion источника — монотонный guardnoneнет
UpsertedAtDateTime64(3)нетвремя события (CreatedAt)none (версия движка)нет

Минимальный набор для фильтрации/подписи. Жилая площадь, схемы (Schemas jsonb), счётчик объектов недвижимости (PropertyObjectsCount) в dev-stats не нужны — не реплицируются. У планировки нет статусного цикла (активна/архив через soft-delete), поэтому реплицируется флаг IsArchived из snapshot-а события.

Инварианты

  • INV-DIM-LAY-1 (единственность): одна логическая строка на LayoutId (единственный компонент ORDER BY); merge оставляет строку с максимальным UpsertedAt.
  • INV-DIM-LAY-2 (родитель-группа опционален): LayoutGroupId может отсутствовать (Nullable(UUID)); ProjectId и DeveloperId реплицируются всегда (планировки типовые в рамках проекта) для восхождения по иерархии/проверки scope без обращения к dim_project.
  • INV-DIM-LAY-3 (не удаляется): строка живёт, пока существует планировка; IsArchived = 1 помечает архив, но не удаляет (3-летние факты должны резолвить подпись/фильтр по уровню Layout).
  • INV-DIM-LAY-4 (precedence): актуальность — по UpsertedAt (max побеждает); SourceVersion монотонна per-key, служит анти-out-of-order-диагностикой.
  • INV-DIM-LAY-5 (статус-эквивалент): у планировки нет статусного цикла — вместо Status реплицируется флаг IsArchived; Rooms/TotalArea — атрибуты разбивки.

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

  • Ожидает от chessboard (layout-upserted): Id, LayoutGroupId?, ProjectId, TenantId (= DeveloperId), Name, Rooms, TotalArea, IsArchived, Version; key = LayoutId, at-least-once, атомарная публикация снапшота, additive-only для minor.
  • Предоставляет downstream (в пределах dev-stats): JOIN-free подпись (Name — «1К-А 32.5») и атрибуты разбивки (Rooms, TotalArea) для метрик уровня Layout. Измерение не на критическом пути обогащения факта: факт несёт денормализованный LayoutId (из dim_property_object); dim_layout нужен для подписи/ разбивки при чтении.
  • При отсутствии/устаревании: строки ещё нет → метрика уровня Layout резолвится по LayoutId, подпись деградирует до id без ошибки; устаревшая доставка → берётся max(UpsertedAt).

Deduplication

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

Partition И TTL

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

Consumers И Backfill

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

Security

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

Acceptance criteria

  • Happy upsert. Given layout-upserted с непустым Id, When обработан батч, Then в dim_layout есть строка с Name/Rooms/TotalArea/IsArchived, родителями ProjectId/DeveloperId и опциональным LayoutGroupId; чтение argMax(…, UpsertedAt) GROUP BY LayoutId (без FINAL) отдаёт значения.
  • Идемпотентный повтор / устаревшая доставка. Given то же/старое событие доставлено повторно, When обработано, Then после merge логически одна строка; побеждает max(UpsertedAt).
  • Архив не удаляет. Given планировка переведена в IsArchived = 1, When событие обработано, Then строка остаётся, факты 3-летней глубины резолвят подпись.
  • Группа отсутствует. Given LayoutGroupId не задан, When событие обработано, Then строка пишется с LayoutGroupId = NULL, метрики уровня Layout резолвятся корректно.
  • Деградация подписи. Given строки ещё нет при чтении метрики уровня Layout, Then метрика резолвится по LayoutId, подпись показывает id без ошибки.
  • Scope. Given запрос застройщика, When фильтруется dim_layout, Then только строки DeveloperId = актор.

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

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

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