-- 153_objective_lots_district_price_idx.sql -- Issue #70 (B4-3 Performance аудит) — covering-index для analyze district-price block. -- -- ПРОБЛЕМА (EXPLAIN ANALYZE на prod, district='Академический', 2026-06-13): -- app/api/v1/parcels.py:2123 (analyze hot-path, "B5-5 District price ranges") -- SELECT MIN/MAX/PERCENTILE_CONT(price_per_m2_rub), COUNT(*) -- FROM objective_lots -- WHERE district = :dn AND price_per_m2_rub IS NOT NULL -- AND price_per_m2_rub BETWEEN 30000 AND 600000 -- На плотном районе матчит ~182k строк (objective_lots ~1.12M строк всего). -- Существующий objective_lots_district_class_idx (district, class) даёт только -- district-match по индексу, НО price_per_m2_rub лежит в heap → Bitmap Heap Scan -- читает ~41k heap-блоков (~320 МБ) чтобы достать цену каждой строки. -- Замеры: cold-cache 5286 ms, warm-cache 499 ms — оба пробивают analyze -- p95-таргет ≤3 с (issue #70 targets) на холодном кэше / плотных районах. -- -- РЕШЕНИЕ: частичный covering-index (district, price_per_m2_rub). Обе колонки — -- и фильтр, и агрегируемое значение — в индексе → планировщик может сделать -- Index Only Scan (читает ~283 листовых страницы индекса вместо 41k heap-блоков), -- heap не трогается (видимость закрывает visibility map после VACUUM). -- WHERE price_per_m2_rub IS NOT NULL — частичный: индексируем только строки с -- ценой (~919k из 1.12M), отсекая ~204k NULL-цен; зеркалит предикат запроса -- (IS NOT NULL) и сужает индекс. Конкретный район в запросе делает Index Cond -- по первой колонке, BETWEEN — диапазон по второй. -- -- Dependencies: objective_lots (существует; обновляется objective_etl). PG 16.4. -- Idempotent: CREATE INDEX IF NOT EXISTS. Не-CONCURRENTLY намеренно — deploy.yml -- гоняет файл в транзакции (BEGIN/COMMIT), а CREATE INDEX CONCURRENTLY в tx -- запрещён. Index ~45 MB, build несколько секунд; обычный CREATE INDEX берёт -- SHARE-lock (блокирует только writes/objective_etl, НЕ analyze-читателей) — -- приемлемо в deploy-окне (как индексы 87/97). Не запускать одновременно с ETL. -- Applied: automatically via deploy.yml in NN order (трекер _schema_migrations). BEGIN; CREATE INDEX IF NOT EXISTS idx_objective_lots_district_price ON objective_lots (district, price_per_m2_rub) WHERE price_per_m2_rub IS NOT NULL; COMMENT ON INDEX idx_objective_lots_district_price IS 'Covering-index для analyze district-price block (#70): district-фильтр + ' 'агрегаты по price_per_m2_rub через Index Only Scan, без Bitmap Heap Scan ' '~41k блоков. См. parcels.py:2123, docs/Performance_Audit.md.'; COMMIT;