From 2f7b22f659baab9198f58f9476eb5796d328f081 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Fri, 21 Aug 2026 15:17:50 +0500 Subject: [PATCH] =?UTF-8?q?feat(db):=20GiST=20=D0=BF=D0=BE=20(geom::geogra?= =?UTF-8?q?phy)=20=D0=BD=D0=B0=20houses=20=E2=80=94=20=D0=BC=D0=B0=D1=82?= =?UTF-8?q?=D1=87=D0=B8=D0=BD=D0=B3=20=D0=B8=D0=B4=D1=91=D1=82=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=B8=D0=BD=D0=B4=D0=B5=D0=BA=D1=81=D1=83=20(#2997)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Почему: весь матчинг домов — 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 --- .../data/sql/270_houses_geog_gist_idx.sql | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tradein-mvp/backend/data/sql/270_houses_geog_gist_idx.sql diff --git a/tradein-mvp/backend/data/sql/270_houses_geog_gist_idx.sql b/tradein-mvp/backend/data/sql/270_houses_geog_gist_idx.sql new file mode 100644 index 00000000..0b869139 --- /dev/null +++ b/tradein-mvp/backend/data/sql/270_houses_geog_gist_idx.sql @@ -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).'; -- 2.45.3