gendesign/data/sql/120_backfill_kn_price_per_m2.sql
Light1YT 36bdda9290 fix(tradein): renumber migration 120 + numeric cast for PERCENTILE_CONT (#573)
Rename 100_backfill_kn_price_per_m2.sql → 120_backfill_kn_price_per_m2.sql to
avoid collision with three existing 100_* files on main (max was 119).
Cast median_ppm2 to ::numeric in Pass 3 so ROUND(double precision, int) resolves
to ROUND(numeric, int) — the double precision overload does not exist in PostgreSQL.
2026-05-28 18:44:12 +05:00

76 lines
3.4 KiB
PL/PgSQL

-- 120_backfill_kn_price_per_m2.sql
-- Context: Issue #573 — domrf_kn_flats.price_per_m2 is populated in only ~3.7%
-- of rows (status='free'/'sold'/'booked'). The 756 977 NULL-status rows imported
-- from historical DOM.RF snapshots carry total_area but no price_rub, so the
-- scraper's price_rub/total_area derivation never fires for them.
-- This leaves 5/8 EKB districts with 0% price_per_m2 coverage and NULL district
-- medians, blocking the Limit_District_Median_Partial analytics limitation.
--
-- Strategy (three-pass, most-precise source first):
-- Pass 1 — direct arithmetic: price_rub / total_area where both columns present
-- and price_per_m2 is still NULL or zero. Covers any rows the scraper
-- missed (idempotent sanity net).
-- Pass 2 — sale_graph join: use domrf_kn_sale_graph.price_avg (type='apartments',
-- latest snapshot) as a proxy for the object-level avg ₽/м². Applied
-- only to rows where Pass 1 left price_per_m2 NULL and the object has
-- a sale_graph row.
-- Pass 3 — peer median: for objects without sale_graph, derive price_per_m2 from
-- the PERCENTILE_CONT(0.5) of free/sold/booked flats in the same obj_id.
--
-- Idempotent: WHERE price_per_m2 IS NULL guard prevents re-overwriting correct values.
-- Dependencies: domrf_kn_flats, domrf_kn_sale_graph (migration 50_schema_kn_extensions.sql).
-- Apply order: after 99_nspd_entities_denorm.sql.
-- Estimated runtime on prod: < 30 s (785 K rows, index on obj_id exists).
BEGIN;
-- ── Pass 1: direct price_rub / total_area ────────────────────────────────────
UPDATE public.domrf_kn_flats
SET price_per_m2 = ROUND(price_rub / total_area, 2)
WHERE price_rub IS NOT NULL
AND total_area IS NOT NULL
AND total_area > 0
AND (price_per_m2 IS NULL OR price_per_m2 = 0);
-- ── Pass 2: sale_graph proxy (latest snapshot, type='apartments') ─────────────
-- domrf_kn_sale_graph.price_avg is the average contract ₽/м² reported by DOM.RF
-- for the object in a given month. We use the most recent snapshot's average
-- across all apartment sub-rows as a reasonable object-level proxy.
WITH sg_latest AS (
SELECT obj_id,
AVG(price_avg) AS avg_ppm2
FROM public.domrf_kn_sale_graph
WHERE type = 'apartments'
AND price_avg > 50000
AND snapshot_date = (
SELECT MAX(snapshot_date)
FROM public.domrf_kn_sale_graph
WHERE type = 'apartments'
)
GROUP BY obj_id
)
UPDATE public.domrf_kn_flats f
SET price_per_m2 = ROUND(sg.avg_ppm2, 2)
FROM sg_latest sg
WHERE f.obj_id = sg.obj_id
AND f.price_per_m2 IS NULL;
-- ── Pass 3: peer median from free/sold/booked flats in same obj ───────────────
-- Fallback for objects absent from sale_graph: compute the object-level median
-- from the priced (status IS NOT NULL) flats in the same building.
WITH peer AS (
SELECT obj_id,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY price_per_m2) AS median_ppm2
FROM public.domrf_kn_flats
WHERE status IS NOT NULL
AND price_per_m2 IS NOT NULL
AND price_per_m2 > 0
GROUP BY obj_id
)
UPDATE public.domrf_kn_flats f
SET price_per_m2 = ROUND(p.median_ppm2::numeric, 2)
FROM peer p
WHERE f.obj_id = p.obj_id
AND f.price_per_m2 IS NULL;
COMMIT;