gendesign/data/sql/157_analyze_hotpath_indexes.sql
bot-backend 5bf30570a5
Some checks failed
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
perf(sql): geography GIST + covering index для analyze-пути Site Finder (157) (#1334)
2026-06-14 16:45:22 +00:00

122 lines
9.5 KiB
PL/PgSQL
Raw Permalink 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.

-- 157_analyze_hotpath_indexes.sql
-- Perf/index аудит горячего analyze-пути Site Finder (POST /parcels/{cad}/analyze).
-- Прецедент: migration 153 (#70) — partial covering index objective_lots(district,
-- price_per_m2_rub) → cold 5.3s→87ms Index Only Scan. Этот файл — аналогичные wins,
-- найденные EXPLAIN (ANALYZE, BUFFERS) на проде (2026-06-14, district='Академический',
-- centroid POINT(60.4869 56.7992), objective_lots ~1.12M / 2.1 ГБ, cad_buildings 38k / 88 МБ).
--
-- КОРНЕВАЯ ПРИЧИНА (3 из 4 индексов): запросы фильтруют по `X.geom::geography` для
-- точного метрового ST_DWithin (radius в метрах). Существующие GIST построены на ПЛОСКОМ
-- `geom` (geometry, SRID 4326) → планировщик НЕ может ими воспользоваться для
-- geography-предиката → Seq Scan всей таблицы на КАЖДЫЙ analyze. Функциональный GIST на
-- выражении `(geom::geography)` закрывает именно этот предикат (Index Cond через `&&`
-- + _st_expand). Плоские geom-GIST НЕ трогаем — они обслуживают ST_Intersects(geom,…)
-- (geometry) ветки (например overlap_rows в _neighbors_summary) и остаются нужны.
--
-- ИЗМЕРЕНИЯ (EXPLAIN ANALYZE до/после; «после» — warm 2-й прогон с индексом в BEGIN…ROLLBACK):
--
-- 1) osm_poi_ekb (POI в 1км, parcels.py:1488 «сек.3», 1× на analyze):
-- ДО: Seq Scan, Rows Removed by Filter 4824, 538 buf, ~36 ms
-- ПОСЛЕ: Index Scan на (geom::geography), 12 buf, ~0.2 ms (≈190x)
-- Индекс ~384 кБ, build 28 мс.
--
-- 2) osm_noise_sources_ekb (шум/гидрология/инж.сети, parcels.py:1773/1840/1889, 4× на analyze):
-- ДО: Parallel Seq Scan, Rows Removed 5122, 1701 buf, ~92 ms КАЖДЫЙ
-- ПОСЛЕ: Index Scan на (geom::geography), 176 buf, ~3.8 ms (≈24x)
-- Индекс ~816 кБ, build 55 мс. Кумулятивно ~4×88 ≈ 350 мс с одного analyze.
-- NB: geotech-запрос (parcels.py:779, source_type='industrial') уже идёт по
-- osm_noise_sources_ekb_source_idx (селективный source_type) — этот индекс его не
-- ухудшает (планировщик выберет дешевле из двух).
--
-- 3) cad_buildings (_neighbors_summary соседи в 100м, parcels.py:612, 1× на analyze):
-- ДО: Parallel Seq Scan, Rows Removed 12670, 11235 buf, ~314 ms
-- ПОСЛЕ: Index Scan на (geom::geography), 18 buf, ~0.3 ms (≈1000x)
-- Индекс ~2.9 МБ, build 338 мс. САМЫЙ крупный единичный выигрыш analyze-пути.
-- overlap_rows CTE (ST_Intersects(b.geom, wkt), geometry) по-прежнему обслуживается
-- существующим cad_buildings_geom_gist — без регрессии.
--
-- 4) objective_lots covering (obj_pricing CTE в competitor-блоке, parcels.py:1614, 1× на analyze):
-- Запрос агрегирует AVG(price)/AVG(area)/COUNT(is_sold) по ol.project_name =
-- cm.objective_complex_name (objective_complex_mapping, 286 строк).
-- ДО: Parallel Seq Scan ВСЕХ objective_lots, 215761 buf (~1.7 ГБ), Hash Join,
-- warm ~493 ms (cold ~681 ms).
-- ПОСЛЕ: Nested Loop + Index Only Scan на (project_name) INCLUDE(price,area,is_sold),
-- 286 проб по имени, 16586 buf, warm ~355 ms (≈1.4x, 13x buffers).
-- Индекс ~74 МБ, build 1.75 c. Существующий objective_lots_project_idx
-- (project_name, snapshot_date DESC) НЕ покрывает payload-колонки → не давал
-- Index Only Scan; этот — covering, поэтому не дубль.
-- ОГРАНИЧЕНИЕ: запрос всё равно агрегирует ВСЕ лоты ВСЕХ маппингов (не только
-- ~245 конкурентов в 3км). Полноценный фикс — переписать obj_pricing с
-- пушдауном domrf_obj_id IN (nearby) — это backend-engineer (правка SQL в
-- parcels.py), вне scope index-аудита. Индекс — выигрыш «здесь и сейчас» без
-- правки кода. Heap Fetches ~64k остаются (visibility map тёплый после VACUUM;
-- objective_lots dead-tup 1.25% — well-vacuumed).
--
-- ЧТО НЕ ДОБАВЛЕНО (доказанно не нужно):
-- • rosreestr_deals (market_trend, parcels.py:2211): warm ~254 ms, уже partition-pruned
-- (5 subplans removed) + per-partition BitmapAnd по 3 индексам. Композитный
-- (region_code,doc_type,realestate_type_code,period_start_date) пришлось бы строить на
-- ~20+ партициях, каждая ACCESS EXCLUSIVE на ПИШУЩЕЙСЯ таблице → высокий lock-риск,
-- несоразмерный ~2x-выигрышу. Партиционированную таблицу под индекс-аудит не трогаем.
-- • pipeline-блок (parcels.py:1663): warm ~37 мс, корр. подзапрос уже идёт по
-- objective_lots_project_idx (Bitmap Index Scan). Индекс не нужен.
-- • ekburg_construction_permits (LEFT(cadastral_number,…) prefix): уже покрыт
-- idx_ekburg_permits_cad_cpat (text_pattern_ops).
-- • geom-resolution UNION (parcels.py:1334): 15 мс, всё по PK index scan.
-- • district_price_block (parcels.py:2123): уже покрыт idx_objective_lots_district_price (153).
--
-- LOCK-ПРОФИЛЬ: обычный (НЕ CONCURRENTLY) CREATE INDEX берёт SHARE на таблицу — блокирует
-- ТОЛЬКО writes (loaders/objective_etl/NSPD on-demand fetch), читателей analyze НЕ
-- блокирует. CONCURRENTLY здесь невозможен: deploy.yml гоняет файл в транзакции
-- (BEGIN/COMMIT), а CREATE INDEX CONCURRENTLY в tx запрещён. Суммарный build ~2.2 c
-- (74 МБ objective_lots — доминирует, как и в 153 на той же таблице). Приемлемо в
-- deploy-окне (тот же класс риска, что 153/87/97). Не запускать одновременно с тяжёлым ETL.
--
-- Dependencies: osm_poi_ekb, osm_noise_sources_ekb, cad_buildings, objective_lots,
-- objective_complex_mapping (все существуют). PostgreSQL 16.4 + PostGIS 3.4.
-- Idempotent: CREATE INDEX IF NOT EXISTS (re-apply безопасен).
-- Applied: автоматически через deploy.yml в NN-порядке (трекер _schema_migrations).
-- Backend code change НЕ требуется — индексы прозрачны для существующих запросов.
BEGIN;
-- 1) POI в 1км (analyze сек.3) — geography-предикат ST_DWithin(geom::geography,…,1000).
CREATE INDEX IF NOT EXISTS idx_osm_poi_ekb_geom_geog
ON osm_poi_ekb USING gist ((geom::geography));
COMMENT ON INDEX idx_osm_poi_ekb_geom_geog IS
'Geography GIST для analyze POI-блока (#157): ST_DWithin(geom::geography,…,1000м). '
'Плоский osm_poi_ekb_geom_gist не обслуживает geography-cast → был Seq Scan ~36мс, '
'стал Index Scan ~0.2мс. См. parcels.py:1488.';
-- 2) Шумовые источники в 2км (analyze сек.7/9c/9d) — geography ST_DWithin, 4× на analyze.
CREATE INDEX IF NOT EXISTS idx_osm_noise_sources_ekb_geom_geog
ON osm_noise_sources_ekb USING gist ((geom::geography));
COMMENT ON INDEX idx_osm_noise_sources_ekb_geom_geog IS
'Geography GIST для analyze noise/hydro/utility-блоков (#157): '
'ST_DWithin(geom::geography,…,2000м), 4 запроса на analyze. Был Parallel Seq Scan '
'~92мс каждый, стал Index Scan ~3.8мс. См. parcels.py:1773/1840/1889.';
-- 3) Соседние здания в 100м (_neighbors_summary) — geography ST_DWithin на 88МБ-таблице.
CREATE INDEX IF NOT EXISTS idx_cad_buildings_geom_geog
ON cad_buildings USING gist ((geom::geography));
COMMENT ON INDEX idx_cad_buildings_geom_geog IS
'Geography GIST для analyze _neighbors_summary (#157): ST_DWithin(geom::geography,…,100м). '
'Плоский cad_buildings_geom_gist обслуживает ST_Intersects(geom,…) (overlap_rows), но не '
'geography-cast → был Parallel Seq Scan ~314мс, стал Index Scan ~0.3мс. См. parcels.py:612.';
-- 4) Covering-index для obj_pricing competitor-агрегации (analyze сек.5).
CREATE INDEX IF NOT EXISTS idx_objective_lots_pname_pricing
ON objective_lots (project_name)
INCLUDE (price_per_m2_rub, area_pd, is_sold);
COMMENT ON INDEX idx_objective_lots_pname_pricing IS
'Covering-index для analyze competitor obj_pricing (#157): агрегаты по '
'project_name = objective_complex_name через Index Only Scan (286 проб), без '
'Parallel Seq Scan ~1.7ГБ. Существующий objective_lots_project_idx без payload не '
'давал Index Only Scan. Long-term: пушдаун domrf_obj_id IN(nearby) в SQL (backend). '
'См. parcels.py:1614.';
COMMIT;