fix(analytics): address review-bot PR B fixes (#113)
SQL (data/sql/94_mv_layout_velocity.sql): - 🟠 idempotency WARN comment перед DROP CASCADE - 🟡 WITH NO DATA + REFRESH after CREATE INDEX (меньше lock duration) - 🟡 WHERE cm.objective_group = 'Екатеринбург' (cross-region Cartesian защита) - 🟡 COMMENT ON MATERIALIZED VIEW с источниками + refresh механизмом Python (services/site_finder/layout_velocity_refresh.py): - 🟡 db.commit() после REFRESH (visibility через connection pool) - 🟡 убрать f-string в SQL — две явных text() ветки - 🟡 try/except OperationalError fallback на non-concurrent
This commit is contained in:
parent
144970aadd
commit
46c2a84ea4
2 changed files with 36 additions and 3 deletions
|
|
@ -16,6 +16,7 @@ from __future__ import annotations
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
from sqlalchemy.exc import OperationalError
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -34,8 +35,22 @@ def refresh_layout_velocity(db: Session, *, concurrently: bool = True) -> int:
|
||||||
Returns:
|
Returns:
|
||||||
Row count in the MV after refresh (for observability / alerting).
|
Row count in the MV after refresh (for observability / alerting).
|
||||||
"""
|
"""
|
||||||
mode = "CONCURRENTLY" if concurrently else ""
|
try:
|
||||||
db.execute(text(f"REFRESH MATERIALIZED VIEW {mode} mv_layout_velocity"))
|
if concurrently:
|
||||||
|
db.execute(text("REFRESH MATERIALIZED VIEW CONCURRENTLY mv_layout_velocity"))
|
||||||
|
else:
|
||||||
|
db.execute(text("REFRESH MATERIALIZED VIEW mv_layout_velocity"))
|
||||||
|
db.commit()
|
||||||
|
except OperationalError as e:
|
||||||
|
if concurrently and "cannot refresh materialized view" in str(e).lower():
|
||||||
|
logger.warning(
|
||||||
|
"CONCURRENTLY failed (MV likely not populated), falling back to non-concurrent"
|
||||||
|
)
|
||||||
|
db.rollback()
|
||||||
|
db.execute(text("REFRESH MATERIALIZED VIEW mv_layout_velocity"))
|
||||||
|
db.commit()
|
||||||
|
else:
|
||||||
|
raise
|
||||||
row = db.execute(text("SELECT COUNT(*) FROM mv_layout_velocity")).first()
|
row = db.execute(text("SELECT COUNT(*) FROM mv_layout_velocity")).first()
|
||||||
count = int(row[0]) if row else 0
|
count = int(row[0]) if row else 0
|
||||||
logger.info("mv_layout_velocity refreshed: %d rows", count)
|
logger.info("mv_layout_velocity refreshed: %d rows", count)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,13 @@
|
||||||
--
|
--
|
||||||
-- Deploy: auto-applied by deploy.yml via _schema_migrations tracking.
|
-- Deploy: auto-applied by deploy.yml via _schema_migrations tracking.
|
||||||
-- Dependencies: none (no views depend on this MV at creation time).
|
-- Dependencies: none (no views depend on this MV at creation time).
|
||||||
|
--
|
||||||
|
-- WARN: Re-apply этого файла (DR / lost _schema_migrations entry / dev local)
|
||||||
|
-- снесёт MV + CASCADE удалит зависимости (e.g. future /best-layouts views).
|
||||||
|
-- После re-apply ПЕРВЫЙ refresh должен быть concurrently=False
|
||||||
|
-- (CONCURRENTLY требует уже populated MV — упадёт на пустой).
|
||||||
|
-- Tracking table `_schema_migrations` нормально предотвращает re-apply,
|
||||||
|
-- но добавлен comment как safety reminder.
|
||||||
|
|
||||||
BEGIN;
|
BEGIN;
|
||||||
|
|
||||||
|
|
@ -59,9 +66,12 @@ JOIN objective_complex_mapping cm
|
||||||
ON cm.objective_complex_name = l.project_name
|
ON cm.objective_complex_name = l.project_name
|
||||||
WHERE l.room_bucket IS NOT NULL
|
WHERE l.room_bucket IS NOT NULL
|
||||||
AND cm.domrf_obj_id IS NOT NULL
|
AND cm.domrf_obj_id IS NOT NULL
|
||||||
GROUP BY cm.domrf_obj_id, l.room_bucket;
|
AND cm.objective_group = 'Екатеринбург' -- защита от cross-region Cartesian при future multi-city
|
||||||
|
GROUP BY cm.domrf_obj_id, l.room_bucket
|
||||||
|
WITH NO DATA;
|
||||||
|
|
||||||
-- UNIQUE index required for REFRESH CONCURRENTLY (future periodic refreshes)
|
-- UNIQUE index required for REFRESH CONCURRENTLY (future periodic refreshes)
|
||||||
|
-- Created on empty MV → instant, no lock duration on data
|
||||||
CREATE UNIQUE INDEX mv_layout_velocity_pk
|
CREATE UNIQUE INDEX mv_layout_velocity_pk
|
||||||
ON mv_layout_velocity (obj_id, room_bucket);
|
ON mv_layout_velocity (obj_id, room_bucket);
|
||||||
|
|
||||||
|
|
@ -69,4 +79,12 @@ CREATE UNIQUE INDEX mv_layout_velocity_pk
|
||||||
CREATE INDEX mv_layout_velocity_obj_idx
|
CREATE INDEX mv_layout_velocity_obj_idx
|
||||||
ON mv_layout_velocity (obj_id);
|
ON mv_layout_velocity (obj_id);
|
||||||
|
|
||||||
|
-- Initial populate (non-concurrent — MV was just created, CONCURRENTLY not allowed on empty MV)
|
||||||
|
REFRESH MATERIALIZED VIEW mv_layout_velocity;
|
||||||
|
|
||||||
|
COMMENT ON MATERIALIZED VIEW mv_layout_velocity IS
|
||||||
|
'Per-(obj_id, room_bucket) deals aggregation за last 24 months. '
|
||||||
|
'Source: objective_corpus_room_month × objective_complex_mapping (EKB only). '
|
||||||
|
'Refresh via layout_velocity_refresh.py (concurrently=True after initial populate).';
|
||||||
|
|
||||||
COMMIT;
|
COMMIT;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue