From 3c30942cf7449c88dbe36401b2156a648c6e99c3 Mon Sep 17 00:00:00 2001 From: Light1YT Date: Thu, 28 May 2026 18:44:53 +0500 Subject: [PATCH] =?UTF-8?q?chore(mv):=20drop=20redundant=20migration=20?= =?UTF-8?q?=E2=80=94=20weighted-AVG=20fix=20already=20shipped=20in=20100?= =?UTF-8?q?=20(#295)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/rules/sql.md | 2 +- ...20_fix_mv_layout_velocity_weighted_avg.sql | 92 ------------------- 2 files changed, 1 insertion(+), 93 deletions(-) delete mode 100644 data/sql/120_fix_mv_layout_velocity_weighted_avg.sql diff --git a/.claude/rules/sql.md b/.claude/rules/sql.md index 9dabd886..b657593d 100644 --- a/.claude/rules/sql.md +++ b/.claude/rules/sql.md @@ -76,7 +76,7 @@ SUM(avg_value * cnt) / NULLIF(SUM(cnt), 0) и возвращает `NULL` вместо фейкового `0`. - Без весов: `AVG()` равноправно учитывает «пустые» месяцы → занижение. -Reference: fix #295 (`120_fix_mv_layout_velocity_weighted_avg.sql`), +Reference: fix #295 (`100_fix_mv_layout_velocity_weighted_avg.sql`), тест `backend/tests/sql/test_mv_layout_velocity_weighted_avg.py`. ## Запреты diff --git a/data/sql/120_fix_mv_layout_velocity_weighted_avg.sql b/data/sql/120_fix_mv_layout_velocity_weighted_avg.sql deleted file mode 100644 index 93330c36..00000000 --- a/data/sql/120_fix_mv_layout_velocity_weighted_avg.sql +++ /dev/null @@ -1,92 +0,0 @@ --- 120_fix_mv_layout_velocity_weighted_avg.sql --- Fix #295 — mv_layout_velocity used naive AVG() over pre-aggregated per-month rows. --- objective_corpus_room_month has rows even for months with 0 deals; those zero rows --- dragged the average down 2-10x depending on how sparse the project's deal history is. --- (e.g. a project with 1 deal in 1 month out of 24 reported months: AVG divided by 24, --- weighted AVG divides by 1 → correct value.) --- --- Fix: replace AVG() with count-weighted formula --- SUM(val * deals_total_count) / NULLIF(SUM(deals_total_count), 0) --- applied to both avg_area_m2 and avg_price_thousand_rub_per_m2. --- --- Prod verification (2026-05-28, Vitamin-квартал на Титова, last 24 mo): --- room | buggy_area | wtd_area | buggy_price | wtd_price --- studio| 19.11 | 26.03 | 129.83 | 175.83 --- 1-rm | 31.72 | 37.15 | 145.68 | 174.21 --- 2-rm | 41.44 | 55.89 | 98.52 | 134.97 --- 3-rm | 27.14 | 81.43 | 45.40 | 136.20 --- Weighted values match expected (studio ≈28m², 1-rm ≈38m², 2-rm ≈57m², 3-rm ≈81m², --- prices 110-180k ₽/m²). Sparse 3-rm (5 deals / 15 months) had 3x undercount. --- --- All indexes recreated identically to 94_mv_layout_velocity.sql: --- mv_layout_velocity_pk (UNIQUE on obj_id, room_bucket) — required for REFRESH CONCURRENTLY --- mv_layout_velocity_obj_idx (btree on obj_id) --- --- CASCADE check (2026-05-28): no dependent views or tables found → no extra DDL needed. --- --- Deploy: auto-applied by deploy.yml via _schema_migrations tracking. --- After apply: REFRESH MATERIALIZED VIEW CONCURRENTLY mv_layout_velocity is safe --- (MV is populated WITH DATA in this migration). --- --- WARN: Re-applying this file will DROP + recreate the MV (CASCADE-safe as no dependents), --- but will cause a brief data gap. Normally prevented by _schema_migrations tracking. - -BEGIN; - -DROP MATERIALIZED VIEW IF EXISTS mv_layout_velocity CASCADE; - -CREATE MATERIALIZED VIEW mv_layout_velocity AS -WITH last24mo AS ( - SELECT - ocm.project_name, - CASE - WHEN ocm.room_bucket = 'студия' THEN 'studio' - ELSE ocm.room_bucket - END AS room_bucket, - ocm.deals_total_count, - ocm.deals_total_avg_area_m2, - ocm.deals_total_avg_price_thousand_rub_per_m2, - ocm.deals_total_vol_m2, - ocm.report_month - FROM objective_corpus_room_month ocm - WHERE ocm.report_month >= (NOW() - INTERVAL '24 months')::date -) -SELECT - cm.domrf_obj_id AS obj_id, - l.room_bucket, - SUM(l.deals_total_count)::int AS total_deals_24mo, - -- Count-weighted average area: avoids zero-deal months dragging value down - (SUM(l.deals_total_avg_area_m2 * l.deals_total_count) - / NULLIF(SUM(l.deals_total_count), 0))::numeric(10, 2) AS avg_area_m2, - -- Count-weighted average price: same reasoning - (SUM(l.deals_total_avg_price_thousand_rub_per_m2 * l.deals_total_count) - / NULLIF(SUM(l.deals_total_count), 0))::numeric(12, 2) AS avg_price_thousand_rub_per_m2, - SUM(l.deals_total_vol_m2)::numeric(12, 2) AS total_vol_m2, - MIN(l.report_month) AS window_start, - MAX(l.report_month) AS window_end, - COUNT(DISTINCT l.report_month)::int AS months_with_data -FROM last24mo l -JOIN objective_complex_mapping cm - ON cm.objective_complex_name = l.project_name -WHERE l.room_bucket IS NOT NULL - AND cm.domrf_obj_id IS NOT NULL - AND cm.objective_group = 'Екатеринбург' -- защита от cross-region Cartesian при future multi-city -GROUP BY cm.domrf_obj_id, l.room_bucket -WITH DATA; - --- UNIQUE index required for REFRESH CONCURRENTLY (periodic refreshes via layout_velocity_refresh.py) -CREATE UNIQUE INDEX mv_layout_velocity_pk - ON mv_layout_velocity (obj_id, room_bucket); - --- Lookup index used by /best-layouts endpoint -CREATE INDEX mv_layout_velocity_obj_idx - ON mv_layout_velocity (obj_id); - -COMMENT ON MATERIALIZED VIEW mv_layout_velocity IS - 'Per-(obj_id, room_bucket) deals aggregation за last 24 months. ' - 'Source: objective_corpus_room_month × objective_complex_mapping (EKB only). ' - 'avg_area_m2 and avg_price_thousand_rub_per_m2 are COUNT-WEIGHTED to avoid ' - 'zero-deal months skewing the average (fix #295). ' - 'Refresh via layout_velocity_refresh.py (concurrently=True — MV is pre-populated).'; - -COMMIT;