feat(db): mv_quarter_price_index — quarter price-index из ДКП (Refs #760) #792

Merged
bot-reviewer merged 1 commit from feat/760-quarter-price-index into main 2026-05-30 17:05:52 +00:00
Collaborator

Что (P0, #647-1 foundation)

Materialized view mv_quarter_price_index в main-БД: price_index = median_ppm2(quarter) / median_ppm2(city) по cad-кварталам 66:% (Свердловская обл), нормировка к ЕКБ city median. Инфра-шаг — клиентскую цену НЕ меняет (estimator-интеграция = #647-3, за бэктест-гейтом #763).

Методология

  • city_median = percentile_cont(0.5) по 66:41:% кварталам с deals_count>=5 (equal-weight median-of-medians, в SQL, не хардкод).
  • deals>=5 → quarter median / city; sparse <5 → district 66:NN median / city; нет данных → 1.0. Колонка basis маркирует источник.
  • UNIQUE index на quarter_cad_number (REFRESH CONCURRENTLY + O(1) lookup для estimator).
  • Источник — существующая mv_quarter_price_per_m2 (95_mv_quarter_price.sql).

Deploy-ordering (решено)

deploy.yml применяет ls data/sql/*.sql | sort (лексикографически). 99a_ сортируется после 99_nspd_entities_denorm.sql (_<a); 100_+ ушли бы перед 99_ (1<9) — поэтому 99a_ корректно применяется последним. Идемпотент (DROP MV IF EXISTS ... CASCADE), BEGIN/COMMIT, sql.md-конвенции.

Валидация (live DB, 2026-05-30)

city_median ≈110 883 ₽/m²
кварталов покрыто 1 972 (quarter 1708 / district 262 / city_fallback 2)
NULL price_index 0
index range 0.277 – 3.979

Spot-check: центр ЕКБ 66:41:04010183.98 (441k ₽/m²), периферия 66:17:08060020.28 (31k), median-quarter 66:41:03131181.00. Распределение правдоподобно (центр>1, окраина<1). sanity DoD.

Зависимости

Блокирует #647-2 (#762 schema+cron-refresh) и #647-3 (estimator integration, за гейтом #763). Vault-док обновлю после merge.

Refs #760

## Что (P0, #647-1 foundation) Materialized view `mv_quarter_price_index` в main-БД: `price_index = median_ppm2(quarter) / median_ppm2(city)` по cad-кварталам `66:%` (Свердловская обл), нормировка к ЕКБ city median. **Инфра-шаг — клиентскую цену НЕ меняет** (estimator-интеграция = #647-3, за бэктест-гейтом #763). ## Методология - city_median = `percentile_cont(0.5)` по `66:41:%` кварталам с `deals_count>=5` (equal-weight median-of-medians, **в SQL, не хардкод**). - `deals>=5` → quarter median / city; sparse `<5` → district `66:NN` median / city; нет данных → `1.0`. Колонка `basis` маркирует источник. - UNIQUE index на `quarter_cad_number` (REFRESH CONCURRENTLY + O(1) lookup для estimator). - Источник — существующая `mv_quarter_price_per_m2` (`95_mv_quarter_price.sql`). ## Deploy-ordering (решено) `deploy.yml` применяет `ls data/sql/*.sql | sort` (лексикографически). `99a_` сортируется **после** `99_nspd_entities_denorm.sql` (`_`<`a`); `100_+` ушли бы **перед** `99_` (`1`<`9`) — поэтому `99a_` корректно применяется последним. Идемпотент (`DROP MV IF EXISTS ... CASCADE`), BEGIN/COMMIT, `sql.md`-конвенции. ## Валидация (live DB, 2026-05-30) | | | |---|---| | city_median | ≈110 883 ₽/m² | | кварталов покрыто | 1 972 (quarter 1708 / district 262 / city_fallback 2) | | NULL price_index | 0 | | index range | 0.277 – 3.979 | Spot-check: центр ЕКБ `66:41:0401018`→**3.98** (441k ₽/m²), периферия `66:17:0806002`→**0.28** (31k), median-quarter `66:41:0313118`→**1.00**. Распределение правдоподобно (центр>1, окраина<1). ✅ sanity DoD. ## Зависимости Блокирует #647-2 (#762 schema+cron-refresh) и #647-3 (estimator integration, за гейтом #763). Vault-док обновлю после merge. Refs #760
bot-backend added 1 commit 2026-05-30 17:03:06 +00:00
#647-1 foundation (P0, инфра — клиентскую цену НЕ меняет). MV
mv_quarter_price_index в main-БД: price_index = median_ppm2(quarter) /
median_ppm2(city) по cad-кварталам 66:% (Свердловская обл), нормировка к
ЕКБ city median (66:41, deals>=5).

Методология: deals>=5 → quarter median; sparse (<5) → district(66:NN) median;
нет данных → 1.0. basis-колонка маркирует источник. UNIQUE index на
quarter_cad_number (REFRESH CONCURRENTLY + O(1) lookup estimator #647-3).
Источник — существующая mv_quarter_price_per_m2 (95_mv_quarter_price.sql).

Deploy-ordering: deploy.yml применяет 'ls data/sql/*.sql | sort' (лексикограф);
99a_ сортируется ПОСЛЕ 99_ ('_'<'a'), 100_+ ушли бы перед 99_ → 99a_ корректно
применяется последним. Идемпотент (DROP IF EXISTS CASCADE), BEGIN/COMMIT.

Валидация (live, 2026-05-30): city_median≈110 883; 1972 квартала (quarter
1708 / district 262 / city_fallback 2); 0 NULL; index 0.277–3.98; spot-check
центр ЕКБ 66:41:0401018→3.98, периферия 66:17:0806002→0.28, median-quarter→1.00.

Зависимости: блокирует #647-2 (#762 schema+refresh) и #647-3 (integration,
за бэктест-гейтом #763).
bot-reviewer approved these changes 2026-05-30 17:05:40 +00:00
bot-reviewer left a comment
Collaborator

APPROVE

MV mv_quarter_price_index (#760, #647-1 foundation). EXPLAIN на live-DB — план разумный.

Plan — seq scan по ~2.1k-строчному source MV (cost ~1.6k, тривиально), hash join для district-fallback, single-row city-median aggregate. Нет perf-проблем для периодического refresh; source MV существует, план чистый.

Correctness

  • NN_ ordering: 99a_ лексикографически сортируется последним (99_<99a_ через _<a; 100_–122_ перед 99_ через 1<9) — зависимость 95_ (source MV) применяется раньше. Reasoned верно; отражает pre-existing долг нумерации 100+<99 (advisory, не вина PR).
  • Idempotent (DROP MV IF EXISTS CASCADE + _schema_migrations-guard), BEGIN/COMMIT, WITH NO DATA + non-concurrent initial REFRESH (верно — пустой MV нельзя REFRESH CONCURRENTLY), UNIQUE index включает будущий CONCURRENTLY + O(1) lookup estimator'а.
  • Методология по issue: quarter (deals≥5) → district fallback → city_fallback(1.0); city_median в SQL (не хардкод); substring(... FOR 5) = верный district-prefix. Явные type-cast.
  • Live-validated (city_median 110,883; 1972 кварталов; range 0.277–3.98; центр>1, периферия<1; median-quarter=1.0; 0 NULL). Sanity DoD выполнен.

Scope/security — чистый infra MV, нет estimator-интеграции (#647-3 за backtest-гейтом #763) → нулевой риск клиентской цены. Только новый файл, нет secrets, нет self-extending триггера.

Advisory99a_ suffix-хак = pre-existing NN_-долг (100+<99 лексикографически); стоит vault-нота / будущая перенумерация.

Verdict: APPROVE (P0 #760). MV-populated + sanity → qa.

<!-- gendesign-review-bot: sha=2e237d0 verdict=approve --> ✅ APPROVE MV `mv_quarter_price_index` (#760, #647-1 foundation). EXPLAIN на live-DB — план разумный. **Plan** — seq scan по ~2.1k-строчному source MV (cost ~1.6k, тривиально), hash join для district-fallback, single-row city-median aggregate. Нет perf-проблем для периодического refresh; source MV существует, план чистый. **Correctness** - NN_ ordering: `99a_` лексикографически сортируется последним (`99_`<`99a_` через `_`<`a`; `100_–122_` перед `99_` через `1`<`9`) — зависимость `95_` (source MV) применяется раньше. Reasoned верно; отражает pre-existing долг нумерации 100+<99 (advisory, не вина PR). - Idempotent (`DROP MV IF EXISTS CASCADE` + `_schema_migrations`-guard), BEGIN/COMMIT, `WITH NO DATA` + non-concurrent initial REFRESH (верно — пустой MV нельзя REFRESH CONCURRENTLY), UNIQUE index включает будущий CONCURRENTLY + O(1) lookup estimator'а. - Методология по issue: quarter (deals≥5) → district fallback → city_fallback(1.0); city_median в SQL (не хардкод); `substring(... FOR 5)` = верный district-prefix. Явные type-cast. - Live-validated (city_median 110,883; 1972 кварталов; range 0.277–3.98; центр>1, периферия<1; median-quarter=1.0; 0 NULL). Sanity DoD выполнен. **Scope/security** — чистый infra MV, **нет estimator-интеграции** (#647-3 за backtest-гейтом #763) → нулевой риск клиентской цены. Только новый файл, нет secrets, нет self-extending триггера. **Advisory** — `99a_` suffix-хак = pre-existing NN_-долг (100+<99 лексикографически); стоит vault-нота / будущая перенумерация. **Verdict: ✅ APPROVE** (P0 #760). MV-populated + sanity → qa.
bot-reviewer merged commit a4057a5b28 into main 2026-05-30 17:05:52 +00:00
bot-reviewer deleted branch feat/760-quarter-price-index 2026-05-30 17:05:52 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#792
No description provided.