perf(sql): покрывающий индекс под offer-trend §4.3 — 6.8с → 1.1с (миграция 186) #2331

Merged
bot-backend merged 2 commits from feat/offer-trend-covering-index into main 2026-07-03 22:26:30 +00:00
Collaborator

Что

Корневой фикс QueryCanceled на /analyze (пара с #2329): offer-trend §4.3 — самый тяжёлый запрос analyze. Nested Loop на 141 936 итераций в objective_lots_history с heap-fetch на каждую → ~590k буфер-доступов, 6.8с тёплый / >8с холодный (пробивал 8с-гвард ~2/сутки).

Миграция 186: покрывающий partial-индекс (objective_lot_id, snapshot_date, fetched_at DESC) INCLUDE (price_per_m2_rub, is_sold, status) WHERE price BETWEEN 10000 AND 1000000 — ключ = join-ключ + точный порядок DISTINCT ON, INCLUDE = фильтр+проекция → Index Only Scan. Partial режет 2.24M → 1.86M строк (83%); литералы = _OFFER_PRICE_MIN/MAX. Полный разбор потребителей таблицы в комментарии миграции: триггер-писатель (166) и MV sales-tracker (164) остаются на _lot_idx, не затронуты.

Применение (уже сделано)

Deploy-раннер гоняет миграции в транзакции → CONCURRENTLY невозможен, а обычный CREATE INDEX заблокировал бы writes на ~20-60с. Поэтому индекс уже создан на проде руками CONCURRENTLY (без блокировки, 134 МБ — по оценке) + зарегистрирован в _schema_migrations → деплой-apply этой миграции — no-op (idempotent IF NOT EXISTS + трекер). Файл — фиксация схемы.

Прод-верификация (тот же baseline-запрос, до/после)

до после
План history-узла Index Scan _lot_idx + heap-fetch ×141936 Index Only Scan offer_trend_idx
Execution 6789 мс 1072 мс (6.3×)

Heap Fetches 21.7k → дожмётся к ~0 после autovacuum (visibility map). 8с-гвард теперь недостижим и на холодном кэше → GlitchTip-события исчезают по корню.

Валидация

Эфемерный postgis 16-3.4: миграция применяется чисто, 2-й прогон idempotent-skip, Index Only Scan подтверждён, после VACUUM Heap Fetches = 0.

## Что Корневой фикс QueryCanceled на /analyze (пара с #2329): offer-trend §4.3 — самый тяжёлый запрос analyze. Nested Loop на 141 936 итераций в `objective_lots_history` с heap-fetch на каждую → ~590k буфер-доступов, 6.8с тёплый / >8с холодный (пробивал 8с-гвард ~2/сутки). **Миграция 186**: покрывающий partial-индекс `(objective_lot_id, snapshot_date, fetched_at DESC) INCLUDE (price_per_m2_rub, is_sold, status) WHERE price BETWEEN 10000 AND 1000000` — ключ = join-ключ + точный порядок DISTINCT ON, INCLUDE = фильтр+проекция → Index Only Scan. Partial режет 2.24M → 1.86M строк (83%); литералы = `_OFFER_PRICE_MIN/MAX`. Полный разбор потребителей таблицы в комментарии миграции: триггер-писатель (166) и MV sales-tracker (164) остаются на `_lot_idx`, не затронуты. ## Применение (уже сделано) Deploy-раннер гоняет миграции в транзакции → CONCURRENTLY невозможен, а обычный CREATE INDEX заблокировал бы writes на ~20-60с. Поэтому индекс **уже создан на проде руками CONCURRENTLY** (без блокировки, 134 МБ — по оценке) + зарегистрирован в `_schema_migrations` → деплой-apply этой миграции — no-op (idempotent IF NOT EXISTS + трекер). Файл — фиксация схемы. ## Прод-верификация (тот же baseline-запрос, до/после) | | до | после | |---|---|---| | План history-узла | Index Scan `_lot_idx` + heap-fetch ×141936 | **Index Only Scan** `offer_trend_idx` | | Execution | 6789 мс | **1072 мс** (6.3×) | Heap Fetches 21.7k → дожмётся к ~0 после autovacuum (visibility map). 8с-гвард теперь недостижим и на холодном кэше → GlitchTip-события исчезают по корню. ## Валидация Эфемерный postgis 16-3.4: миграция применяется чисто, 2-й прогон idempotent-skip, Index Only Scan подтверждён, после VACUUM Heap Fetches = 0.
bot-backend added 1 commit 2026-07-03 22:07:33 +00:00
perf(sql): покрывающий индекс под offer-trend §4.3 (миграция 186)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m52s
CI / backend-tests (pull_request) Successful in 14m41s
5238ec9977
Профилирование 2026-07-04: offer-trend — самый тяжёлый запрос /analyze
(Nested Loop 141k итераций в objective_lots_history с heap-fetch на
каждую — ~590k буфер-доступов, 6.8с тёплый, >8с холодный → пробивал
8с-гвард ~2/сутки). Покрывающий partial-индекс: ключ = join+порядок
DISTINCT ON, INCLUDE = фильтр+проекция → Index Only Scan.

УЖЕ ПРИМЕНЁН на прод руками CONCURRENTLY (без блокировки writes,
build 134 МБ) + зарегистрирован в _schema_migrations — деплой-apply
этой миграции no-op (IF NOT EXISTS + трекер). Прод-верификация:
Execution 6789мс → 1072мс (6.3×), Index Only Scan подтверждён.
Триггер-писатель (166) и MV (164) остаются на _lot_idx — не затронуты
(разбор потребителей в комментарии миграции).
Light1YT added 1 commit 2026-07-03 22:09:50 +00:00
docs(sql): миграция 186 — комментарий актуализирован (индекс уже на проде)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m42s
CI / backend-tests (pull_request) Successful in 14m52s
366d06dcbd
bot-backend merged commit 30e033d653 into main 2026-07-03 22:26:30 +00:00
bot-backend deleted branch feat/offer-trend-covering-index 2026-07-03 22:26:31 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2331
No description provided.