perf(objective_lots): drop 3 dead indexes (status/rooms_area/bank) — write-amp (#1953) (#2056)
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 33s
Deploy / build-worker (push) Successful in 34s
Deploy / deploy (push) Successful in 8m34s

This commit is contained in:
bot-backend 2026-06-28 14:39:05 +00:00
parent fb25180929
commit c97dd57e1c

View file

@ -0,0 +1,65 @@
-- Контекст: perf-hunt finding #4 (follow-up эпика #1953) — срезаем
-- write-amplification на горячей UPSERT-таблице objective_lots.
--
-- objective_lots — append/UPSERT-per-snapshot, очень write-heavy: за 35 дней
-- аптайма (pg_stat без reset, prod 2026-06-28) n_tup_ins=1.76M, n_tup_upd=1.39M.
-- Каждый INSERT/UPDATE обязан поддерживать ВСЕ индексы таблицы → мёртвый индекс =
-- чистый write-amp + место на диске + WAL, ноль пользы для чтения.
--
-- Дропаем 3 индекса, у которых pg_stat_user_indexes.idx_scan почти нулевой за весь
-- аптайм И в кодовой базе (backend services/api/workers + data/sql views) НЕТ ни
-- одного sargable-запроса, который мог бы их использовать. По каждому проверено
-- EXPLAIN на prod: планировщик берёт Seq Scan на реальной форме запроса даже при
-- наличии индекса (доказывает, что индекс не служит ничему).
--
-- ── DROP 1: objective_lots_status_idx ── (idx_scan=8 за 35д, 19 MB)
-- Опр.: ON objective_lots(status) WHERE status IS NOT NULL (68_schema_objective.sql:250)
-- Единственные потребители status: market_metrics._STOCK_SQL,
-- supply_layers, competitors, special_indices — ВСЕ через
-- `LOWER(COALESCE(ol.status,'')) = 'продан'`. Обёртка LOWER(COALESCE(...))
-- НЕ sargable против plain B-tree на status → индекс не используется НИКОГДА.
-- Bare `status = :x` / `status IN (...)` в коде НЕ встречается (grep пуст).
-- Колонка status уже в payload covering-индекса (176_*, INCLUDE) — для проекции
-- отдельный индекс тоже не нужен. Триггер истории (166_*) читает NEW.status /
-- prev.status построчно (без индекса).
-- EXPLAIN prod: `WHERE LOWER(COALESCE(status,''))='продан'` → Parallel Seq Scan.
--
-- ── DROP 2: objective_lots_rooms_area_idx ── (idx_scan=7 за 35д, 78 MB)
-- Опр.: ON objective_lots(rooms_int, area_pd) WHERE area_pd IS NOT NULL (:252)
-- rooms_int / area_pd встречаются только в SELECT-проекциях и внутри CASE-bucket
-- выражений (market_metrics, sales_series, analytics) — НЕ как WHERE/ORDER/JOIN
-- предикат. Room-bucket фильтр оборачивает обе колонки в CASE → не sargable.
-- Никакого `WHERE rooms_int = / IN` или `area_pd >/</BETWEEN`-фильтра в коде нет
-- (grep пуст). Драйвер этих запросов — district/premise_kind/class
-- (objective_lots_district_class_idx, idx_scan=49746 — жив, НЕ трогаем).
-- EXPLAIN prod: CASE-bucket-форма → Parallel Seq Scan; индекс не выбран.
--
-- ── DROP 3: objective_lots_bank_idx ── (idx_scan=2 за 35д, 8 KB)
-- Опр.: ON objective_lots(bank_name) WHERE bank_name IS NOT NULL (:256)
-- Единственный потребитель objective_lots.bank_name — special_indices
-- ._ARTIFICIAL_DEMAND_SQL: `(ol.encumbrance_type IS NOT NULL OR
-- ol.bank_name IS NOT NULL) AS has_mortgage` — это проекция в SELECT, не фильтр;
-- WHERE там по premise_kind/is_sold/contract_date/district/class. Запросы с
-- `WHERE bank_name = ...` есть только к ДРУГОЙ таблице (domrf_rates,
-- 40_relations.sql:270/273), не к objective_lots.
-- EXPLAIN prod: реальная форма → Parallel Seq Scan; индекс не выбран.
--
-- НЕ ТРОГАЕМ: objective_lots_project_name_trgm_idx (236 MB, idx_scan=19) — у него
-- ETL-потребитель (objective_backfill.py, similarity()); дроп требует отдельной
-- проверки ETL-каденса → отдельный тикет, вне scope этой миграции.
--
-- Откат: пересоздать из data/sql/68_schema_objective.sql:250-256
-- (CREATE INDEX IF NOT EXISTS ...). Дроп индекса обратим, данные не теряются.
--
-- Порядок применения для prod: миграция самодостаточна (ничего в app-коде не
-- зависит от этих индексов — потому и дропаем), порядок deploy не критичен.
-- Idempotent (DROP INDEX IF EXISTS). Self-wrapped BEGIN/COMMIT.
-- Экономия: ~97 MB диска + соответствующий WAL/maintenance write-amp на каждый
-- из ~3.15M lifetime INSERT/UPDATE.
BEGIN;
DROP INDEX IF EXISTS objective_lots_status_idx;
DROP INDEX IF EXISTS objective_lots_rooms_area_idx;
DROP INDEX IF EXISTS objective_lots_bank_idx;
COMMIT;