gendesign/data/sql/175_v_objective_lots_latest.sql
Light1YT 74f1ffb500
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m49s
CI / backend-tests (pull_request) Successful in 13m58s
perf(objective): request-path consumers dedup inline, not via whole-table view (#1964)
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.
2026-06-28 05:00:47 +05:00

67 lines
5.1 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.

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