-- 189_land_reservation_nulls_not_distinct.sql -- #2464 — дедуп land_reservation и UNIQUE NULLS NOT DISTINCT на живой таблице. -- -- БАГ. `_UPSERT_NO_ACT_SQL` (workers/tasks/izyatie_ocr_ingest.py) заканчивается -- `ON CONFLICT DO NOTHING`, а единственный подходящий констрейнт — -- `uq_land_reservation_cad_act UNIQUE (cad_num, act_number)` с обычной NULL-семантикой. -- В Postgres NULL != NULL, поэтому у записей БЕЗ номера акта конфликт не наступает -- никогда: `ON CONFLICT DO NOTHING` не срабатывает, и каждый недельный прогон -- вставляет копию. Docstring таски при этом обещает per-batch дедуп и двухшаговый -- upsert по (cad_num, doc_url) — ни того, ни другого в коде нет. -- -- ЗАМЕР ПРОДА 2026-08-20 (до правки): -- строк всего 297 -- из них с act_number IS NULL 297 (то есть все) -- групп (cad_num, doc_url) с дублями 27 -- максимум копий в группе 11 -- лишних строк 270 (91% таблицы) -- -- Проверено, что ключ подходит: ни у одного cad_num нет более одного doc_url -- (max = 1), то есть NULLS NOT DISTINCT по (cad_num, act_number) НЕ схлопнет -- разные документы одного участка. Дубли внутри групп — точные копии: по одному -- различному значению reservation_kind и act_date на группу. -- -- ЧТО УДАЛЯЕТСЯ. Строки-копии сверх первой (по возрастанию id) в каждой группе -- (cad_num, act_number) среди act_number IS NULL. Это порождение бага, а не -- пользовательские данные; таблица — кэш OCR-разбора PDF с сайта, пересобираемый -- прогоном таски. Первая строка группы (минимальный id) сохраняется целиком. -- -- ПОЧЕМУ ОТДЕЛЬНОЙ МИГРАЦИЕЙ, а не правкой CREATE TABLE: та же причина, что в -- м.158 — исходный файл уже в _schema_migrations и на деплое пропускается. -- Прецеденты NULLS NOT DISTINCT в репо: м.110, м.125, м.140, м.158. Prod = PG16.4. -- Apply after: 188_regrant_quarter_price_index_fdw.sql BEGIN; -- #2752: блокирующий DDL обязан иметь lock_timeout — иначе ALTER TABLE встанет в -- очередь за чужой сессией и уведёт за собой запросы приложения. Таблица крошечная -- (297 строк), сам DDL мгновенный; пять секунд — про ОЖИДАНИЕ блокировки, не про -- работу. Не дождались — миграция падает, а не подвешивает прод. SET LOCAL lock_timeout = '5s'; -- 1) Дедуп: оставляем строку с минимальным id в каждой группе. DELETE FROM land_reservation a USING land_reservation b WHERE a.act_number IS NULL AND b.act_number IS NULL AND a.cad_num = b.cad_num AND a.id > b.id; -- 2) Пересоздаём констрейнт с NULL-семантикой, при которой ON CONFLICT матчит. ALTER TABLE land_reservation DROP CONSTRAINT IF EXISTS uq_land_reservation_cad_act; ALTER TABLE land_reservation ADD CONSTRAINT uq_land_reservation_cad_act UNIQUE NULLS NOT DISTINCT (cad_num, act_number); COMMIT;