gendesign/data/sql/177_drop_dead_objective_lots_indexes.sql
Light1YT 800e68af58
All checks were successful
CI / changes (pull_request) Successful in 8s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m17s
CI / backend-tests (pull_request) Successful in 14m47s
perf(objective_lots): drop 3 dead indexes to cut UPSERT write-amp (#1953)
perf-hunt finding #4: objective_lots is a hot UPSERT table (1.76M ins /
1.39M upd over 35d) — each write maintains every index. These three have
near-zero lifetime idx_scan AND no sargable consumer anywhere in the
backend/SQL, so they are pure write-amplification:

  - objective_lots_status_idx   (idx_scan=8,  19 MB) — consumers use
    LOWER(COALESCE(status,''))='продан', not sargable on plain B-tree
  - objective_lots_rooms_area_idx (idx_scan=7, 78 MB) — rooms_int/area_pd
    only in SELECT projections + CASE-bucket, never a WHERE/ORDER filter
  - objective_lots_bank_idx     (idx_scan=2,  8 KB) — bank_name only in an
    IS NOT NULL projection; WHERE bank_name= is a different table

Verified on prod: EXPLAIN of each real query shape → Seq Scan (index not
chosen even when present). Frees ~97 MB + per-write WAL/maintenance.
Idempotent (DROP INDEX IF EXISTS), BEGIN/COMMIT, dry-run clean.
Rollback: recreate from data/sql/68_schema_objective.sql:250-256.

NOT touched: project_name_trgm_idx (236 MB) — has an ETL consumer
(objective_backfill.py similarity()); needs separate ETL-cadence check.
2026-06-28 19:13:42 +05:00

65 lines
5 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Контекст: 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;