Merge pull request 'perf(analyze): partial index on objective_corpus_room_month (~3.8x on hot velocity baseline)' (#1942) from perf/171-objective-crm-report-month-idx into main
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 30s
Deploy / build-worker (push) Successful in 31s
Deploy / deploy (push) Successful in 1m2s

This commit is contained in:
bot-backend 2026-06-27 05:02:41 +00:00
commit 47fe8e2469

View file

@ -0,0 +1,45 @@
-- 171_objective_crm_report_month_idx.sql
-- Partial index ускоряющий EKB-wide velocity-baseline на КАЖДОМ /analyze.
--
-- WHY (hot-path perf, найдено через pg_stat_user_tables + EXPLAIN ANALYZE на проде):
-- `_get_ekb_median()` (app/services/site_finder/velocity.py:706) вызывается
-- безусловно из compute_velocity() → внутри POST /parcels/{cad}/analyze
-- (parcels.py:3157) — т.е. РАЗ НА КАЖДЫЙ analyze (самый горячий эндпоинт).
-- Он агрегирует EKB-wide медиану sqm/мес по всем проектам, без district/project
-- фильтра; единственные предикаты — `report_month >= CURRENT_DATE - 6mo`
-- AND `deals_total_count > 0`.
--
-- До индекса: Seq Scan по всей objective_corpus_room_month (81893 строк, ~95 MB,
-- ~12159 блоков) на каждый analyze. EXPLAIN (ANALYZE,BUFFERS) на проде:
-- Seq Scan ... Rows Removed by Filter: 74589 (keeps 7304); Execution Time: 144 ms.
-- Существующие 5 индексов с report_month НЕ помогают: ни один не partial по
-- deals_total_count, а голый `report_month >=` диапазон = 27% строк (не
-- селективно). С enable_seqscan=off планнер падает на full index scan (6100
-- буферов / 68 ms) — хуже seq scan, поэтому по умолчанию выбирает Seq Scan.
--
-- WHAT:
-- Partial b-tree на (report_month) WHERE deals_total_count > 0. Реальная
-- селективность запроса 8.9% (7304/81893) → bitmap index scan.
-- После (проверено BEGIN…ROLLBACK на проде):
-- Bitmap Heap Scan ... rows=7304; Execution Time: 37.6 ms; buffers ~3136.
-- 144 ms → 38 ms (~3.8x), buffers 12281 → 3136 (74%). Планнер использует
-- индекс ЕСТЕСТВЕННО (без enable_seqscan-хаков).
--
-- COST: индекс ~280 kB (37204 строк matching partial-предикат). Запись в таблицу
-- идёт ТОЛЬКО еженедельным ETL (objective_etl.py:288), не на request-path →
-- write-overhead пренебрежим. Build sub-second (плейн CREATE INDEX, SHARE-lock
-- блокирует лишь weekly-ETL writer, не analyze-читателей).
--
-- IDEMPOTENCY: CREATE INDEX IF NOT EXISTS → повторный прогон no-op.
-- NB: плейн CREATE INDEX (НЕ CONCURRENTLY) — CONCURRENTLY нельзя внутри BEGIN/COMMIT,
-- а deploy-runner оборачивает миграцию в транзакцию; для 280 kB это безопасно.
--
-- Deploy order: после 170_osm_utility_infrastructure_ekb.sql.
BEGIN;
CREATE INDEX IF NOT EXISTS idx_objective_crm_report_month_sold
ON objective_corpus_room_month (report_month)
WHERE deals_total_count > 0;
COMMIT;