gendesign/data/sql/173_objective_lots_physflat_idx.sql
Light1YT 41804ed70e fix(forecast): посегментный+дедуплицированный индекс дефицита (#1959)
Корень «−1.00 везде» (эпик #1953): compute_demand_supply_forecast брал
district-wide unit_velocity (847.5/мес, ВСЕ классы/комнаты) как спрос и
весь district-сток (~63k доступных) как предложение для КАЖДОЙ ячейки
what_to_build → один и тот же ratio во всех ячейках → все deficit_index
прижаты к −1.0. Плюс objective_lots — append-per-snapshot (~2.9× инфляция
строк), что симметрично раздувало обе базы → даже сегментация без дедупа
осталась бы вырожденной.

Фикс (blast radius — ТОЛЬКО forecast/deficit calc; platform-wide dedup = #1964):
- market_metrics.compute_market_metrics: +obj_class/+room_bucket (+cache key).
  _STOCK_SQL и _SALES_WINDOW_SQL дедуплят до ПОСЛЕДНЕГО снапшота на физлот
  (DISTINCT ON project_name,corpus_name,section,floor,lot_number ORDER BY …
  snapshot_date DESC,id DESC), затем агрегируют. Class-фильтр (LOWER=LOWER,
  class lowercase) + room-bucket (Source-B room_area-вокабуляр, зеркало
  sales_series.room_area_bucket_of → what_to_build фильтрует без перевода).
  ROLLUP/GROUPING сохранён; confidence считается на дедуплицированных counts.
- demand_supply_forecast: base_pace и open-сток теперь ПОСЕГМЕНТНЫЕ
  (market_metrics(obj_class,room_bucket)). При заданном сегменте L2/L3
  (hidden/future) ИСКЛЮЧЕНЫ из баланса — они класс/формат-агностичны, иначе
  двоились бы по всем ячейкам. +_market_room_bucket VOCAB-мост (валидирующий
  pass-through Source-B меток; неизвестное → None = без фильтра, не тихий 0-rows).
- what_to_build/_DEFAULT_CLASSES и recommendation Economy-маппинг: «эконом»→
  «стандарт» (в objective_lots эконома НЕТ, стандарт=483k → раньше ячейка
  матчила 0 строк и молча выпадала).
- report_assembler honesty-guard: если ВСЯ сетка прижата к ±1.0
  (degenerate-fallback) — не эмитим «строить»/«избегать», показываем
  «недостаточно гранулярных данных для посегментного вывода».
- data/sql/173_objective_lots_physflat_idx.sql: partial index под DISTINCT ON
  (Index Only Scan + Unique, без Sort на 1.75M строк; idempotent, BEGIN/COMMIT).

Prod-verify (parcel 66:41:0205010:287, Железнодорожный, h=24): ячейки
ДИФФЕРЕНЦИРУЮТ (12 measured, 7 distinct) вместо all −1.0; MOI комфорт/студия
38.5 vs стандарт/студия 244.3 (точное совпадение с ожидаемым).

Тесты: регрессия «ячейки различаются (не all −1.0)» + vocab-translation +
honesty-guard + посегментное предложение. ruff clean; no :name::type.
2026-06-27 22:50:58 +05:00

38 lines
2.3 KiB
PL/PgSQL
Raw Permalink 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.

-- Контекст: индекс дефицита #1959 (эпик #1953) — «1.00 везде».
--
-- Прогнозный движок дефицита (compute_demand_supply_forecast) дедуплицирует
-- objective_lots до ПОСЛЕДНЕГО снапшота на каждый ФИЗИЧЕСКИЙ лот
-- (objective_lots — append-per-snapshot, ~2.9× инфляция строк; без дедупа спрос И
-- предложение оба раздуты симметрично → каждая ячейка what_to_build даёт один и тот
-- же ratio → все deficit_index = 1.0). Дедуп делается через
-- SELECT DISTINCT ON (project_name, corpus_name, section, floor, lot_number) …
-- ORDER BY project_name, corpus_name, section, floor, lot_number,
-- snapshot_date DESC, id DESC
-- по квартирам (premise_kind='квартира'). Этот partial index точно совпадает с
-- ключом DISTINCT ON и его направлениями сортировки → планировщик берёт Index-Only
-- Scan + Unique вместо сортировки 1.75M строк (EXPLAIN на prod: Sort исчезает,
-- cost ≈ 122k вместо полной сортировки).
--
-- DESC на snapshot_date/id — чтобы «последний снапшот первым» читался прямо из
-- индекса (DISTINCT ON берёт первую строку каждой группы). premise_kind в WHERE —
-- partial index только по квартирам (единственный сегмент дефицит-расчёта), меньше
-- и плотнее, чем полный.
--
-- Порядок применения: миграция ПЕРВОЙ (deploy first), затем backend-код, который
-- использует DISTINCT ON. Idempotent (CREATE INDEX IF NOT EXISTS) — безопасно при
-- ре-apply. Self-wrapped BEGIN/COMMIT.
BEGIN;
CREATE INDEX IF NOT EXISTS objective_lots_physflat_latest_idx
ON objective_lots (
project_name,
corpus_name,
section,
floor,
lot_number,
snapshot_date DESC,
id DESC
)
WHERE premise_kind = 'квартира';
COMMIT;