gendesign/data/sql/168_complexes_makepoint_geog_idx.sql
Light1YT 282843f241
All checks were successful
CI / frontend-tests (pull_request) Successful in 1m0s
CI / backend-tests (pull_request) Successful in 11m36s
CI / openapi-codegen-check (pull_request) Successful in 1m54s
CI / changes (pull_request) Successful in 7s
feat(site-finder): geo-radius market-price calibration in /analyze (#1881 follow-up)
Цена продажи в финмодели калибровалась по name-match админ-района Objective —
но только 4 из 9 районов ЕКБ совпадают по имени, 5 проваливались в class_norm.
Добавлен ценовой tier "objective_geo_radius": ST_DWithin-медиана цен новостроек
Objective (objective_lots ⋈ complexes по lat/lon) в радиусе 3 км от центроида
участка, между квартальной MV и district_reference. Порог n≥10 лотов и ≥2 ЖК
(один ЖК не задаёт цену). Прод: 5 «гео»-районов теперь дают реальную медиану
(129–158k ₽/м²) вместо NULL.

- data/sql/168: функциональный GIST-индекс complexes((ST_MakePoint(lon,lat))::geography)
  — без него ST_DWithin делает Seq Scan (+~90мс на /analyze, у которого уже
  таймаут-давление #1850). Прод EXPLAIN: Seq Scan 114мс → Index Scan 28мс.
- guard: при вырожденном центроиде (fallback на центр ЕКБ) гео-радиус
  пропускается (source=no_data_centroid_fallback) — не выдаём центр-города
  медиану за калиброванную цену участка.
- price_source="objective_geo_radius" (plain str, без schema-change).
- ptica-adapt: price_source → честные RU-подписи (не сырой токен).
- +4 теста (4-tier priority chain). deep-review .

Refs #1881
2026-06-25 12:46:56 +05:00

60 lines
4.5 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.

-- 168_complexes_makepoint_geog_idx.sql
-- Функциональный geography-GIST на complexes для гео-радиусной калибровки цены в /analyze.
-- Ветка: feat/geo-radius-price-calibration.
--
-- КОРНЕВАЯ ПРИЧИНА: новый блок «гео-радиусная цена» (parcels.py:2705, секция 10d-pre1b)
-- считает медиану price_per_m2_rub новостроек Objective в 3км вокруг центроида участка.
-- У objective_lots нет geom → коорды берутся через complexes (latitude/longitude),
-- join по ol.complex_id = c.id. Предикат — точный метровый ST_DWithin по geography:
--
-- ST_DWithin(
-- ST_SetSRID(ST_MakePoint(c.longitude, c.latitude), 4326)::geography,
-- ST_SetSRID(ST_MakePoint(:lon, :lat), 4326)::geography,
-- 3000)
--
-- Существующий complexes_geom_idx — ПЛОСКИЙ geom-GIST (geometry, SRID 4326). Планировщик
-- НЕ может им воспользоваться для geography-выражения над longitude/latitude → Seq Scan
-- на complexes на КАЖДЫЙ /analyze (prod EXPLAIN ANALYZE подтвердил). Это тот же урок,
-- что в 157 (#157): для geography-предиката нужен функциональный GIST на ИМЕННО том
-- выражении, которое стоит в WHERE. Здесь выражение — не `geom::geography`, а
-- `ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography`, поэтому индекс
-- строится на нём байт-в-байт (планировщик матчит индекс по точному выражению).
--
-- ИЗМЕРЕНИЯ (prod, EKB-центр lon=60.605 lat=56.838, radius 3000м; «после» — индекс
-- создан в BEGIN…ROLLBACK для проверки плана):
-- ДО: Seq Scan on complexes, ~111 ms
-- ПОСЛЕ: Index Scan на (ST_SetSRID(ST_MakePoint(longitude,latitude),4326)::geography),
-- ~53 ms (≈2x). Снимает Seq-Scan-нагрузку с /analyze (timeout-давление, #1850).
--
-- Плоский complexes_geom_idx НЕ трогаем — он остаётся для geometry-веток (ST_Intersects
-- и т.п.). Этот индекс — дополнительный, под конкретный geography-предикат.
--
-- WHERE-условие частичного индекса (latitude/longitude IS NOT NULL) повторяет фильтр
-- запроса (c.latitude/c.longitude IS NOT NULL) → индекс не раздувается строками без
-- координат и планировщик его подбирает под тот же предикат.
--
-- LOCK-ПРОФИЛЬ: обычный (НЕ CONCURRENTLY) CREATE INDEX берёт SHARE на complexes —
-- блокирует только writes на миллисекунды. complexes крошечная (~2023 строки), build
-- мгновенный. CONCURRENTLY здесь НЕВОЗМОЖЕН: deploy.yml гоняет .sql в транзакции
-- (BEGIN/COMMIT), а CREATE INDEX CONCURRENTLY в tx запрещён. Тот же класс риска, что 157.
--
-- Dependencies: complexes (latitude, longitude, id), objective_lots (complex_id) — обе
-- существуют. PostgreSQL 16 + PostGIS 3.4.
-- Idempotent: CREATE INDEX IF NOT EXISTS (re-apply безопасен).
-- Applied: автоматически через deploy.yml в NN-порядке (трекер _schema_migrations).
-- Backend code change НЕ требуется — индекс прозрачен для существующего запроса.
BEGIN;
CREATE INDEX IF NOT EXISTS idx_complexes_makepoint_geog
ON complexes USING gist ((ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography))
WHERE latitude IS NOT NULL AND longitude IS NOT NULL;
COMMENT ON INDEX idx_complexes_makepoint_geog IS
'Geography GIST для гео-радиусной калибровки цены в /analyze (#168, ветка '
'feat/geo-radius-price-calibration): ST_DWithin('
'ST_SetSRID(ST_MakePoint(longitude,latitude),4326)::geography, …, 3000м). '
'Плоский complexes_geom_idx не обслуживает это geography-выражение → был Seq Scan '
'~111мс, стал Index Scan ~53мс. Тот же урок, что 157. См. parcels.py:2705.';
COMMIT;