-- Контекст: #1964 (эпик #1953) — physflat-дедуп current-state потребителей -- objective_lots через общий VIEW. -- -- objective_lots — current-state UPSERT (UNIQUE objective_lot_id, ON CONFLICT DO -- UPDATE), НЕ snapshot-append: всего 5 distinct snapshot_date. Инфляция строк -- (~2.91×: прод 2026-06-28 — 1 753 283 сырых квартир-строки vs 603 049 физлотов) -- возникает потому, что Объектив за время пере-листингов присваивает ОДНОМУ -- физическому лоту (project_name, corpus_name, section, floor, lot_number) -- НЕСКОЛЬКО objective_lot_id. История лотов живёт в ОТДЕЛЬНОЙ objective_lots_history -- (её НЕ трогаем — это корректный time-series). -- -- View отдаёт ТОТ ЖЕ набор колонок, что и objective_lots (ol.* — прод-проверка: -- 51 base cols == 51 view cols), по ОДНОЙ строке на физлот — последний снапшот -- (snapshot_date DESC, tie-break id DESC). objective_lot_id в результате уникален -- (603 049 строк = 603 049 distinct objective_lot_id, 0 NULL) → потребители, -- которым нужна fan-out-защита по маппингу (competitors._SOLD_COUNT_SQL), могут -- использовать COUNT(DISTINCT objective_lot_id) поверх view. -- -- ⚠ ПЛАН/ИНДЕКС (deep-review #1964, прод-EXPLAIN 2026-06-28): partial-индекс -- objective_lots_physflat_latest_idx (миграция 173, WHERE premise_kind='квартира') -- НЕ обслуживает этот view. Внутри view НЕТ premise-фильтра, а внешний -- WHERE premise_kind/district потребителя НЕ проталкивается НИЖЕ DISTINCT ON → -- индекс-173 не применим. View материализует ВСЮ таблицу: Parallel Seq Scan + -- external Sort 1.76M строк → ~5.8 s (count по одному району). -- ПОЛНЫЙ индекс (project,corpus,section,floor,lot_number,snapshot_date DESC,id DESC) -- НЕ помогает и НЕ добавлен: DISTINCT ON отдаёт ol.* (51 кол., width≈945) → -- index-only-unique невозможен; 605k heap-проб через 142 MB индекс ДОРОЖЕ -- seq-scan+sort (планировщик его игнорирует: forced index-path всё равно ~3.9 s). -- 142 MB write-amp ради нуля на request-path + замедление bulk-INSERT objective- -- scrape — не берём. Подтверждает исходное решение #1964 «новый индекс НЕ добавляем». -- -- Следствие: view применяем ТОЛЬКО к BATCH/широким консьюмерам, где полная -- материализация амортизирована или кэширована (supply_layers L1 группирует ВСЕ -- районы; competitors._SOLD_COUNT_SQL — по mapping; special_indices; admin; landing). -- REQUEST-PATH консьюмеры внутри analyze_parcel с фильтром по ОДНОМУ району -- (concepts._OBJECTIVE_MEDIAN_SQL, parcels district-price/geo-median) view НЕ -- используют — у них inline DISTINCT ON с district, протолкнутым В CTE (bitmap по -- району ~240k строк + sort ~19 MB → ~1.7 s vs 5.8 s через view). Дедуп платится в -- любом случае (старый 303 ms был быстрым потому что считал РАЗДУТЫЕ сырые строки). -- -- #1959 уже устранил инфляцию ВНУТРИ compute_market_metrics (inline DISTINCT ON в -- _STOCK_SQL/_SALES_WINDOW_SQL) и его 9 forecast/scoring-вызовов — НЕ дублируем. -- -- НЕ трогаем (time-series, корректны): sales_series._SOURCE_B_SQL, -- objective_lots_history, mv_sales_tracker_*. -- -- Порядок: миграция ПЕРВОЙ (deploy first), затем backend-код, читающий view. -- Idempotent: CREATE OR REPLACE VIEW. Self-wrapped BEGIN/COMMIT. -- Прод DRY-RUN (BEGIN; CREATE OR REPLACE VIEW; SELECT count; ROLLBACK): -- view kvartira = 603 049 (vs 1 753 283 raw), колонки 51==51 ✓. BEGIN; CREATE OR REPLACE VIEW v_objective_lots_latest AS SELECT DISTINCT ON (project_name, corpus_name, section, floor, lot_number) ol.* FROM objective_lots ol ORDER BY project_name, corpus_name, section, floor, lot_number, snapshot_date DESC, id DESC; COMMENT ON VIEW v_objective_lots_latest IS '#1964: physflat-deduped current state of objective_lots — one row per ' '(project_name, corpus_name, section, floor, lot_number) at the latest ' 'snapshot (snapshot_date DESC, id DESC). objective_lots stores ~2.91x ' 'inflated rows (multiple objective_lot_id per physical flat across ' 're-listings); this view collapses to ~603k physical flats. History lives ' 'in objective_lots_history (do not use this view for time-series).'; COMMIT;