From 800e68af58420813bb8cbdd833f73280e6c2b4a7 Mon Sep 17 00:00:00 2001 From: Light1YT Date: Sun, 28 Jun 2026 19:13:42 +0500 Subject: [PATCH] perf(objective_lots): drop 3 dead indexes to cut UPSERT write-amp (#1953) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../177_drop_dead_objective_lots_indexes.sql | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 data/sql/177_drop_dead_objective_lots_indexes.sql diff --git a/data/sql/177_drop_dead_objective_lots_indexes.sql b/data/sql/177_drop_dead_objective_lots_indexes.sql new file mode 100644 index 00000000..d5c7796a --- /dev/null +++ b/data/sql/177_drop_dead_objective_lots_indexes.sql @@ -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 >/