Россети WFS: сессионный feature.id как external_id — power_supply_centers раздут ×10, ON CONFLICT не сработал ни разу #3322

Closed
opened 2026-09-02 06:57:00 +00:00 by bot-backend · 1 comment
Collaborator

Армейский аудит 01-02.09, линза ptica-workers; скептик подтвердил прод-замером.

Факт

power_supply_centers: 4880 строк / 481 уникальное имя ПС — ровно 4880 различных external_id, то есть ON CONFLICT (source, external_id) не сработал ни разу за всю историю. Каждый прогон вставляет весь слой заново: ~481 настоящих подстанций лежат в среднем десятью копиями.

Механизм

rosseti_wfs_loader.py:110-112: _stable_external_id берёт feature['id'] — а это сессионный fid GeoServer-а, нестабильный между запросами. Задуманный sha1-фолбэк по атрибутам (:113-117) мёртв, пока fid присутствует — а он присутствует всегда.

Знакомый класс: ключ дедупликации выведен из нестабильного источника — «выведенный ключ обходит защиту», только здесь наоборот: нестабильный ключ отменяет схлопывание.

Последствия

Всё, что агрегирует по слою (power_summary, ближайшие ПС в скоринге, ЕЭСК-обогащение по sc_name_norm — ему повезло, оно по имени), считает по раздутой таблице. ЕЭСК-джойн вдобавок обновляет по 10 копий каждой ПС.

Лечение

  1. Ключ — sha1 от стабильных атрибутов (имя_norm + напряжение + геометрия с округлением), fid игнорировать всегда.
  2. Backfill-миграция: схлопнуть существующие копии (победитель — свежайший snapshot), пересчитать зависимые агрегаты.
  3. Замер «сколько ПРЕЖНИХ строк изменилось» до/после — по уроку моста complex_id.

Приёмка

  • count(*) == count(distinct новый_ключ) на проде после backfill (~481)
  • Повторный прогон лоадера: inserted=0, updated>0 (сейчас inserted=всё)
  • Тест: два фида с разными fid и одинаковыми атрибутами → один ключ
Армейский аудит 01-02.09, линза ptica-workers; скептик подтвердил прод-замером. ## Факт `power_supply_centers`: **4880 строк / 481 уникальное имя ПС** — ровно 4880 различных `external_id`, то есть `ON CONFLICT (source, external_id)` не сработал **ни разу** за всю историю. Каждый прогон вставляет весь слой заново: ~481 настоящих подстанций лежат в среднем десятью копиями. ## Механизм `rosseti_wfs_loader.py:110-112`: `_stable_external_id` берёт `feature['id']` — а это **сессионный fid GeoServer-а**, нестабильный между запросами. Задуманный sha1-фолбэк по атрибутам (`:113-117`) мёртв, пока fid присутствует — а он присутствует всегда. Знакомый класс: ключ дедупликации выведен из нестабильного источника — «выведенный ключ обходит защиту», только здесь наоборот: нестабильный ключ отменяет схлопывание. ## Последствия Всё, что агрегирует по слою (power_summary, ближайшие ПС в скоринге, ЕЭСК-обогащение по `sc_name_norm` — ему повезло, оно по имени), считает по раздутой таблице. ЕЭСК-джойн вдобавок обновляет по 10 копий каждой ПС. ## Лечение 1. Ключ — sha1 от стабильных атрибутов (имя_norm + напряжение + геометрия с округлением), fid игнорировать всегда. 2. Backfill-миграция: схлопнуть существующие копии (победитель — свежайший snapshot), пересчитать зависимые агрегаты. 3. Замер «сколько ПРЕЖНИХ строк изменилось» до/после — по уроку моста complex_id. ## Приёмка - [ ] count(*) == count(distinct новый_ключ) на проде после backfill (~481) - [ ] Повторный прогон лоадера: inserted=0, updated>0 (сейчас inserted=всё) - [ ] Тест: два фида с разными fid и одинаковыми атрибутами → один ключ
Author
Collaborator

Прод-приёмка после мержа PR #3329 (02.09.2026, ~13:05 UTC):

  • миграция 99c применилась деплоем: power_supply_centers 4880 → 487 строк (наблюдал переход между двумя тактами поллинга);
  • count(*) == count(DISTINCT external_id) == 487 — дублей нет;
  • count(*) WHERE external_id NOT LIKE 'h:%' = 0 — формулы питона и SQL не разошлись ни на одной строке (после синхронизации округления по deep-ревью: двоичная double-семантика в обоих концах);
  • код лоадера в живом контейнере: feature['id'] остался только в докстринге «ИГНОРИРУЕТСЯ».

487 против ожидавшихся ~481: ключ различает имя+напряжение+координаты, у нескольких одноимённых ПС это законные разные объекты (то самое «у ключа ради редкого случая считать оба числа» — здесь лишних схлопываний нет).

Осталось непроверенным (годно до 2026-09-10): следующий weekly-прогон rosseti_wfs_loader должен показать inserted=0, updated>0 — впервые в истории сработавший ON CONFLICT. Если после прогона count(*) вырастет заметно выше 487 — ключ всё ещё нестабилен, вернуться сюда.

Прод-приёмка после мержа PR #3329 (02.09.2026, ~13:05 UTC): - миграция 99c применилась деплоем: `power_supply_centers` **4880 → 487** строк (наблюдал переход между двумя тактами поллинга); - `count(*) == count(DISTINCT external_id) == 487` — дублей нет; - `count(*) WHERE external_id NOT LIKE 'h:%'` = **0** — формулы питона и SQL не разошлись ни на одной строке (после синхронизации округления по deep-ревью: двоичная double-семантика в обоих концах); - код лоадера в живом контейнере: `feature['id']` остался только в докстринге «ИГНОРИРУЕТСЯ». 487 против ожидавшихся ~481: ключ различает имя+напряжение+координаты, у нескольких одноимённых ПС это законные разные объекты (то самое «у ключа ради редкого случая считать оба числа» — здесь лишних схлопываний нет). **Осталось непроверенным (годно до 2026-09-10):** следующий weekly-прогон rosseti_wfs_loader должен показать `inserted=0, updated>0` — впервые в истории сработавший ON CONFLICT. Если после прогона `count(*)` вырастет заметно выше 487 — ключ всё ещё нестабилен, вернуться сюда.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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#3322
No description provided.