feat(db): mv_quarter_price_index — quarter price-index из ДКП (Refs #760) #792
1 changed files with 115 additions and 0 deletions
115
data/sql/99a_quarter_price_index.sql
Normal file
115
data/sql/99a_quarter_price_index.sql
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
-- 99a_quarter_price_index.sql
|
||||
-- Issue #760 — Materialized view mv_quarter_price_index: per-cadastral-quarter
|
||||
-- price index normalized to EKB city median. Foundation for estimator #647-3.
|
||||
--
|
||||
-- Naming: deploy.yml applies files via `ls -1 data/sql/*.sql | sort` (lexicographic).
|
||||
-- '99a_' sorts AFTER '99_nspd_entities_denorm.sql' because '_' (0x5F) < 'a' (0x61).
|
||||
-- Files 100_–122_ sort before 99_ (since '1' < '9') — they are already applied.
|
||||
-- This file applies last of all current migrations.
|
||||
--
|
||||
-- Dependencies:
|
||||
-- - public.mv_quarter_price_per_m2 (created by 95_mv_quarter_price.sql)
|
||||
-- Columns used: quarter_cad_number (varchar), deals_count (bigint), median (float8)
|
||||
--
|
||||
-- Methodology:
|
||||
-- 1. Source: mv_quarter_price_per_m2, filter scope = all 66:% quarters present.
|
||||
-- 2. City normalizer (city_median): percentile_cont(0.5) over EKB (66:41:%) quarters
|
||||
-- with deals_count >= 5. Computed in SQL — NOT hardcoded.
|
||||
-- 3. District fallback: per district prefix substring(quarter_cad_number for 5)
|
||||
-- (e.g. '66:41'), district_median = median of quarter medians (deals_count>=5).
|
||||
-- 4. price_index assignment:
|
||||
-- deals_count >= 5 → q.median / city_median basis='quarter'
|
||||
-- deals_count < 5 + district exists → district_median / city_median basis='district'
|
||||
-- no data → 1.0 basis='city_fallback'
|
||||
-- 5. Coverage: all 66:% quarters (Sverdlovsk oblast), normalizer = EKB city median.
|
||||
--
|
||||
-- REFRESH CONCURRENTLY: enabled via UNIQUE index on quarter_cad_number.
|
||||
-- Re-apply: DROP MATERIALIZED VIEW IF EXISTS ... CASCADE makes it idempotent.
|
||||
-- _schema_migrations tracking prevents re-apply in prod (deploy.yml guard).
|
||||
--
|
||||
-- Validation snapshot (2026-05-30, source MV current):
|
||||
-- city_median ≈ 110,883 ₽/m²
|
||||
-- total 66:% quarters covered: 1,972
|
||||
-- basis breakdown: quarter=1,708 / district=262 / city_fallback=2
|
||||
-- NULL price_index: 0
|
||||
-- index range: 0.2771 – 3.9788
|
||||
-- Near-1.0 quarter: 66:41:0313118 (index=1.0000, q_median=110,883)
|
||||
-- Top quarter: 66:41:0401018 (index=3.9788, q_median=441,176 — центр ЕКБ)
|
||||
-- Low quarter: 66:17:0806002 (index=0.2771, q_median=30,722 — периферия области)
|
||||
-- City-fallback examples: 66:00:0000000 (null-district), 66:39:0302011 (district no data)
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP MATERIALIZED VIEW IF EXISTS mv_quarter_price_index CASCADE;
|
||||
|
||||
CREATE MATERIALIZED VIEW mv_quarter_price_index AS
|
||||
WITH
|
||||
-- All 66:% quarters from source MV (Sverdlovsk oblast scope)
|
||||
all_66_quarters AS (
|
||||
SELECT
|
||||
quarter_cad_number,
|
||||
deals_count,
|
||||
median,
|
||||
substring(quarter_cad_number FOR 5) AS district_prefix
|
||||
FROM mv_quarter_price_per_m2
|
||||
WHERE quarter_cad_number LIKE '66:%'
|
||||
),
|
||||
|
||||
-- City normalizer: equal-weight median-of-quarter-medians for EKB (66:41:%)
|
||||
-- Uses only quarters with deals_count >= 5 to suppress noise.
|
||||
city_median_cte AS (
|
||||
SELECT
|
||||
percentile_cont(0.5) WITHIN GROUP (ORDER BY median) AS city_median
|
||||
FROM all_66_quarters
|
||||
WHERE quarter_cad_number LIKE '66:41:%'
|
||||
AND deals_count >= 5
|
||||
),
|
||||
|
||||
-- District fallback: median of quarter medians per district prefix,
|
||||
-- also restricted to deals_count >= 5 quarters.
|
||||
district_medians AS (
|
||||
SELECT
|
||||
district_prefix,
|
||||
percentile_cont(0.5) WITHIN GROUP (ORDER BY median) AS district_median
|
||||
FROM all_66_quarters
|
||||
WHERE deals_count >= 5
|
||||
GROUP BY district_prefix
|
||||
)
|
||||
|
||||
SELECT
|
||||
q.quarter_cad_number,
|
||||
CASE
|
||||
WHEN q.deals_count >= 5 THEN q.median / c.city_median
|
||||
WHEN dm.district_median IS NOT NULL THEN dm.district_median / c.city_median
|
||||
ELSE 1.0
|
||||
END::double precision AS price_index,
|
||||
q.deals_count AS n_deals,
|
||||
CASE
|
||||
WHEN q.deals_count >= 5 THEN 'quarter'
|
||||
WHEN dm.district_median IS NOT NULL THEN 'district'
|
||||
ELSE 'city_fallback'
|
||||
END::text AS basis,
|
||||
NOW() AS computed_at
|
||||
FROM all_66_quarters q
|
||||
CROSS JOIN city_median_cte c
|
||||
LEFT JOIN district_medians dm ON dm.district_prefix = q.district_prefix
|
||||
WITH NO DATA;
|
||||
|
||||
-- UNIQUE index required for REFRESH MATERIALIZED VIEW CONCURRENTLY.
|
||||
-- Also serves as O(1) PK lookup for the estimator (#647-3).
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS mv_quarter_price_index_uq
|
||||
ON mv_quarter_price_index (quarter_cad_number);
|
||||
|
||||
-- Initial populate (non-concurrent — MV is empty, CONCURRENTLY not allowed on empty MV).
|
||||
REFRESH MATERIALIZED VIEW mv_quarter_price_index;
|
||||
|
||||
COMMENT ON MATERIALIZED VIEW mv_quarter_price_index IS
|
||||
'Per-cadastral-quarter price index normalized to EKB city median (=1.0). '
|
||||
'Source: mv_quarter_price_per_m2. Scope: all 66:% (Sverdlovsk oblast) quarters. '
|
||||
'city_median = percentile_cont(0.5) over 66:41:% quarters with deals_count>=5. '
|
||||
'basis: quarter (>=5 deals) | district (sparse, district fallback) | city_fallback. '
|
||||
'Refresh: CONCURRENTLY safe (UNIQUE index on quarter_cad_number). '
|
||||
'Consumer: estimator service (#647-3) reads O(1) by quarter_cad_number. '
|
||||
'Issue: #760.';
|
||||
|
||||
COMMIT;
|
||||
Loading…
Add table
Reference in a new issue