gendesign/data/sql/100_fix_mv_layout_velocity_weighted_avg.sql
lekss361 913cdcfcef fix(sf-21): mv_layout_velocity weighted AVG — P0 data corruption
AVG(deals_total_avg_area_m2) averaged pre-aggregated monthly values
including months with 0 deals (area=0, price=0), pulling results down
2-14x. Replace with SUM(x * count) / NULLIF(SUM(count), 0).

Verified on prod: obj 65307 room 4+ area 7.4->105.8 m2, price 7.9->112 k/m2.
Migration 100_ includes REFRESH — no manual step needed post-deploy.
2026-05-17 16:53:36 +03:00

88 lines
4.5 KiB
PL/PgSQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- 100_fix_mv_layout_velocity_weighted_avg.sql
-- SF Bug #21 (P0) — mv_layout_velocity использовал unweighted AVG() по
-- deals_total_avg_area_m2 и deals_total_avg_price_thousand_rub_per_m2.
-- objective_corpus_room_month хранит строки за каждый (project, room, month)
-- включая месяцы с 0 сделок (count=0, area=0, price=0). Нули тянули
-- среднее вниз в 2-14×. Примеры (verified prod):
-- obj 65307, room=3: 114 месяцев, 106 нулевых → AVG=7.4 м² vs weighted=105.8 м²
-- obj 65903, room=4+: 36 мес, 33 нулевых → AVG=9.1 м² vs weighted=109.0 м²
--
-- Fix: заменяем AVG(x) на SUM(x * count) / NULLIF(SUM(count), 0) — weighted average.
--
-- Dependencies: mv_layout_velocity не имеет зависимых VIEW/MV (проверено 2026-05-17).
-- Existing indexes: mv_layout_velocity_pk (UNIQUE obj_id, room_bucket),
-- mv_layout_velocity_obj_idx (obj_id) — пересоздаются ниже.
--
-- ВАЖНО: этот файл выполняет DROP + CREATE + REFRESH (non-concurrent, блокировка ~30 с).
-- REFRESH CONCURRENTLY после первого populate — безопасен через layout_velocity_refresh.py.
--
-- Deploy order: SQL migration → app deploy (никаких backend изменений в этом PR).
-- Idempotency: DROP IF EXISTS → безопасен при повторном запуске.
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,
-- FIX SF #21: weighted average — нули из месяцев без сделок не тянут вниз.
-- Формула: SUM(avg_x * count) / SUM(count) эквивалентна SUM(total_x) / SUM(count),
-- т.к. avg_x = total_x / count за один месяц → произведение восстанавливает total_x.
(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,
(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 = 'Екатеринбург'
GROUP BY cm.domrf_obj_id, l.room_bucket
WITH NO DATA;
-- UNIQUE index: required for REFRESH CONCURRENTLY (periodic via layout_velocity_refresh.py).
-- GROUP BY (obj_id, room_bucket) guarantees uniqueness.
CREATE UNIQUE INDEX mv_layout_velocity_pk
ON mv_layout_velocity (obj_id, room_bucket);
-- Lookup index for /best-layouts endpoint queries by obj_id.
CREATE INDEX mv_layout_velocity_obj_idx
ON mv_layout_velocity (obj_id);
-- Initial populate (non-concurrent — MV just created, CONCURRENTLY requires populated MV).
-- Блокировка ~30 с на production при ~19 738 source rows.
REFRESH MATERIALIZED VIEW mv_layout_velocity;
COMMENT ON MATERIALIZED VIEW mv_layout_velocity IS
'Per-(obj_id, room_bucket) deals aggregation за last 24 months. '
'WEIGHTED average площади и цены (SF Bug #21 fix, 2026-05-17). '
'Source: objective_corpus_room_month × objective_complex_mapping (EKB only). '
'Refresh via layout_velocity_refresh.py (CONCURRENTLY после initial populate).';
COMMIT;