feat(tradein): схема «доли квартир дома в продаже» — gar_house_flats + houses.gar_* + v_building_sale_share (143)
This commit is contained in:
parent
a100f03aa7
commit
fa3ae581ee
1 changed files with 167 additions and 0 deletions
167
tradein-mvp/backend/data/sql/143_building_sale_share_schema.sql
Normal file
167
tradein-mvp/backend/data/sql/143_building_sale_share_schema.sql
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
-- 143_building_sale_share_schema.sql
|
||||
--
|
||||
-- CONTEXT: схема-фундамент новой страницы «доля квартир дома в продаже» —
|
||||
-- список домов ВТОРИЧНОГО рынка, где
|
||||
-- active_vtorichka_listings / total_apartments_in_building >= threshold%.
|
||||
-- Числитель (активные вторичные объявления на дом) у нас уже есть в `listings`
|
||||
-- (house_id_fk + is_active + listing_segment='vtorichka'). Знаменатель —
|
||||
-- «сколько всего квартир в доме» — на проде заполнен слабо (total_units/flat_count
|
||||
-- неполные/шумные), поэтому отдельно паримся над ГАР/ФИАС-дампом по региону 66 и
|
||||
-- матчим к houses по НОРМАЛИЗОВАННОМУ адресу (tradein_normalize_short_addr).
|
||||
--
|
||||
-- Этот файл — ТОЛЬКО схема (таблица-приёмник ГАР + provenance-колонки в houses +
|
||||
-- rollup-view). Сам парс/загрузка ГАР и backfill houses.gar_* — отдельная задача;
|
||||
-- до неё view опирается на fallback-знаменатель (total_units / flat_count).
|
||||
--
|
||||
-- WHAT:
|
||||
-- 1. Таблица gar_house_flats — «квартир на дом» из ГАР (region-66, ключ — адрес/GUID).
|
||||
-- 2. houses.gar_* — provenance матча ГАР→houses (guid, flat_count, matched_at, method).
|
||||
-- 3. v_building_sale_share — per-building rollup вторички с эффективным знаменателем
|
||||
-- COALESCE(gar_flat_count, total_units, flat_count) и sale_share_pct.
|
||||
--
|
||||
-- DEPENDENCIES (existing prod objects):
|
||||
-- - houses (id, total_units, flat_count, geom Point/4326, short_address,
|
||||
-- full_address, address, lat, lon, year_built, house_type, total_floors,
|
||||
-- series_name, is_emergency) — core tables.
|
||||
-- - listings (house_id_fk → houses.id, is_active, listing_segment, price_rub,
|
||||
-- price_per_m2, days_on_market) — core tables.
|
||||
-- - tradein_normalize_short_addr(text) — нормализатор адреса (используется
|
||||
-- loader'ом ГАР для заполнения norm_address; в этом файле прямо не вызывается).
|
||||
-- - pg_trgm extension — создаётся здесь IF NOT EXISTS (нужен для trgm-GIN на
|
||||
-- norm_address: fuzzy-матч ГАР↔houses по адресу на следующем шаге).
|
||||
--
|
||||
-- SAFETY / IDEMPOTENCY:
|
||||
-- - НЕ деструктивно: только CREATE TABLE/INDEX IF NOT EXISTS, ADD COLUMN IF NOT
|
||||
-- EXISTS, CREATE OR REPLACE VIEW. Повторный прогон — no-op.
|
||||
-- - NB: раннер деплоя (deploy-tradein.yml) гонит файл через psql -v
|
||||
-- ON_ERROR_STOP=on БЕЗ --single-transaction → транзакцию открывает САМ файл
|
||||
-- (BEGIN/COMMIT ниже), как 137/134/133.
|
||||
--
|
||||
-- NB по нумерации: исходно планировался как «139», но на forgejo/main 139–142
|
||||
-- уже заняты (139_premium_houses … 142_premium_buildings_curated) → следующий
|
||||
-- свободный sequential = 143 (проверено `ls | grep '^143'`).
|
||||
--
|
||||
-- Deploy order: после 142_premium_buildings_curated.sql.
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- pg_trgm нужен для GIN-trgm индекса на norm_address (fuzzy ГАР↔houses match).
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────
|
||||
-- 1. gar_house_flats — «квартир на дом» из ГАР (region-66), address-keyed
|
||||
-- ─────────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS gar_house_flats (
|
||||
house_guid text PRIMARY KEY, -- ГАР OBJECTGUID дома
|
||||
object_id bigint, -- ГАР OBJECTID
|
||||
region_code text NOT NULL,
|
||||
flat_count int, -- # помещений-квартир под домом
|
||||
full_address text,
|
||||
norm_address text, -- tradein_normalize_short_addr(full_address)
|
||||
street_name text,
|
||||
house_num text,
|
||||
loaded_at timestamptz NOT NULL DEFAULT now(),
|
||||
gar_version text
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS gar_house_flats_norm_address_idx
|
||||
ON gar_house_flats (norm_address);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS gar_house_flats_norm_address_trgm_idx
|
||||
ON gar_house_flats USING gin (norm_address gin_trgm_ops);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS gar_house_flats_region_code_idx
|
||||
ON gar_house_flats (region_code);
|
||||
|
||||
COMMENT ON TABLE gar_house_flats IS
|
||||
'ГАР/ФИАС: квартир на дом (region-66), ключ house_guid (OBJECTGUID). '
|
||||
'norm_address = tradein_normalize_short_addr(full_address) для матча к houses. '
|
||||
'Заполняется отдельным loader''ом из self-parsed ГАР-дампа (мигр. 143).';
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────
|
||||
-- 2. houses — provenance-колонки матча ГАР→houses
|
||||
-- ─────────────────────────────────────────────────────────────────────
|
||||
ALTER TABLE houses ADD COLUMN IF NOT EXISTS gar_house_guid text;
|
||||
ALTER TABLE houses ADD COLUMN IF NOT EXISTS gar_flat_count int;
|
||||
ALTER TABLE houses ADD COLUMN IF NOT EXISTS gar_matched_at timestamptz;
|
||||
ALTER TABLE houses ADD COLUMN IF NOT EXISTS gar_match_method text;
|
||||
|
||||
-- partial: индексируем только реально заматченные дома (gar_flat_count populated).
|
||||
CREATE INDEX IF NOT EXISTS houses_gar_flat_count_idx
|
||||
ON houses (gar_flat_count)
|
||||
WHERE gar_flat_count IS NOT NULL;
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────
|
||||
-- 3. v_building_sale_share — per-building rollup вторички
|
||||
-- Эффективный знаменатель = COALESCE(ГАР, total_units, flat_count).
|
||||
-- Строки с NULL-знаменателем НЕ отбрасываем (sale_share_pct=NULL) —
|
||||
-- coverage-запрос должен уметь их посчитать.
|
||||
-- ─────────────────────────────────────────────────────────────────────
|
||||
CREATE OR REPLACE VIEW v_building_sale_share AS
|
||||
WITH listing_agg AS (
|
||||
-- агрегат активных вторичных объявлений на дом (keyed на house_id_fk)
|
||||
SELECT
|
||||
l.house_id_fk AS house_id,
|
||||
count(*) FILTER (
|
||||
WHERE l.is_active AND l.listing_segment = 'vtorichka'
|
||||
) AS active_secondary,
|
||||
percentile_cont(0.5) WITHIN GROUP (ORDER BY l.price_rub)
|
||||
FILTER (
|
||||
WHERE l.is_active AND l.listing_segment = 'vtorichka'
|
||||
AND l.price_rub IS NOT NULL
|
||||
) AS median_price_rub,
|
||||
percentile_cont(0.5) WITHIN GROUP (ORDER BY l.price_per_m2)
|
||||
FILTER (
|
||||
WHERE l.is_active AND l.listing_segment = 'vtorichka'
|
||||
AND l.price_per_m2 IS NOT NULL
|
||||
) AS median_price_per_m2,
|
||||
avg(l.days_on_market) FILTER (
|
||||
WHERE l.is_active AND l.listing_segment = 'vtorichka'
|
||||
AND l.days_on_market IS NOT NULL
|
||||
) AS avg_days_on_market
|
||||
FROM listings l
|
||||
WHERE l.house_id_fk IS NOT NULL
|
||||
GROUP BY l.house_id_fk
|
||||
)
|
||||
SELECT
|
||||
h.id AS house_id,
|
||||
h.short_address,
|
||||
h.full_address,
|
||||
h.address,
|
||||
h.lat,
|
||||
h.lon,
|
||||
h.year_built,
|
||||
h.house_type,
|
||||
h.total_floors,
|
||||
h.series_name,
|
||||
h.is_emergency,
|
||||
COALESCE(h.gar_flat_count, NULLIF(h.total_units, 0), NULLIF(h.flat_count, 0))
|
||||
AS flat_count_effective,
|
||||
h.gar_flat_count,
|
||||
h.gar_match_method,
|
||||
la.active_secondary,
|
||||
la.median_price_rub,
|
||||
la.median_price_per_m2,
|
||||
la.avg_days_on_market,
|
||||
CASE
|
||||
WHEN COALESCE(h.gar_flat_count, NULLIF(h.total_units, 0), NULLIF(h.flat_count, 0)) > 0
|
||||
THEN round(
|
||||
100.0 * la.active_secondary
|
||||
/ COALESCE(h.gar_flat_count, NULLIF(h.total_units, 0), NULLIF(h.flat_count, 0)),
|
||||
1
|
||||
)
|
||||
ELSE NULL
|
||||
END AS sale_share_pct
|
||||
FROM houses h
|
||||
JOIN listing_agg la ON la.house_id = h.id
|
||||
WHERE h.geom IS NOT NULL
|
||||
AND la.active_secondary > 0;
|
||||
|
||||
COMMENT ON VIEW v_building_sale_share IS
|
||||
'Per-building rollup вторички для страницы «доля квартир дома в продаже» (мигр. 143). '
|
||||
'flat_count_effective = COALESCE(gar_flat_count, NULLIF(total_units,0), NULLIF(flat_count,0)); '
|
||||
'sale_share_pct = round(100*active_secondary/flat_count_effective,1) только при denom>0 '
|
||||
'(иначе NULL — строка сохраняется для coverage-подсчёта). Фильтр: geom NOT NULL AND '
|
||||
'active_secondary>0.';
|
||||
|
||||
COMMIT;
|
||||
Loading…
Add table
Reference in a new issue