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