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
| Поле | Значение |
|---|---|
| Database | pin_dev_stats |
| Table | dim_layout |
| Engine | ReplacingMergeTree(UpsertedAt) |
| Owner service | dev-stats (DevStats.Background пишет, DevStats.Api читает; DDL — DevStats.Migrator) |
| Source events | pin.chessboard.layout-upserted.v1 |
| Partition / Order | без партиционирования (тысячи строк); ORDER BY (LayoutId) |
| Freshness SLA | ≤ 5 минут от события до видимости |
Стабильное измерение, не append-only факт: последняя версия перезаписывает предыдущую. Онлайн-чтение —
дубль-устойчивое, без FINAL: актуальная строка берётся argMax(col, UpsertedAt) c GROUP BY LayoutId.
Columns
| Column | Type | Nullable | Source (поле LayoutEntity) | Partition/Order | PII |
|---|---|---|---|---|---|
LayoutId | UUID | нет | Id | order (единственный ключ) | нет |
LayoutGroupId | Nullable(UUID) | да | LayoutGroupId — родитель (может отсутствовать) | none | нет |
ProjectId | UUID | нет | ProjectId — планировки типовые в рамках проекта | none | нет |
DeveloperId | UUID | нет | TenantId (= DeveloperId проекта) — вверх по иерархии | none | нет |
Name | String | нет | Name («1К-А 32.5», «Студия S») | none | нет |
Rooms | Int16 | нет | Rooms (0 = студия) — атрибут разбивки | none | нет |
TotalArea | Decimal(8,2) | нет | TotalArea (типовая общая площадь, м²) | none | нет |
IsArchived | UInt8 | нет | IsArchived (soft-delete планировки) — статус-эквивалент | none | нет |
SourceVersion | Int64 | нет | Version источника — монотонный guard | none | нет |
UpsertedAt | DateTime64(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 = актор.
Обратные ссылки
Автоссылки: где используется этот документ.
- Консюмеры (1): Consumer: DevStatsLayoutUpsertedConsumer
- dimensions (5): dim_building (ClickHouse dimension), dim_developer (ClickHouse dimension), dim_layout_group (ClickHouse dimension), dim_project (ClickHouse dimension), dim_property_object (ClickHouse dimension)
- registries (1): Реестр сущностей PIN
Связанные документы
- Источник: chessboard:
LayoutEntity, событие layout-upserted. - Соседние измерения: developer.md, project.md, building.md, layout-group.md, property-object.md.
- Шаблон: clickhouse-fact-template.md; ADR-0057, ADR-0056.