feat(db): GiST по (geom::geography) на houses — матчинг идёт по индексу (#2997)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m15s

Почему: весь матчинг домов — ST_DWithin(geom::geography, …), а единственный
пространственный индекс houses_geom_idx построен по geometry. Планировщик
берёт его лишь как bitmap по geom IS NOT NULL и фильтрует все ~9.4k строк:
70.7 мс на запрос (EXPLAIN ANALYZE на проде 21.08), 66+5 вызовов за 13 ч по
pg_stat_statements. При росте справочника ×8.5 это главное узкое место
матчинга (#2997).

Что: миграция 270 — CREATE INDEX CONCURRENTLY houses_geog_gist_idx
USING gist ((geom::geography)); без BEGIN/COMMIT по образцу 225 (CIC не
живёт в транзакции; раннер деплоя — psql autocommit по операторам).
houses_geom_idx и дубль на listings не трогаю: в pg_stat_statements есть
живой потребитель listings.geom как geometry (сшивка с cad_buildings_local)
— дроп требует отдельного разбора.

Проверка: EXPLAIN ANALYZE до/после на проде — в комментарии к #2997.

Refs #2997
This commit is contained in:
bot-backend 2026-08-21 15:17:50 +05:00
parent bb74a0e5e1
commit 2f7b22f659

View file

@ -0,0 +1,27 @@
-- 270_houses_geog_gist_idx.sql
-- #2997: у houses нет индекса под geography. Весь матчинг ходит через
-- ST_DWithin(geom::geography, …) (app/services/matching/houses.py:300, :352, :554),
-- а живой houses_geom_idx построен по geometry. Планировщик берёт его только как
-- bitmap по `geom IS NOT NULL` и дальше фильтрует все ~9.4k строк с координатами.
-- Замер на проде 21.08.2026 (EXPLAIN ANALYZE, точка в центре ЕКБ, 150 м):
-- Bitmap Index Scan on houses_geom_idx, Index Cond: (geom IS NOT NULL)
-- Rows Removed by Filter: 4718 на воркер, Execution Time: 70.7 ms
-- pg_stat_statements за 13 ч: 66 вызовов × 39.9 мс и 5 × 53.3 мс на этот класс запросов.
--
-- Функциональный GiST по (geom::geography) совпадает с выражением в запросах
-- (h.geom::geography ≡ (geom)::geography), и ST_DWithin на geography начинает
-- идти по индексу. houses_geom_idx НЕ трогаем: его дроп — отдельное решение по
-- эпику #2989 (см. #2997, там же — почему дубль на listings тоже пока не снят).
--
-- CONCURRENTLY и без BEGIN/COMMIT — по образцу 225: раннер деплоя исполняет файл
-- через psql autocommit по операторам; CIC нельзя внутри транзакции, а
-- lock_timeout ему вреден (гейт scripts/check-migration-lock-timeout.py
-- CONCURRENTLY-формы не требует и не терпит). Таблица маленькая (10 111 строк),
-- но её читает матчинг каждого объявления — ACCESS EXCLUSIVE даже на секунду не
-- нужен. Если CIC оборвётся, останется INVALID-индекс — его ловит шаг проверки
-- невалидных индексов в deploy-tradein.yml, и деплой краснеет, а не молчит.
CREATE INDEX CONCURRENTLY IF NOT EXISTS houses_geog_gist_idx
ON houses USING gist ((geom::geography));
COMMENT ON INDEX houses_geog_gist_idx IS
'#2997: GiST по (geom::geography) под ST_DWithin(geom::geography, …) матчинга (matching/houses.py). До него планировщик шёл через houses_geom_idx как bitmap по geom IS NOT NULL и фильтровал все строки (~70 мс на запрос, прод 21.08.2026).';