fix(sf-21): mv_layout_velocity weighted AVG (P0 data corruption) #290
1 changed files with 88 additions and 0 deletions
88
data/sql/100_fix_mv_layout_velocity_weighted_avg.sql
Normal file
88
data/sql/100_fix_mv_layout_velocity_weighted_avg.sql
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
-- 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;
|
||||
Loading…
Add table
Reference in a new issue