perf(objective_lots): drop 3 dead indexes (status/rooms_area/bank) — write-amp (#1953) #2056
1 changed files with 65 additions and 0 deletions
65
data/sql/177_drop_dead_objective_lots_indexes.sql
Normal file
65
data/sql/177_drop_dead_objective_lots_indexes.sql
Normal 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;
|
||||
Loading…
Add table
Reference in a new issue