From fa3ae581eeec2a6441af96686fd39cb5dfd2618d Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sun, 28 Jun 2026 16:45:27 +0300 Subject: [PATCH] =?UTF-8?q?feat(tradein):=20=D1=81=D1=85=D0=B5=D0=BC=D0=B0?= =?UTF-8?q?=20=C2=AB=D0=B4=D0=BE=D0=BB=D0=B8=20=D0=BA=D0=B2=D0=B0=D1=80?= =?UTF-8?q?=D1=82=D0=B8=D1=80=20=D0=B4=D0=BE=D0=BC=D0=B0=20=D0=B2=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B4=D0=B0=D0=B6=D0=B5=C2=BB=20=E2=80=94=20gar=5F?= =?UTF-8?q?house=5Fflats=20+=20houses.gar=5F*=20+=20v=5Fbuilding=5Fsale=5F?= =?UTF-8?q?share=20(143)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sql/143_building_sale_share_schema.sql | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 tradein-mvp/backend/data/sql/143_building_sale_share_schema.sql diff --git a/tradein-mvp/backend/data/sql/143_building_sale_share_schema.sql b/tradein-mvp/backend/data/sql/143_building_sale_share_schema.sql new file mode 100644 index 00000000..139865ff --- /dev/null +++ b/tradein-mvp/backend/data/sql/143_building_sale_share_schema.sql @@ -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; -- 2.45.3