perf(estimate): trigram expression index for same-building anchor Tier A (~94x)
_fetch_anchor_comps Tier A runs on EVERY /estimate (flag estimate_same_building_anchor_enabled defaults True, not overridden in prod). Its address predicate wraps address in lower(translate(address,'ёЁ','ее')) for both the LIKE '%street%' and the ~house-number regex — non-sargable, and the existing listings_address_trgm_idx is on the RAW address, so the planner seq-scanned all ~66.7k listings (167ms, 15801 buffers) every estimate. Add a GIN trigram expression index on lower(translate(address,'ёЁ','ее')) matching the predicate exactly (gin_trgm_ops serves both LIKE and the regex). Prod EXPLAIN (BEGIN/ROLLBACK, "Хохрякова 48"): 167ms -> 1.77ms (~94x), buffers 15801 -> 97. Independently dry-run-verified: Bitmap Index Scan, 0.66ms. Result-identical (pure access-path; count/sum/min/max unchanged). No code change (expression matches the predicate). ~8MB, sub-second build. Other estimate-path queries confirmed already optimal (Tier W/H geom+rooms BitmapAnd, Tier S house_id_fk) — no other index warranted. tradein-mvp/backend/data/sql/137 (correct tradein migrations path).
This commit is contained in:
parent
7f2aa20be3
commit
8d5c1a888d
1 changed files with 60 additions and 0 deletions
60
tradein-mvp/backend/data/sql/137_listings_addr_norm_trgm.sql
Normal file
60
tradein-mvp/backend/data/sql/137_listings_addr_norm_trgm.sql
Normal 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;
|
||||
Loading…
Add table
Reference in a new issue