gendesign/tradein-mvp/backend/data/sql/143_building_sale_share_schema.sql
bot-backend fa3ae581ee
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
feat(tradein): схема «доли квартир дома в продаже» — gar_house_flats + houses.gar_* + v_building_sale_share (143)
2026-06-28 16:45:27 +03:00

167 lines
9.4 KiB
PL/PgSQL
Raw 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.

-- 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 139142
-- уже заняты (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;