🛠️ 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 без ограничения в БД — слабое место в конкурентном сценарии. Проверка в коде помогает описать намерение, но целостность должна защищаться там, где возникает запись.

Сохраните сценарий: он полезен при разборе дублей, повторных запросов и гонок в бизнес-логике.

🔹🔹🔹🔹

🛠️ SELECT показал, что строки нет | Сетка — социальная сеть от hh.ru