56 lines
2.7 KiB
PL/PgSQL
56 lines
2.7 KiB
PL/PgSQL
-- 139_premium_houses.sql
|
|
-- Purpose: premium_houses materialized view — premium / elite EKB buildings
|
|
-- identified by listing concentration (#2002, premium-building identification).
|
|
--
|
|
-- Rationale (#2002):
|
|
-- We flag a house as "premium" by the concentration of expensive listings on it.
|
|
-- Validated query returns ~297 houses that together cover ~65% of all >=20M RUB
|
|
-- elite listings. A house qualifies when it has a meaningful sample (n >= 4 active
|
|
-- listings with sane price_per_m2) AND either:
|
|
-- - a high median price_per_m2 (>= 220000 RUB/m2), OR
|
|
-- - at least 3 listings priced >= 20M RUB (n_elite >= 3).
|
|
-- price_per_m2 is clamped to [100000, 800000] to drop scrape garbage / mis-parsed
|
|
-- areas before aggregating, so the median is robust.
|
|
--
|
|
-- house_id dupes: house_dedup is imperfect, so the same physical building can appear
|
|
-- under several house_id values. That is FINE here — we deliberately keep every
|
|
-- variant in the set; dropping dupes would only shrink coverage, never improve it.
|
|
-- The unique index is on house_id (each MV row is one house_id), not on a physical
|
|
-- building, so distinct house_id rows never collide.
|
|
--
|
|
-- Refresh: refreshed by the existing MV-refresh task (app/tasks/refresh_search_matview.py,
|
|
-- scheduled as `refresh_search_matview` in scrape_schedules) right AFTER
|
|
-- listings_search_mv — premium_houses derives from `listings`, so refreshing it once
|
|
-- the search MV is fresh keeps both in step. REFRESH ... CONCURRENTLY requires the
|
|
-- UNIQUE index below.
|
|
--
|
|
-- Dependencies: 002_core_tables.sql (listings: house_id_fk, price_rub, price_per_m2,
|
|
-- area_m2, is_active).
|
|
-- Deploy order: after 002_core_tables.sql. No backend schema coupling.
|
|
-- Re-run safe: CREATE MATERIALIZED VIEW IF NOT EXISTS + CREATE UNIQUE INDEX IF NOT EXISTS.
|
|
-- No DROP — dropping would break in-flight REFRESH ... CONCURRENTLY and momentarily
|
|
-- delete the object during deploy.
|
|
|
|
BEGIN;
|
|
|
|
CREATE MATERIALIZED VIEW IF NOT EXISTS premium_houses AS
|
|
WITH lh AS (
|
|
SELECT l.house_id_fk AS house_id,
|
|
count(*) AS n,
|
|
count(*) FILTER (WHERE l.price_rub >= 20000000) AS n_elite,
|
|
percentile_cont(0.5) WITHIN GROUP (ORDER BY l.price_per_m2)
|
|
FILTER (WHERE l.price_per_m2 BETWEEN 100000 AND 800000) AS med_ppm2
|
|
FROM listings l
|
|
WHERE l.is_active AND l.house_id_fk IS NOT NULL AND l.area_m2 > 0
|
|
AND l.price_per_m2 BETWEEN 100000 AND 800000
|
|
GROUP BY l.house_id_fk
|
|
)
|
|
SELECT house_id, n, n_elite, round(med_ppm2)::int AS med_ppm2
|
|
FROM lh
|
|
WHERE n >= 4 AND (med_ppm2 >= 220000 OR n_elite >= 3);
|
|
|
|
-- UNIQUE index is mandatory for REFRESH MATERIALIZED VIEW CONCURRENTLY.
|
|
CREATE UNIQUE INDEX IF NOT EXISTS premium_houses_house_id_uidx
|
|
ON premium_houses (house_id);
|
|
|
|
COMMIT;
|