perf(db): physflat-covering индекс v2 с price_per_m2_rub в INCLUDE (#2177 follow-up) #2243
1 changed files with 104 additions and 0 deletions
104
data/sql/183_objective_lots_physflat_covering_v2_price_idx.sql
Normal file
104
data/sql/183_objective_lots_physflat_covering_v2_price_idx.sql
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
-- Контекст: follow-up ревью #2240 — best-layouts supply-only запрос уходит в
|
||||
-- heap scan + external-merge Sort на тяжёлых проектах, потому что покрывающий
|
||||
-- индекс 176-й (objective_lots_physflat_covering_idx) НЕ несёт price_per_m2_rub.
|
||||
--
|
||||
-- best_layouts._SUPPLY_ONLY_LOTS_SQL дедуплицирует objective_lots до последнего
|
||||
-- снапшота на каждый ФИЗИЧЕСКИЙ лот через
|
||||
-- SELECT DISTINCT ON (project_name, corpus_name, section, floor, lot_number) …
|
||||
-- ORDER BY …, snapshot_date DESC, id DESC
|
||||
-- по квартирам (premise_kind='квартира'), а ПОСЛЕ DISTINCT ON проецирует
|
||||
-- (best_layouts.py:381-386): project_name (ключ), rooms_int, area_pd,
|
||||
-- price_per_m2_rub, is_sold, status. Все они, КРОМЕ price_per_m2_rub, уже лежат
|
||||
-- в INCLUDE-нагрузке 176-го covering-индекса. price_per_m2_rub там сознательно
|
||||
-- НЕ клали (см. коммент 176: «его проецируют только analyze-inline запросы,
|
||||
-- которые не фильтруют premise_kind='квартира'») — но supply-only запрос
|
||||
-- #2240 как раз фильтрует premise_kind='квартира' И проецирует price_per_m2_rub,
|
||||
-- поэтому одной недостающей колонки хватает, чтобы Index-Only стал невозможен:
|
||||
-- планировщик добирает price_per_m2_rub из кучи → Bitmap/Heap fetch + внешняя
|
||||
-- сортировка проекции.
|
||||
--
|
||||
-- ЗАМЕР (prod EXPLAIN ANALYZE, два тяжелейших проекта, 2026-07-02):
|
||||
-- ДО (176-й covering без price_per_m2_rub):
|
||||
-- heap scan + external-merge Sort (~7 MB temp на диск), Execution ~500 ms.
|
||||
-- ПОСЛЕ (covering + price_per_m2_rub в INCLUDE, создан в BEGIN…ROLLBACK для
|
||||
-- проверки плана):
|
||||
-- Index Only Scan using objective_lots_physflat_covering_v2_idx + Unique,
|
||||
-- внешней сортировки НЕТ, Execution ~54 ms (≈9×).
|
||||
--
|
||||
-- Пересоздаём покрывающий индекс, добавляя price_per_m2_rub в INCLUDE. Ключ
|
||||
-- (physflat + snapshot_date DESC, id DESC) и partial-WHERE (premise_kind=
|
||||
-- 'квартира') НЕ меняются → тот же DISTINCT ON обслуживает ВСЕХ прежних
|
||||
-- потребителей 176-го (market_metrics._STOCK_SQL / _SALES_WINDOW_SQL,
|
||||
-- competitors._SOLD_COUNT_SQL inline, #1959 inline) без регрессии — расширенная
|
||||
-- нагрузка их только дополняет, ключ и порядок идентичны. Дополнительно теперь
|
||||
-- Index-Only обслуживается и best_layouts supply-only.
|
||||
--
|
||||
-- ── ИМЯ / ПОРЯДОК ────────────────────────────────────────────────────────────
|
||||
-- Новое имя objective_lots_physflat_covering_v2_idx + DROP старого
|
||||
-- objective_lots_physflat_covering_idx (тем же приёмом, что 176: CREATE-new →
|
||||
-- DROP-old). Нельзя `CREATE INDEX IF NOT EXISTS` под ТЕМ ЖЕ именем: старый индекс
|
||||
-- уже существует под этим именем → IF NOT EXISTS стал бы no-op и молча пропустил
|
||||
-- бы пересборку (расширение INCLUDE не применилось бы). v2-суффикс + DROP старого:
|
||||
-- • идемпотентно при ре-apply (IF NOT EXISTS / IF EXISTS);
|
||||
-- • ни одного окна без индекса под DISTINCT ON — новый уже на месте, когда
|
||||
-- дропаем старый (оба кратковременно сосуществуют).
|
||||
-- Имя индекса нигде в backend-коде не хардкодится (планировщик выбирает индекс
|
||||
-- по форме запроса, не по имени) — переименование прозрачно для приложения.
|
||||
--
|
||||
-- ── LOCK-ПРОФИЛЬ / CONCURRENTLY ──────────────────────────────────────────────
|
||||
-- Обычный (НЕ CONCURRENTLY) CREATE INDEX берёт SHARE-lock на objective_lots —
|
||||
-- блокирует writes на таблицу на время сборки (~252 MB + одна numeric-колонка на
|
||||
-- ~1.9M строк). CONCURRENTLY здесь НЕВОЗМОЖЕН: deploy.yml гоняет каждый .sql
|
||||
-- целиком через `psql < file`, а этот файл (как 176/168/173) само-оборачивается
|
||||
-- в BEGIN/COMMIT; CREATE INDEX CONCURRENTLY внутри транзакции PostgreSQL
|
||||
-- запрещает. Тот же класс риска и то же решение, что 176 и 168 (см. их
|
||||
-- lock-комменты). Поэтому: обычный CREATE INDEX в BEGIN/COMMIT.
|
||||
-- ⚠ Деплой ВНЕ недельного окна objective-scrape (bulk INSERT/UPDATE на
|
||||
-- objective_lots): миграция создаёт ~252 MB индекс на ~1.9M строк под
|
||||
-- кратковременным SHARE-lock на objective_lots, затем дропает старый.
|
||||
--
|
||||
-- #5-эхо: ANALYZE objective_lots в конце tx — освежает оценки под новый индекс
|
||||
-- (тот же приём, что 176; выполняется в транзакции штатно).
|
||||
--
|
||||
-- Idempotent (IF NOT EXISTS / IF EXISTS), self-wrapped BEGIN/COMMIT.
|
||||
-- Порядок применения для prod: миграция применяется деплоем в NN-порядке
|
||||
-- (трекер _schema_migrations). Backend-код НЕ требует изменений — индекс
|
||||
-- прозрачен для существующего _SUPPLY_ONLY_LOTS_SQL.
|
||||
BEGIN;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS objective_lots_physflat_covering_v2_idx
|
||||
ON objective_lots (
|
||||
project_name,
|
||||
corpus_name,
|
||||
section,
|
||||
floor,
|
||||
lot_number,
|
||||
snapshot_date DESC,
|
||||
id DESC
|
||||
)
|
||||
INCLUDE (
|
||||
rooms_int,
|
||||
area_pd,
|
||||
sales_start_date,
|
||||
is_sold,
|
||||
contract_date,
|
||||
status,
|
||||
class,
|
||||
price_per_m2_rub
|
||||
)
|
||||
WHERE premise_kind = 'квартира';
|
||||
|
||||
COMMENT ON INDEX objective_lots_physflat_covering_v2_idx IS
|
||||
'Покрывающий physflat-индекс v2 (#2240): 176-й covering + price_per_m2_rub в '
|
||||
'INCLUDE. Обслуживает Index-Only Scan + Unique для DISTINCT ON (physflat) '
|
||||
'ORDER BY snapshot_date DESC, id DESC по квартирам у ВСЕХ прежних потребителей '
|
||||
'176-го (market_metrics._STOCK/_SALES, competitors, #1959) И для '
|
||||
'best_layouts._SUPPLY_ONLY_LOTS_SQL, который проецирует price_per_m2_rub. Без '
|
||||
'этой колонки supply-only уходил в heap scan + external-merge Sort '
|
||||
'(~7 MB temp, ~500 ms); с ней — Index Only Scan (~54 ms). См. 176-й.';
|
||||
|
||||
DROP INDEX IF EXISTS objective_lots_physflat_covering_idx;
|
||||
|
||||
ANALYZE objective_lots;
|
||||
|
||||
COMMIT;
|
||||
Loading…
Add table
Reference in a new issue