deep-review #1964: v_objective_lots_latest has NO premise/district filter inside, so a consumer's outer WHERE cannot push below DISTINCT ON → the view materializes the WHOLE table (Parallel Seq Scan + external Sort 1.76M rows, ~55MB spill) on every query. For REQUEST-PATH consumers inside analyze_parcel this is a ~19x latency regression vs the pre-#1964 raw-table plan. DISPROVEN remedy (NOT applied): a full index on the physflat-key does NOT help — DISTINCT ON selects ol.* (51 cols, width≈945) so index-only-unique is impossible; the planner ignores the index (seq-scan+sort still cheaper) and even forced it is ~3.9 s. A 142MB index for zero request-path benefit + slower bulk-INSERT during objective-scrape is wrong. Honors original #1964 decision "no new index". Prod EXPLAIN (Академический / 3km radius, 2026-06-28): consumer via view inline (this commit) concepts median 5854 ms 1640 ms (bitmap district + sort) parcels district 5854 ms 1640 ms parcels geo-median 6443 ms 122 ms (NestedLoop geo->complex bitmap) parcels obj_pricing 5721 ms 441 ms (project bitmap per nearby ЖК) FIX: keep v_objective_lots_latest ONLY for batch/background/cached consumers (supply_layers L1, competitors._SOLD_COUNT_SQL, special_indices [/forecast bg task 30-180s], admin, landing). Revert the 4 request-path consumers inside analyze_parcel to inline DISTINCT ON (physflat-key, latest snapshot) with the filter pushed INTO the CTE so the district/spatial/project index applies: - concepts._OBJECTIVE_MEDIAN_SQL - parcels.py district price block - parcels.py geo-radius median (complex_id-scoped) - parcels.py obj_pricing CTE (project_name-scoped; aggregates over deduped set) Migration 175 header CORRECTED: accurately states the partial mig-173 index does NOT serve the view (qual can't push below DISTINCT ON), the full index is disproven/not added, and which consumers use the view vs inline. No DDL change (still view-only). Tests: +guards (concepts/obj_pricing dedup inline, not view; obj_pricing physflat DISTINCT ON; perf-pushdown scope preserved). 965 passed.
67 lines
5.1 KiB
PL/PgSQL
67 lines
5.1 KiB
PL/PgSQL
-- Контекст: #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;
|