naive AVG() over objective_corpus_room_month rows gives equal weight to zero-deal months, diluting per-project averages by 2-10x. Projects with sparse deal history (e.g. 5 active months out of 24) were shown avg_area ≈27m² instead of the correct ≈81m² for 3-room flats. Replace both aggregates with SUM(val*cnt)/NULLIF(SUM(cnt),0) so only months with actual deals contribute weight. NULLIF prevents division-by-zero and returns NULL for all-zero projects instead of a misleading 0. Prod check (Vitamin-квартал на Титова, last 24 mo): studio 26m²/176k, 1-rm 37m²/174k, 2-rm 56m²/135k, 3-rm 81m²/136k — matches expected ranges.
3.6 KiB
| paths |
|---|
| data/sql/**/*.sql |
SQL conventions — PostgreSQL 16 / PostGIS 3.4
File naming
NN_topic.sql где NN — следующий sequential номер. Проверка: ls data/sql/ | sort | tail -5.
Structure
-- Контекст: что делает файл, зачем, порядок применения, dependencies.
BEGIN;
-- DDL здесь (idempotent)
COMMIT;
Idempotency (обязательно)
CREATE TABLE IF NOT EXISTSALTER TABLE ... ADD COLUMN IF NOT EXISTSALTER TABLE ... DROP COLUMN IF EXISTSCREATE OR REPLACE VIEWCREATE INDEX IF NOT EXISTSON CONFLICT DO NOTHINGдля seed dataDROP INDEX IF EXISTSпередCREATE INDEXесли меняется тип column
Auto-apply на prod
data/sql/NN_*.sql применяются автоматически через deploy.yml (tracking через _schema_migrations). Каждый файл — ровно один раз, по NN order. Idempotency критична.
Если auto-apply падает → deploy exit 1, containers не обновляются. Diagnose: GHA log → "Apply DB migrations". Fix: psql -f file.sql через SSH tunnel + INSERT INTO _schema_migrations (filename) VALUES ('NN_xxx.sql') ON CONFLICT DO NOTHING.
Migration order
- SQL migration first (схема) — deploy first
- Backend code matching new schema — deploy second
- Никогда наоборот — deployed code напорется на несовместимую схему
psycopg v3 CAST trap
В Python через psycopg v3 / SQLAlchemy text(...):
- ❌
:param::type— игнорируется парсером - ✅
CAST(:param AS type)— canonical
В чистом SQL (.sql файл, без bind params) — обычный ::type работает.
Reference: vault Pattern_CAST_AS_Type.
VIEW dependencies
Перед ALTER COLUMN type column'а:
- Найти зависимые view:
\d+ table_nameилиpg_get_viewdef('view_name'::regclass, true) DROP VIEW <dependent>;ALTER COLUMN ...;CREATE OR REPLACE VIEW <dependent> AS SELECT ...;
Reference: 93_cad_parcels_geom_multipolygon.sql (Polygon → MultiPolygon migration).
Агрегация по pre-aggregated строкам (обязательно weighted AVG)
Если источник содержит строки вида «одна строка = один период (месяц) + уже посчитанный
avg_value + count» (например objective_corpus_room_month), то наивный AVG(avg_value)
неверен: строки с нулевыми сделками занижают результат в 2-10x.
Правильная формула — count-weighted AVG:
SUM(avg_value * cnt) / NULLIF(SUM(cnt), 0)
NULLIF(..., 0)обязателен — предотвращаетdivision by zeroпри all-zero периодах и возвращаетNULLвместо фейкового0.- Без весов:
AVG()равноправно учитывает «пустые» месяцы → занижение.
Reference: fix #295 (120_fix_mv_layout_velocity_weighted_avg.sql),
тест backend/tests/sql/test_mv_layout_velocity_weighted_avg.py.
Запреты
- ❌
DROP TABLE/TRUNCATEбез явного approval пользователя - ❌ Не-idempotent файлы (без
IF EXISTS) — на случай ре-apply - ❌ Миграции без
BEGIN...COMMITобёртки - ❌ Hardcoded даты / IDs без комментария почему