feat(analytics): mv_quarter_price_per_m2 MV + refresh helper (#33 D1 PR A) #209
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#209
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/33-mv-quarter-price"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Issue #33 D1 PR A — materialized view per-cad_quarter price aggregation из 6.83M rosreestr_deals. Заменяет district median (9 районов) на quarter granularity (~9700 ячеек для ЕКБ из 52 492 all-Russia).
Schema findings (real column names — НЕ как в issue spec)
cad_quarterquarter_cad_numberprice_per_m2price_per_sqm(99.9% filled)deal_dateperiod_start_daterealestate_typerealestate_type_codeFallback
deal_price / areaдля ~6K rows с NULLprice_per_sqm.SQL (
data/sql/95_mv_quarter_price.sql, 124 LOC)realestate_type_code='002001003000'(новостройки) + outlier30K..800K руб/м²p25/median/p75/mean+ temporal slices (median_6m/12m/24m)(quarter_cad_number)→ CONCURRENTLY refresh capablePython helper (
backend/app/services/site_finder/quarter_price_refresh.py, 65 LOC)refresh_quarter_price(db, concurrently=True)— pattern reuse из PR #194OperationalErrorfallback на non-concurrent при не-populated MVЧто НЕ в PR (Phase B follow-up)
/analyzeresponse должен возвращатьdemand.market_price: {p25, median, p75, deals_count, freshness}вместо district median. Отдельный PR #33 PR B (~50 LOC backend вparcels.py).refresh_quarter_price(отдельный issue, после Phase B).Test plan
quarter_cad_number,price_per_sqm,period_start_date,realestate_type_code_schema_migrationsSELECT COUNT(*) FROM mv_quarter_price_per_m2≥ 50 000SELECT * FROM mv_quarter_price_per_m2 WHERE quarter_cad_number = '66:41:0204016'(Академический ЕКБ) → median > 150K руб/м²Closes part of #33 (full close after PR B analyze integration).
Related: #194 (mv_layout_velocity — same pattern).
Deep Code Review — verdict
Summary
e1a2a77· base092976f6· mergeable: true🔴 Critical / 🟠 High / 🟡 Medium
None.
🟢 Low / nits (не блокирующее, можно отдельным PR или next iteration)
data/sql/95_mv_quarter_price.sql:88—median_24m AS m.medianалиас "for API symmetry" — fine, но slight redundancy в выборке. Inline comment покрывает.backend/app/services/site_finder/quarter_price_refresh.py— placed undersite_finder/(consistent withlayout_velocity_refresh.pyиз #194); arguablyservices/analytics/точнее по domain, но pattern уже established — оставить.data/sql/95_mv_quarter_price.sql:50-65— два отдельных GROUP BY наfilteredCTE (agg_main+agg_windows) дают 2× full sort на 1.89M rows (видно в EXPLAIN). Можно объединить черезFILTERчерез subquery, но PG16 не поддерживает FILTER наPERCENTILE_CONT→ текущий CASE-NULL workaround корректен. Тред-офф acceptable для weekly batch.Verified live via postgres MCP
quarter_cad_number varchar,price_per_sqm numeric,deal_price numeric,area numeric,period_start_date date,realestate_type_code text) match diff 1-to-1 — PR body claim верный, issue spec был устаревшим.rosreestr_dealspartitioned byRANGE (period_start_date), 9 partitions (2024q1..2026q1).WHERE period_start_date >= NOW() - INTERVAL '24 months'корректно prune'ит_2024q1(out of window) — EXPLAIN подтвердил.rosreestr_deals_realtype_idxonrealestate_type_codeиспользуется → Parallel Bitmap Heap Scan по 8 партициям, не Seq Scan. Дополнительный composite index НЕ нужен.Cross-file impact analysis
mv_quarter_price_per_m2— этот PR его вводит. Zero breakage risk.backend/app/services/analytics_queries.py:1842-2493используетekb_districts.median_price_per_m2) — НЕ тронут. Phase B заменит — корректное scope discipline.backend/app/api/v1/parcels.pyиспользуетrosreestr_dealsдля cadastre comparables — независимый path, no interference.analytics_refresh.py/workers/tasks/refresh_analytics.pyсуществуют для будущей beat schedule integration (Issue #33 PR C) — корректно не тронуты в этом PR.Pattern consistency vs merged PR #194 (
mv_layout_velocity)1-to-1 reuse всех fix'ов из #194:
WITH NO DATA+ UNIQUE INDEX → REFRESH after — ✅mv_quarter_price_pk (quarter_cad_number)для CONCURRENTLY — ✅DROP ... IF EXISTS CASCADE+ WARN comment про DR re-apply — ✅COMMENT ON MATERIALIZED VIEWдля observability — ✅db.commit()после REFRESH, literal SQL strings (no f-string), try/exceptOperationalErrorс fallback на non-concurrent — ✅from __future__ import annotations, type hints,logger.*— ✅Conventions check
BEGIN; ... COMMIT;обёртка — ✅95_*— next free после94_mv_layout_velocity.sql✅DROP IF EXISTS CASCADE+_schema_migrationstracking — ✅CAST(:x AS type)rule — N/A (no bind params в helper)print, norequests, noimport psycopg2, noexcept: pass✅Vault cross-check
domains/rosreestr/MV_Quarter_Price_Per_M2_May16.md— entry создана в той же сессии (per CLAUDE.md rule #6: "Vault обновляется вместе с кодом").limitations/Limit_District_Median_Null.md— этот MV is the documented replacement, no conflict.domains/api/endpoints/Endpoint_Parcel_BestLayouts.md— references PR #194 as same pattern; этот PR — consistent extension.Performance findings
rosreestr_deals— existingrosreestr_deals_realtype_idxдоминирует план.Positive observations
quarter_cad_numbervscad_quarter,price_per_sqmvsprice_per_m2, etc.)median_24malias предупреждает readers о намеренной избыточностиdeal_price / areaдля ~6K rows с NULLprice_per_sqm— корректно черезCASE WHEN ... area > 0Recommended next steps
data/sql/**+backend/app/services/**).SELECT COUNT(*) FROM mv_quarter_price_per_m2≥ 50K66:41:0204016) median > 150K руб/м²/analyzeresponse →demand.market_price: {p25, median, p75, deals_count, freshness}(planned, ~50 LOC вparcels.py).refresh_quarter_price.Complexity / blast radius score
DROP MATERIALIZED VIEW mv_quarter_price_per_m2 CASCADE;+ delete entry из_schema_migrations. No data loss (MV is derived).Auto-merge policy: ❌ BLOCKED for auto-merge — touches
data/sql/**+backend/app/services/**per scope policy. ✅ Verdict APPROVE — clean для human-initiated merge.