-- 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;