From 986514be3e1836263c796f535de7bb33c082f63c Mon Sep 17 00:00:00 2001 From: bot-backend Date: Wed, 17 Jun 2026 20:37:46 +0300 Subject: [PATCH] fix(sql): v_supply_layers_latest keep dev_group_name in DISTINCT ON key (#1650) --- .../161_supply_layers_view_devgroup_key.sql | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 data/sql/161_supply_layers_view_devgroup_key.sql diff --git a/data/sql/161_supply_layers_view_devgroup_key.sql b/data/sql/161_supply_layers_view_devgroup_key.sql new file mode 100644 index 00000000..613f4319 --- /dev/null +++ b/data/sql/161_supply_layers_view_devgroup_key.sql @@ -0,0 +1,80 @@ +-- 161_supply_layers_view_devgroup_key.sql +-- #1650 — v_supply_layers_latest collapses L3 dev_groups: fix DISTINCT ON key. +-- +-- БАГ: v_supply_layers_latest (м.125) использует DISTINCT ON по 6-колоночному ключу +-- (layer, district_name, complex_id, obj_class, source) — без dev_group_name. +-- После м.128 логический UNIQUE-ключ supply_layers расширен до 7 колонок: +-- (layer, district_name, complex_id, obj_class, dev_group_name, source, snapshot_date). +-- Для L3 (layer=3, domrf_multiphase): complex_id=NULL у ВСЕХ строк, разные dev_group'ы +-- района имеют одинаковый 6-column DISTINCT-ключ → VIEW схлопывает их в 1 строку. +-- Измерено на prod: 8 L3 строк в view (по 1 на район) vs 102 фактических строк в таблице. +-- Например, Академический: view→1 строка (607 юнитов), таблица→48 строк (41 887 юнитов). +-- +-- ЧИНИМ: добавляем dev_group_name в DISTINCT ON / ORDER BY view'а. +-- Новый DISTINCT ON ключ: +-- (layer, district_name, complex_id, obj_class, dev_group_name, source) +-- зеркалит логический ключ uq_supply_layers_logical (м.128, без snapshot_date — view +-- берёт max-снапшот, snapshot_date остаётся в ORDER BY как tiebreak). +-- +-- Зависимости: +-- • supply_layers (м.125 + м.128) — таблица с колонкой dev_group_name (м.128 обязателен). +-- • v_supply_layers_latest (м.125) — пересоздаётся здесь через CREATE OR REPLACE VIEW. +-- • Проверено: у v_supply_layers_latest нет downstream-зависимых view/matview. +-- CREATE OR REPLACE VIEW безопасен — колонки совместимы (добавляем dev_group_name в конец, +-- существующие колонки 1-13 не переупорядочиваем). +-- +-- Idempotent: CREATE OR REPLACE VIEW — повторный прогон перезаписывает view тем же DDL, +-- без ошибок. +-- Deploy: auto-applied через _schema_migrations (ровно один раз по NN). +-- Migration order: м.125 и м.128 уже применены на prod — этот файл идёт ПОСЛЕ них. +-- После применения v_supply_layers_latest сразу вернёт корректные L3 per-dev_group строки +-- без перезапуска воркера (view читает актуальные данные supply_layers напрямую). + +BEGIN; + +-- ── Пересоздать v_supply_layers_latest с dev_group_name в DISTINCT ON ───────────── +-- Ключ: (layer, district_name, complex_id, obj_class, dev_group_name, source) +-- зеркалит uq_supply_layers_logical (м.128). DISTINCT ON требует совпадения ведущих +-- ORDER BY-выражений с DISTINCT-ключом — они перечислены первыми, далее snapshot_date +-- DESC, computed_at DESC, id DESC как детерминированный tiebreak на max-снапшот. +-- dev_group_name добавлена в SELECT как последняя колонка (позиция 14) — CREATE OR REPLACE +-- VIEW в PG допускает добавление колонок в конец списка без пересоздания зависимостей. +-- NULLS NOT DISTINCT логика: view просто делает DISTINCT ON, не UNIQUE-constraint — +-- NULL в dev_group_name у L1/L2 работает как обычный NULL в DISTINCT ON (все L1/L2 строки +-- с dev_group_name=NULL дедуплируются по остальным ключевым полям, что корректно). +CREATE OR REPLACE VIEW v_supply_layers_latest AS +SELECT DISTINCT ON (layer, district_name, complex_id, obj_class, dev_group_name, source) + id, + layer, + district_name, + complex_id, + obj_class, + units_estimate, + area_estimate, + expected_online_date, + source, + confidence, + method, + snapshot_date, + computed_at, + dev_group_name +FROM supply_layers +ORDER BY + layer, + district_name, + complex_id, + obj_class, + dev_group_name, + source, + snapshot_date DESC, + computed_at DESC, + id DESC; + +COMMENT ON VIEW v_supply_layers_latest IS + 'Свежий снапшот supply_layers на логический ключ (layer, district_name, complex_id, ' + 'obj_class, dev_group_name, source) — max(snapshot_date), детерминир. tiebreak ' + 'computed_at→id DESC. dev_group_name добавлен в DISTINCT ON в м.161 (#1650): ' + 'без него L3 dev_group''ы района схлопывались в 1 строку (~95.6% потерь). ' + 'Canonical read-path для future-supply (#950 Step6) и UI/forecast.'; + +COMMIT; -- 2.45.3