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