perf(estimate): trigram expr index for same-building anchor Tier A (~94x) #1946

Merged
bot-backend merged 1 commit from perf/tradein-estimate-audit into main 2026-06-27 06:18:02 +00:00

View file

@ -0,0 +1,60 @@
-- 137_listings_addr_norm_trgm.sql
-- Trigram GIN expression-индекс на lower(translate(address,'ёЁ','ее')) —
-- ускоряет same-building anchor Tier A эстиматора (горячий, на КАЖДОЙ оценке).
--
-- WHY (perf-аудит /estimate, EXPLAIN-решение об индексе):
-- `_fetch_anchor_comps` Tier A (estimator.py, anchor-путь за флагом
-- estimate_same_building_anchor_enabled=True — дефолт ON, в проде не
-- переопределён) запускается на каждой оценке с распознаваемым номером дома.
-- Предикат:
-- lower(translate(address,'ёЁ','ее')) LIKE '%<street>%' -- substring
-- lower(translate(address,'ёЁ','ее')) ~ '(^|[^0-9])<no>...'-- regex
-- Оба условия оборачивают address в функцию lower(translate(...)) →
-- non-sargable. Существующий listings_address_trgm_idx = gin(address
-- gin_trgm_ops) построен на СЫРОМ address, поэтому планировщик НЕ может его
-- применить к нормализованному выражению → Seq Scan по всем ~66.7k строкам.
--
-- Прод-EXPLAIN (2026-06-27, ЕКБ "Хохрякова 48", 18 строк-результат):
-- было — Seq Scan on listings: Rows Removed by Filter 66714,
-- read=15329 buffers → 167 мс.
-- стало — Bitmap Index Scan on listings_addr_norm_trgm_idx
-- (LIKE %s% И ~regex оба как Index Cond) → 97 buffers → 1.77 мс.
-- (~94× быстрее, ~98% меньше прочитанных буферов). Замерено dry-run'ом
-- (BEGIN; CREATE INDEX; EXPLAIN ANALYZE; ROLLBACK) на реальном проде.
-- Эквивалентность результата проверена: count/sum/min/max address с индексом и
-- без — байт-в-байт (18 строк, sum(price_per_m2)=8 536 769). Индекс — только
-- путь доступа, выборка не меняется.
--
-- WHAT:
-- GIN trgm-индекс по выражению lower(translate(address,'ёЁ','ее')). Выражение
-- ТОЧНО совпадает с тем, как Tier A пишет предикат — без правок кода. gin_trgm_ops
-- обслуживает И LIKE '%...%', И regex (~) — оба условия Tier A покрыты.
-- ё→е нормализация в индексе симметрична нормализатору адресов (#677/#679).
--
-- SAFETY:
-- - НЕ деструктивно: только добавление индекса, данные не трогаются.
-- - IF NOT EXISTS → идемпотентно при повторном прогоне (no-op).
-- - Размер ~8 МБ (меньше существующего listings_address_trgm_idx=16 МБ —
-- нормализация схлопывает часть триграмм). Сборка GIN по ~66.7k строк дёшева
-- (<1с), кратковременный ACCESS EXCLUSIVE lock на время сборки. CONCURRENTLY
-- не используем: файл обёрнут в BEGIN/COMMIT (CREATE INDEX CONCURRENTLY в
-- транзакции нельзя), на таком объёме обычная сборка дёшева. NB: раннер
-- деплоя (deploy.yml) гонит файл через psql -v ON_ERROR_STOP=on БЕЗ
-- --single-transaction, т.е. транзакцию открывает сам файл — как 134/133/104
-- и house-rule (Schema_Migrations_vs_Alembic.md #4).
-- - listings_address_trgm_idx (gin(address gin_trgm_ops)) ОСТАВЛЯЕМ — он на
-- сыром address и нужен другим путям (ILIKE по необработанному адресу).
--
-- DEPENDENCIES:
-- - pg_trgm extension (присутствует на проде — verified 2026-06-27).
-- - listings.address text -- 011_listings_alter.sql.
--
-- Deploy order: after 136_backfill_listings_house_id_fk_source_identity.sql.
BEGIN;
CREATE INDEX IF NOT EXISTS listings_addr_norm_trgm_idx
ON listings USING gin (lower(translate(address, 'ёЁ', 'ее')) gin_trgm_ops)
WHERE address IS NOT NULL;
COMMIT;