diff --git a/tradein-mvp/backend/data/sql/137_listings_addr_norm_trgm.sql b/tradein-mvp/backend/data/sql/137_listings_addr_norm_trgm.sql new file mode 100644 index 00000000..82700819 --- /dev/null +++ b/tradein-mvp/backend/data/sql/137_listings_addr_norm_trgm.sql @@ -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 '%%' -- substring +-- lower(translate(address,'ёЁ','ее')) ~ '(^|[^0-9])...'-- 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;