🛠️ SELECT показал, что строки нет. Почему после INSERT появился дубль?
Классический сценарий:
T1: SELECT — строки нет T2: SELECT — строки нет T1: INSERT T2: INSERT
Обе транзакции проверили, что заказа с таким external_id нет.
Обе получили честный результат на момент своей проверки.
Обе пошли создавать строку.
Если уникальность держится только в коде приложения и не закреплена ограничением в БД, второй INSERT может пройти.
Так ломается паттерн check-then-insert: сначала проверить, потом вставить.
На малой нагрузке ошибка может долго не проявляться. В тестах всё зелёное. Один пользователь отправляет один запрос — дублей нет.
Но при ретраях, параллельных запросах, фоновых воркерах или интеграциях окно между SELECT и INSERT становится реальным.
Что проверить:
— есть ли UNIQUE-ограничение или уникальный индекс на бизнес-ключ; — что именно считается дублем: external_id, пара (customer_id, external_id), заказ за период, событие интеграции; — как приложение обрабатывает конфликт уникальности; — нужен ли INSERT ... ON CONFLICT; — есть ли идемпотентный ключ для повторных запросов; — где проходит граница транзакции; — не создают ли ретраи новые операции вместо повторения старой.
Важно: UNIQUE — это не «страховка на всякий случай». Это часть модели данных. Если бизнес-правило говорит «такая операция должна быть одна», база должна знать об этом.
А ON CONFLICT — не магия против всех гонок. Он работает там, где корректно задан конфликт уникальности. И всё равно нужно решить, что делать при повторе: игнорировать, обновлять, возвращать существующую запись или фиксировать отдельное событие.
Практический вывод: check-then-insert без ограничения в БД — слабое место в конкурентном сценарии. Проверка в коде помогает описать намерение, но целостность должна защищаться там, где возникает запись.
Сохраните сценарий: он полезен при разборе дублей, повторных запросов и гонок в бизнес-логике.
🔹🔹🔹🔹