Šis raksts pašlaik ir krievu valodā — angļu tulkojums tiek gatavots.
Обычный INSERT в PostgreSQL падает с ошибкой, если вы пытаетесь вставить строку, которая нарушает уникальность.
Например, в таблице users есть уникальный email:
CREATE TABLE users (
id bigint PRIMARY KEY,
email text NOT NULL UNIQUE,
name text,
country text
);
Если строка с таким email уже есть, обычный INSERT упадёт:
INSERT INTO users (email, name, country)
VALUES ('ada@example.com', 'Ada', 'GB');
Ошибка будет примерно такая:
duplicate key value violates unique constraint
Иногда это нормальное поведение. Но часто нам нужно другое:
если строка уже есть — просто пропусти её и не ломай весь процесс.
Для этого в PostgreSQL есть конструкция:
ON CONFLICT DO NOTHING
Она превращает вставку в безопасную повторяемую операцию.
Что делает ON CONFLICT DO NOTHING
ON CONFLICT DO NOTHING говорит PostgreSQL:
попробуй вставить строку;
если она конфликтует с уникальным ограничением — ничего не делай.
Пример:
INSERT INTO users (id, email, name, country)
VALUES (1, 'ada@example.com', 'Ada', 'GB')
ON CONFLICT (id) DO NOTHING;
Если пользователя с id = 1 ещё нет, строка вставится.
Если пользователь с id = 1 уже есть, PostgreSQL не упадёт с ошибкой. Он просто пропустит вставку.
Существующая строка при этом не изменится.
То есть DO NOTHING — это не обновление. Это именно:
если можно вставить — вставь
если нельзя из-за конфликта — пропусти
Зачем это нужно
ON CONFLICT DO NOTHING часто используют там, где операция может выполниться несколько раз.
Например:
- сидинг справочников;
- миграции;
- повторяемые SQL-скрипты;
- фоновые задачи;
- импорт данных;
- обработка событий;
- вставка записей из очереди;
- создание дефолтных настроек;
- защита от повторного запуска одного и того же процесса.
Главная идея — идемпотентность.
Идемпотентная операция — это операция, которую можно выполнить несколько раз, а итоговое состояние останется таким же, как после первого успешного запуска.
Например:
INSERT INTO users (id, email, name, country)
VALUES (1, 'ada@example.com', 'Ada', 'GB')
ON CONFLICT (id) DO NOTHING;
Если выполнить этот запрос 10 раз, после первого запуска таблица уже будет содержать нужную строку. Остальные запуски ничего не изменят и не упадут с ошибкой.
Это очень удобно в деплое: скрипт можно перезапустить после сбоя, и он не начнёт ломаться на уже созданных строках.
Обычный INSERT против ON CONFLICT DO NOTHING
Обычный INSERT:
INSERT INTO users (id, email, name)
VALUES (1, 'ada@example.com', 'Ada');
Если id = 1 уже существует, команда упадёт.
А если это часть транзакции, ошибка может испортить всю транзакцию, и дальше придётся делать ROLLBACK.
С ON CONFLICT DO NOTHING:
INSERT INTO users (id, email, name)
VALUES (1, 'ada@example.com', 'Ada')
ON CONFLICT (id) DO NOTHING;
Если конфликт есть, команда не падает.
Она просто вставит 0 строк.
Важно:
- транзакция остаётся живой;
- существующая строка не обновляется;
- конфликтующая строка пропускается;
- остальные строки в массовой вставке могут вставиться.
Пример с уникальным email
Допустим, в таблице есть ограничение:
CREATE TABLE users (
id bigserial PRIMARY KEY,
email text NOT NULL UNIQUE,
name text
);
Теперь можно сделать безопасную вставку по email:
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada')
ON CONFLICT (email) DO NOTHING;
Если email свободен — строка вставится.
Если email уже занят — PostgreSQL ничего не сделает.
Такой запрос можно использовать для создания пользователя, если он ещё не существует.
Но важно понимать: если пользователь уже есть, его имя не обновится.
Например, если в таблице уже лежит:
email | name
----------------+------
ada@example.com | Ada Old
а вы выполните:
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada New')
ON CONFLICT (email) DO NOTHING;
результат останется прежним:
email | name
----------------+------
ada@example.com | Ada Old
DO NOTHING не меняет существующую строку.
Если нужно обновить существующую строку, это уже другая конструкция:
ON CONFLICT ... DO UPDATE
Conflict target: какой конфликт игнорируем
В запросе:
ON CONFLICT (email) DO NOTHING
часть:
(email)
называется conflict target.
Она говорит PostgreSQL:
если конфликт произошёл по уникальности email, пропусти строку.
Пример:
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada')
ON CONFLICT (email) DO NOTHING;
Чтобы это работало, на email должен быть уникальный индекс или уникальное ограничение.
Например:
CREATE UNIQUE INDEX users_email_idx
ON users (email);
или:
CREATE TABLE users (
id bigserial PRIMARY KEY,
email text NOT NULL UNIQUE,
name text
);
Если уникальности на email нет, PostgreSQL не примет такой ON CONFLICT.
Он не может игнорировать конфликт, которого с точки зрения базы не существует.
Конфликт по PRIMARY KEY
Самый частый вариант — конфликт по первичному ключу.
INSERT INTO employees (id, name, dept)
VALUES (1, 'Grace', 'eng')
ON CONFLICT (id) DO NOTHING;
Если сотрудник с id = 1 уже есть, строка будет пропущена.
Это удобно для сидеров и справочников, где id заранее фиксированы.
Например:
INSERT INTO roles (id, code, title)
VALUES
(1, 'admin', 'Administrator'),
(2, 'manager', 'Manager'),
(3, 'student', 'Student')
ON CONFLICT (id) DO NOTHING;
Такой сидер можно запускать повторно.
ON CONSTRAINT: конфликт по имени ограничения
Иногда удобнее указать не список колонок, а имя ограничения.
Например, таблица:
CREATE TABLE users (
id bigserial PRIMARY KEY,
email text NOT NULL,
name text,
CONSTRAINT users_email_key UNIQUE (email)
);
Можно написать:
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada')
ON CONFLICT ON CONSTRAINT users_email_key DO NOTHING;
Это полезно, если:
- ограничение уже имеет понятное имя;
- уникальность сложная;
- уникальный индекс построен по нескольким колонкам;
- вы хотите явно привязаться к конкретному constraint.
Например:
ON CONFLICT ON CONSTRAINT users_email_key DO NOTHING
читается как:
если конфликт случился по ограничению users_email_key, ничего не делай.
Конфликт по нескольким колонкам
Уникальность может быть составной.
Например, один пользователь не может иметь две настройки с одинаковым ключом:
CREATE TABLE user_settings (
user_id bigint NOT NULL,
key text NOT NULL,
value text,
UNIQUE (user_id, key)
);
Тогда безопасная вставка выглядит так:
INSERT INTO user_settings (user_id, key, value)
VALUES (1, 'language', 'ru')
ON CONFLICT (user_id, key) DO NOTHING;
Если у пользователя 1 уже есть настройка language, вставка будет пропущена.
Если такого сочетания ещё нет, строка вставится.
Важно: конфликт здесь не по user_id отдельно и не по key отдельно, а именно по паре:
(user_id, key)
DO NOTHING без conflict target
PostgreSQL позволяет написать так:
INSERT INTO users (id, email, name)
VALUES (1, 'ada@example.com', 'Ada')
ON CONFLICT DO NOTHING;
Здесь мы не указали, по какому именно ограничению игнорировать конфликт.
В таком виде PostgreSQL проигнорирует конфликт по любому подходящему уникальному ограничению или первичному ключу.
На первый взгляд удобно, но часто опасно.
Допустим, в таблице есть:
PRIMARY KEY (id)
UNIQUE (email)
Вы вставляете:
INSERT INTO users (id, email, name)
VALUES (2, 'ada@example.com', 'Ada Clone')
ON CONFLICT DO NOTHING;
Если id = 2 новый, но email = 'ada@example.com' уже занят, строка тоже будет молча пропущена.
Вы можете подумать, что всё хорошо, хотя на самом деле сработал конфликт по email, а не по id.
Поэтому правило:
Если вам важна причина пропуска, указывайте conflict target явно.
Лучше:
ON CONFLICT (id) DO NOTHING
или:
ON CONFLICT (email) DO NOTHING
чем слишком широкое:
ON CONFLICT DO NOTHING
RETURNING показывает только вставленные строки
RETURNING в INSERT возвращает строки, которые реально были вставлены.
Пример:
INSERT INTO orders (id, user_id, amount, status)
VALUES (5001, 1, 99.90, 'paid')
ON CONFLICT (id) DO NOTHING
RETURNING id, status;
Если заказа 5001 ещё не было, результат будет:
id | status
-----+-------
5001 | paid
Если заказ 5001 уже существует, вставка будет пропущена, а RETURNING вернёт 0 строк.
То есть:
строка вставилась -> RETURNING вернул строку
строка была пропущена -> RETURNING ничего не вернул
Это удобно в приложении.
Можно понять, была ли реально вставка, без отдельного запроса.
DO NOTHING не возвращает существующую строку
Важный момент: если строка уже была, DO NOTHING RETURNING не вернёт существующую строку.
Например:
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada')
ON CONFLICT (email) DO NOTHING
RETURNING id, email, name;
Если пользователь уже есть, результат будет пустым.
Не будет так:
id | email | name
---+-----------------+-----
1 | ada@example.com | Ada
Почему?
Потому что RETURNING возвращает только строки, затронутые самим INSERT.
А при DO NOTHING конфликтующая строка не вставляется и не обновляется.
Если нужно получить строку независимо от того, была она вставлена или уже существовала, есть два варианта:
- сделать отдельный
SELECT;
- использовать
ON CONFLICT ... DO UPDATE, если это подходит по смыслу.
Массовая вставка с DO NOTHING
ON CONFLICT DO NOTHING особенно полезен для массовых вставок.
Например, сидинг сотрудников:
INSERT INTO employees (id, name, manager_id, dept, salary)
VALUES
(1, 'Grace', NULL, 'eng', 180000),
(2, 'Linus', 1, 'eng', 150000),
(3, 'Margaret', 1, 'eng', 150000)
ON CONFLICT (id) DO NOTHING;
Если часть строк уже есть, они будут пропущены.
Если часть строк новая, они вставятся.
Например:
id = 1 уже есть -> пропустить
id = 2 уже есть -> пропустить
id = 3 отсутствует -> вставить
Команда не падает целиком из-за одного дубля.
Это удобно для справочников, ролей, тарифов, дефолтных настроек и тестовых данных.
Дубликаты внутри одной пачки
Допустим, в одном INSERT случайно пришли две строки с одинаковым ключом:
INSERT INTO roles (id, code)
VALUES
(1, 'admin'),
(1, 'administrator')
ON CONFLICT (id) DO NOTHING;
Одна строка может вставиться, а другая будет пропущена из-за конфликта.
Но лучше не полагаться на такие дубликаты как на нормальную бизнес-логику.
Если порядок и выбор «какая строка победит» важны, лучше заранее очистить входные данные.
Например, через DISTINCT ON, ROW_NUMBER или отдельный staging-этап.
DO NOTHING — это защита от дублей, а не инструмент выбора правильной версии данных.
INSERT ... SELECT с DO NOTHING
Частый сценарий — перелить данные из staging-таблицы в основную.
Например, есть временная таблица staging_users:
email | name | country
----------------+------+--------
ada@example.com | Ada | GB
bob@example.com | Bob | US
Основная таблица users имеет уникальный email.
Можно вставить только новых пользователей:
INSERT INTO users (email, name, country)
SELECT
email,
name,
country
FROM staging_users
ON CONFLICT (email) DO NOTHING;
Если пользователь с таким email уже есть, строка пропускается.
Если email новый, строка вставляется.
Это часто лучше, чем писать:
WHERE NOT EXISTS (...)
потому что ON CONFLICT безопаснее при параллельных вставках.
Почему ON CONFLICT лучше, чем WHERE NOT EXISTS
Иногда пытаются сделать так:
INSERT INTO users (email, name)
SELECT 'ada@example.com', 'Ada'
WHERE NOT EXISTS (
SELECT 1
FROM users
WHERE email = 'ada@example.com'
);
На первый взгляд логично:
если пользователя нет — вставь.
Но при параллельной работе возможна гонка.
Два процесса одновременно проверили:
пользователя нет
и оба попытались вставить.
Один вставит, второй упадёт на уникальном ограничении.
ON CONFLICT DO NOTHING решает это аккуратнее:
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada')
ON CONFLICT (email) DO NOTHING;
Уникальный индекс становится источником правды.
Если другой процесс успел вставить такую строку первым, текущий запрос просто пропустит вставку.
Главная мысль:
Не проверка перед вставкой защищает от дублей, а уникальное ограничение плюс ON CONFLICT.
Что происходит с триггерами
При ON CONFLICT DO NOTHING существующая строка не обновляется.
Поэтому триггеры на UPDATE не вызываются.
Например, не будет логики вида:
BEFORE UPDATE
AFTER UPDATE
Потому что обновления нет.
Но важно понимать: это не значит, что вообще никакие триггеры никогда не сработают. Логика INSERT и проверка конфликтов имеют свои нюансы, особенно если есть BEFORE INSERT-триггеры.
Практическое правило проще:
DO NOTHING не меняет существующую строку и не запускает update-логику.
Если вам нужно обновлять updated_at, имя, статус или другие поля при конфликте, нужен не DO NOTHING, а DO UPDATE.
DO NOTHING не проверяет бизнес-равенство
Допустим, у вас есть пользователь:
email | name
----------------+------
ada@example.com | Ada Old
Вы выполняете:
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada New')
ON CONFLICT (email) DO NOTHING;
PostgreSQL не будет проверять, совпадает ли name.
Он просто увидит конфликт по email и пропустит строку.
В результате Ada Old останется.
Это правильно для DO NOTHING.
Если бизнес-логика говорит:
если email уже есть, обнови name
тогда нужен upsert:
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada New')
ON CONFLICT (email) DO UPDATE
SET name = EXCLUDED.name;
DO NOTHING подходит только тогда, когда существующая строка должна остаться как есть.
DO NOTHING и NOT NULL / CHECK / FOREIGN KEY
ON CONFLICT DO NOTHING помогает только с конфликтами уникальности.
Он не глушит любые ошибки.
Например, если колонка email обязательная:
email text NOT NULL
то такой запрос всё равно упадёт:
INSERT INTO users (email, name)
VALUES (NULL, 'Ada')
ON CONFLICT (email) DO NOTHING;
Потому что это нарушение NOT NULL, а не уникальный конфликт.
То же самое с CHECK:
salary numeric CHECK (salary > 0)
Если вставить отрицательную зарплату, DO NOTHING не спасёт.
И с внешними ключами тоже:
user_id REFERENCES users(id)
Если вы вставляете заказ с несуществующим user_id, это не конфликт уникальности. Это ошибка внешнего ключа.
Главное правило:
ON CONFLICT DO NOTHING игнорирует конфликты уникальности, но не превращает любой плохой INSERT в успешный.
Частичная уникальность
В PostgreSQL бывают частичные уникальные индексы.
Например, только один активный промокод с конкретным кодом:
CREATE UNIQUE INDEX promo_codes_active_code_idx
ON promo_codes (code)
WHERE active;
Для таких случаев conflict target может включать условие индекса.
Например:
INSERT INTO promo_codes (code, discount, active)
VALUES ('SUMMER', 20, true)
ON CONFLICT (code) WHERE active DO NOTHING;
Так PostgreSQL понимает, что речь идёт о частичном уникальном индексе.
Это уже более продвинутый сценарий, но он важен: ON CONFLICT должен совпадать с реальным уникальным индексом, включая его условие, если индекс частичный.
DO NOTHING и конкурентные вставки
ON CONFLICT DO NOTHING хорошо работает при параллельных вставках.
Например, два воркера одновременно пытаются вставить одно и то же событие:
INSERT INTO processed_events (event_id, processed_at)
VALUES ('evt_123', now())
ON CONFLICT (event_id) DO NOTHING;
Если event_id уникальный, один воркер вставит строку, второй пропустит.
Оба запроса завершатся без ошибки.
Это полезно для идемпотентной обработки событий:
- webhook может прийти дважды;
- очередь может переотдать сообщение;
- фоновой процесс может перезапуститься;
- задача может быть выполнена повторно.
Уникальное ограничение защищает таблицу от дублей, а DO NOTHING защищает приложение от падения на ожидаемом повторе.
Пример: таблица обработанных событий
Допустим, есть таблица:
CREATE TABLE processed_events (
event_id text PRIMARY KEY,
processed_at timestamptz NOT NULL DEFAULT now()
);
Перед обработкой события можно попытаться вставить его id:
INSERT INTO processed_events (event_id)
VALUES ('evt_123')
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;
Если RETURNING вернул строку — событие новое, можно обрабатывать.
Если RETURNING пустой — событие уже было, его можно пропустить.
Пример логики:
RETURNING вернул event_id -> обрабатываем событие
RETURNING пустой -> событие уже обработано
Это простой паттерн для идемпотентных воркеров.
Как понять, сколько строк вставилось
На стороне приложения драйвер обычно показывает количество затронутых строк.
Для ON CONFLICT DO NOTHING:
строка вставилась -> rowcount = 1
строка пропущена -> rowcount = 0
Для массовой вставки:
вставилось 7 строк из 10 -> rowcount = 7
Также можно использовать RETURNING, чтобы получить именно вставленные строки:
INSERT INTO users (email, name)
VALUES
('ada@example.com', 'Ada'),
('bob@example.com', 'Bob')
ON CONFLICT (email) DO NOTHING
RETURNING id, email;
В результате будут только новые пользователи.
Практические шаблоны
Вставить пользователя, если email свободен
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada')
ON CONFLICT (email) DO NOTHING;
Вставить строку по primary key
INSERT INTO employees (id, name, dept)
VALUES (1, 'Grace', 'eng')
ON CONFLICT (id) DO NOTHING;
Вставить настройку пользователя
INSERT INTO user_settings (user_id, key, value)
VALUES (1, 'language', 'ru')
ON CONFLICT (user_id, key) DO NOTHING;
Использовать имя ограничения
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada')
ON CONFLICT ON CONSTRAINT users_email_key DO NOTHING;
Массовый сидинг ролей
INSERT INTO roles (id, code, title)
VALUES
(1, 'admin', 'Administrator'),
(2, 'manager', 'Manager'),
(3, 'student', 'Student')
ON CONFLICT (id) DO NOTHING;
Перелить данные из staging
INSERT INTO users (email, name, country)
SELECT
email,
name,
country
FROM staging_users
ON CONFLICT (email) DO NOTHING;
Узнать, вставилась строка или уже была
INSERT INTO processed_events (event_id)
VALUES ('evt_123')
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;
Игнорировать любой уникальный конфликт
INSERT INTO users (id, email, name)
VALUES (1, 'ada@example.com', 'Ada')
ON CONFLICT DO NOTHING;
Этот вариант лучше использовать осторожно, потому что он гасит конфликт по любому уникальному ограничению.
MySQL: INSERT IGNORE
В MySQL ближайший похожий инструмент:
INSERT IGNORE INTO users (email, name)
VALUES ('ada@example.com', 'Ada');
Но это не полный аналог PostgreSQL ON CONFLICT DO NOTHING.
INSERT IGNORE в MySQL может подавлять не только дубликаты, но и некоторые другие ошибки или предупреждения, например проблемы с данными в зависимости от режима SQL.
Поэтому он менее точный.
В PostgreSQL ON CONFLICT DO NOTHING явно говорит:
игнорируй именно конфликт уникальности.
Для обновления при конфликте в MySQL часто используют:
INSERT ... ON DUPLICATE KEY UPDATE
Но это уже аналог upsert, а не DO NOTHING.
ClickHouse
В ClickHouse другая модель.
Обычно ClickHouse не проверяет уникальность при каждой вставке так, как PostgreSQL.
Дедупликация зависит от движка таблицы и настроек.
Например, ReplacingMergeTree может схлопывать версии строк при фоновых merge-процессах, но это происходит не как строгий уникальный constraint в момент INSERT.
То есть PostgreSQL-паттерн:
ON CONFLICT DO NOTHING
в ClickHouse напрямую не переносится.
Если нужна идемпотентность, её обычно проектируют через:
- ключи вставки;
- движок таблицы;
- дедупликацию блоков;
- материализованные представления;
- отдельную логику загрузки;
- периодические merge-процессы.
Поэтому при переносе из PostgreSQL в ClickHouse важно не просто заменить синтаксис, а пересмотреть модель записи.
Частые ошибки
Используют DO NOTHING и ждут обновления
Запрос:
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada New')
ON CONFLICT (email) DO NOTHING;
не обновит name.
Если строка уже есть, она останется как была.
Для обновления нужен:
ON CONFLICT (email) DO UPDATE
Не указали conflict target
Так можно:
ON CONFLICT DO NOTHING
Но это может скрыть конфликт не по тому ограничению, которое вы ожидали.
Лучше явно:
ON CONFLICT (email) DO NOTHING
или:
ON CONFLICT (id) DO NOTHING
Думают, что RETURNING вернёт существующую строку
Не вернёт.
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada')
ON CONFLICT (email) DO NOTHING
RETURNING id;
Если email уже существует, результат будет пустым.
Нет уникального ограничения
Такой запрос требует уникальности на email:
ON CONFLICT (email) DO NOTHING
Если уникального индекса или ограничения нет, PostgreSQL выдаст ошибку.
ON CONFLICT не ищет дубли магически. Ему нужен реальный уникальный механизм в схеме.
Используют DO NOTHING вместо нормальной модели данных
DO NOTHING — полезный инструмент, но он не должен скрывать проблемы.
Если дубли появляются из-за ошибки бизнес-логики, лучше разобраться с причиной.
DO NOTHING хорош для ожидаемых повторов:
- повторный сидер;
- повторный webhook;
- повторная миграция;
- параллельная попытка вставить тот же ключ.
Но если дубли «случайно откуда-то берутся», не стоит просто молча их гасить.
Что важно запомнить
ON CONFLICT DO NOTHING делает INSERT безопасным при конфликте уникальности.
Пример:
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada')
ON CONFLICT (email) DO NOTHING;
Главные правила:
- работает только при конфликте с уникальным индексом, unique constraint или primary key;
- конфликтующая строка пропускается;
- существующая строка не обновляется;
- транзакция не падает из-за ожидаемого дубля;
- в массовой вставке новые строки вставляются, конфликтующие пропускаются;
RETURNING возвращает только реально вставленные строки;
- если строка пропущена,
RETURNING будет пустым;
- без conflict target PostgreSQL гасит любой подходящий уникальный конфликт;
- лучше указывать конкретный target, если важна причина пропуска;
- для обновления при конфликте нужен
ON CONFLICT ... DO UPDATE;
DO NOTHING не глушит ошибки NOT NULL, CHECK или внешних ключей.
Короткий вывод
INSERT ... ON CONFLICT DO NOTHING нужен, когда вставка должна быть повторяемой и безопасной.
Например:
INSERT INTO processed_events (event_id)
VALUES ('evt_123')
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;
Если событие новое — строка вставится.
Если событие уже было — PostgreSQL ничего не сделает и не упадёт.
Главная мысль:
DO NOTHING превращает ожидаемый дубль из ошибки в безопасный пропуск.
Это отличный инструмент для сидеров, миграций, фоновых задач, webhook-обработчиков и любых сценариев, где одна и та же вставка может произойти повторно.
Но используйте его осознанно: указывайте конкретный conflict target, не ждите обновления существующей строки и не забывайте, что RETURNING покажет только реально вставленные данные.
Обычный
INSERTв PostgreSQL падает с ошибкой, если вы пытаетесь вставить строку, которая нарушает уникальность.Например, в таблице
usersесть уникальный email:CREATE TABLE users ( id bigint PRIMARY KEY, email text NOT NULL UNIQUE, name text, country text );Если строка с таким email уже есть, обычный
INSERTупадёт:INSERT INTO users (email, name, country) VALUES ('ada@example.com', 'Ada', 'GB');Ошибка будет примерно такая:
Иногда это нормальное поведение. Но часто нам нужно другое:
Для этого в PostgreSQL есть конструкция:
ON CONFLICT DO NOTHINGОна превращает вставку в безопасную повторяемую операцию.
Что делает ON CONFLICT DO NOTHING
ON CONFLICT DO NOTHINGговорит PostgreSQL:Пример:
INSERT INTO users (id, email, name, country) VALUES (1, 'ada@example.com', 'Ada', 'GB') ON CONFLICT (id) DO NOTHING;Если пользователя с
id = 1ещё нет, строка вставится.Если пользователь с
id = 1уже есть, PostgreSQL не упадёт с ошибкой. Он просто пропустит вставку.Существующая строка при этом не изменится.
То есть
DO NOTHING— это не обновление. Это именно:Зачем это нужно
ON CONFLICT DO NOTHINGчасто используют там, где операция может выполниться несколько раз.Например:
Главная идея — идемпотентность.
Идемпотентная операция — это операция, которую можно выполнить несколько раз, а итоговое состояние останется таким же, как после первого успешного запуска.
Например:
INSERT INTO users (id, email, name, country) VALUES (1, 'ada@example.com', 'Ada', 'GB') ON CONFLICT (id) DO NOTHING;Если выполнить этот запрос 10 раз, после первого запуска таблица уже будет содержать нужную строку. Остальные запуски ничего не изменят и не упадут с ошибкой.
Это очень удобно в деплое: скрипт можно перезапустить после сбоя, и он не начнёт ломаться на уже созданных строках.
Обычный INSERT против ON CONFLICT DO NOTHING
Обычный
INSERT:INSERT INTO users (id, email, name) VALUES (1, 'ada@example.com', 'Ada');Если
id = 1уже существует, команда упадёт.А если это часть транзакции, ошибка может испортить всю транзакцию, и дальше придётся делать
ROLLBACK.С
ON CONFLICT DO NOTHING:INSERT INTO users (id, email, name) VALUES (1, 'ada@example.com', 'Ada') ON CONFLICT (id) DO NOTHING;Если конфликт есть, команда не падает.
Она просто вставит 0 строк.
Важно:
Пример с уникальным email
Допустим, в таблице есть ограничение:
CREATE TABLE users ( id bigserial PRIMARY KEY, email text NOT NULL UNIQUE, name text );Теперь можно сделать безопасную вставку по email:
INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada') ON CONFLICT (email) DO NOTHING;Если email свободен — строка вставится.
Если email уже занят — PostgreSQL ничего не сделает.
Такой запрос можно использовать для создания пользователя, если он ещё не существует.
Но важно понимать: если пользователь уже есть, его имя не обновится.
Например, если в таблице уже лежит:
а вы выполните:
INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada New') ON CONFLICT (email) DO NOTHING;результат останется прежним:
DO NOTHINGне меняет существующую строку.Если нужно обновить существующую строку, это уже другая конструкция:
ON CONFLICT ... DO UPDATEConflict target: какой конфликт игнорируем
В запросе:
ON CONFLICT (email) DO NOTHINGчасть:
называется conflict target.
Она говорит PostgreSQL:
Пример:
INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada') ON CONFLICT (email) DO NOTHING;Чтобы это работало, на
emailдолжен быть уникальный индекс или уникальное ограничение.Например:
CREATE UNIQUE INDEX users_email_idx ON users (email);или:
CREATE TABLE users ( id bigserial PRIMARY KEY, email text NOT NULL UNIQUE, name text );Если уникальности на
emailнет, PostgreSQL не примет такойON CONFLICT.Он не может игнорировать конфликт, которого с точки зрения базы не существует.
Конфликт по PRIMARY KEY
Самый частый вариант — конфликт по первичному ключу.
INSERT INTO employees (id, name, dept) VALUES (1, 'Grace', 'eng') ON CONFLICT (id) DO NOTHING;Если сотрудник с
id = 1уже есть, строка будет пропущена.Это удобно для сидеров и справочников, где id заранее фиксированы.
Например:
INSERT INTO roles (id, code, title) VALUES (1, 'admin', 'Administrator'), (2, 'manager', 'Manager'), (3, 'student', 'Student') ON CONFLICT (id) DO NOTHING;Такой сидер можно запускать повторно.
ON CONSTRAINT: конфликт по имени ограничения
Иногда удобнее указать не список колонок, а имя ограничения.
Например, таблица:
CREATE TABLE users ( id bigserial PRIMARY KEY, email text NOT NULL, name text, CONSTRAINT users_email_key UNIQUE (email) );Можно написать:
INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada') ON CONFLICT ON CONSTRAINT users_email_key DO NOTHING;Это полезно, если:
Например:
ON CONFLICT ON CONSTRAINT users_email_key DO NOTHINGчитается как:
Конфликт по нескольким колонкам
Уникальность может быть составной.
Например, один пользователь не может иметь две настройки с одинаковым ключом:
CREATE TABLE user_settings ( user_id bigint NOT NULL, key text NOT NULL, value text, UNIQUE (user_id, key) );Тогда безопасная вставка выглядит так:
INSERT INTO user_settings (user_id, key, value) VALUES (1, 'language', 'ru') ON CONFLICT (user_id, key) DO NOTHING;Если у пользователя
1уже есть настройкаlanguage, вставка будет пропущена.Если такого сочетания ещё нет, строка вставится.
Важно: конфликт здесь не по
user_idотдельно и не поkeyотдельно, а именно по паре:DO NOTHING без conflict target
PostgreSQL позволяет написать так:
INSERT INTO users (id, email, name) VALUES (1, 'ada@example.com', 'Ada') ON CONFLICT DO NOTHING;Здесь мы не указали, по какому именно ограничению игнорировать конфликт.
В таком виде PostgreSQL проигнорирует конфликт по любому подходящему уникальному ограничению или первичному ключу.
На первый взгляд удобно, но часто опасно.
Допустим, в таблице есть:
Вы вставляете:
INSERT INTO users (id, email, name) VALUES (2, 'ada@example.com', 'Ada Clone') ON CONFLICT DO NOTHING;Если
id = 2новый, ноemail = 'ada@example.com'уже занят, строка тоже будет молча пропущена.Вы можете подумать, что всё хорошо, хотя на самом деле сработал конфликт по email, а не по id.
Поэтому правило:
Лучше:
ON CONFLICT (id) DO NOTHINGили:
ON CONFLICT (email) DO NOTHINGчем слишком широкое:
ON CONFLICT DO NOTHINGRETURNING показывает только вставленные строки
RETURNINGвINSERTвозвращает строки, которые реально были вставлены.Пример:
INSERT INTO orders (id, user_id, amount, status) VALUES (5001, 1, 99.90, 'paid') ON CONFLICT (id) DO NOTHING RETURNING id, status;Если заказа
5001ещё не было, результат будет:Если заказ
5001уже существует, вставка будет пропущена, аRETURNINGвернёт 0 строк.То есть:
Это удобно в приложении.
Можно понять, была ли реально вставка, без отдельного запроса.
DO NOTHING не возвращает существующую строку
Важный момент: если строка уже была,
DO NOTHING RETURNINGне вернёт существующую строку.Например:
INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada') ON CONFLICT (email) DO NOTHING RETURNING id, email, name;Если пользователь уже есть, результат будет пустым.
Не будет так:
Почему?
Потому что
RETURNINGвозвращает только строки, затронутые самимINSERT.А при
DO NOTHINGконфликтующая строка не вставляется и не обновляется.Если нужно получить строку независимо от того, была она вставлена или уже существовала, есть два варианта:
SELECT;ON CONFLICT ... DO UPDATE, если это подходит по смыслу.Массовая вставка с DO NOTHING
ON CONFLICT DO NOTHINGособенно полезен для массовых вставок.Например, сидинг сотрудников:
INSERT INTO employees (id, name, manager_id, dept, salary) VALUES (1, 'Grace', NULL, 'eng', 180000), (2, 'Linus', 1, 'eng', 150000), (3, 'Margaret', 1, 'eng', 150000) ON CONFLICT (id) DO NOTHING;Если часть строк уже есть, они будут пропущены.
Если часть строк новая, они вставятся.
Например:
Команда не падает целиком из-за одного дубля.
Это удобно для справочников, ролей, тарифов, дефолтных настроек и тестовых данных.
Дубликаты внутри одной пачки
Допустим, в одном
INSERTслучайно пришли две строки с одинаковым ключом:INSERT INTO roles (id, code) VALUES (1, 'admin'), (1, 'administrator') ON CONFLICT (id) DO NOTHING;Одна строка может вставиться, а другая будет пропущена из-за конфликта.
Но лучше не полагаться на такие дубликаты как на нормальную бизнес-логику.
Если порядок и выбор «какая строка победит» важны, лучше заранее очистить входные данные.
Например, через
DISTINCT ON,ROW_NUMBERили отдельный staging-этап.DO NOTHING— это защита от дублей, а не инструмент выбора правильной версии данных.INSERT ... SELECT с DO NOTHING
Частый сценарий — перелить данные из staging-таблицы в основную.
Например, есть временная таблица
staging_users:Основная таблица
usersимеет уникальныйemail.Можно вставить только новых пользователей:
INSERT INTO users (email, name, country) SELECT email, name, country FROM staging_users ON CONFLICT (email) DO NOTHING;Если пользователь с таким email уже есть, строка пропускается.
Если email новый, строка вставляется.
Это часто лучше, чем писать:
WHERE NOT EXISTS (...)потому что
ON CONFLICTбезопаснее при параллельных вставках.Почему ON CONFLICT лучше, чем WHERE NOT EXISTS
Иногда пытаются сделать так:
INSERT INTO users (email, name) SELECT 'ada@example.com', 'Ada' WHERE NOT EXISTS ( SELECT 1 FROM users WHERE email = 'ada@example.com' );На первый взгляд логично:
Но при параллельной работе возможна гонка.
Два процесса одновременно проверили:
и оба попытались вставить.
Один вставит, второй упадёт на уникальном ограничении.
ON CONFLICT DO NOTHINGрешает это аккуратнее:INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada') ON CONFLICT (email) DO NOTHING;Уникальный индекс становится источником правды.
Если другой процесс успел вставить такую строку первым, текущий запрос просто пропустит вставку.
Главная мысль:
Что происходит с триггерами
При
ON CONFLICT DO NOTHINGсуществующая строка не обновляется.Поэтому триггеры на
UPDATEне вызываются.Например, не будет логики вида:
Потому что обновления нет.
Но важно понимать: это не значит, что вообще никакие триггеры никогда не сработают. Логика
INSERTи проверка конфликтов имеют свои нюансы, особенно если естьBEFORE INSERT-триггеры.Практическое правило проще:
Если вам нужно обновлять
updated_at, имя, статус или другие поля при конфликте, нужен неDO NOTHING, аDO UPDATE.DO NOTHING не проверяет бизнес-равенство
Допустим, у вас есть пользователь:
Вы выполняете:
INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada New') ON CONFLICT (email) DO NOTHING;PostgreSQL не будет проверять, совпадает ли
name.Он просто увидит конфликт по
emailи пропустит строку.В результате
Ada Oldостанется.Это правильно для
DO NOTHING.Если бизнес-логика говорит:
тогда нужен upsert:
INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada New') ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name;DO NOTHINGподходит только тогда, когда существующая строка должна остаться как есть.DO NOTHING и NOT NULL / CHECK / FOREIGN KEY
ON CONFLICT DO NOTHINGпомогает только с конфликтами уникальности.Он не глушит любые ошибки.
Например, если колонка
emailобязательная:email text NOT NULLто такой запрос всё равно упадёт:
INSERT INTO users (email, name) VALUES (NULL, 'Ada') ON CONFLICT (email) DO NOTHING;Потому что это нарушение
NOT NULL, а не уникальный конфликт.То же самое с
CHECK:salary numeric CHECK (salary > 0)Если вставить отрицательную зарплату,
DO NOTHINGне спасёт.И с внешними ключами тоже:
user_id REFERENCES users(id)Если вы вставляете заказ с несуществующим
user_id, это не конфликт уникальности. Это ошибка внешнего ключа.Главное правило:
Частичная уникальность
В PostgreSQL бывают частичные уникальные индексы.
Например, только один активный промокод с конкретным кодом:
CREATE UNIQUE INDEX promo_codes_active_code_idx ON promo_codes (code) WHERE active;Для таких случаев conflict target может включать условие индекса.
Например:
INSERT INTO promo_codes (code, discount, active) VALUES ('SUMMER', 20, true) ON CONFLICT (code) WHERE active DO NOTHING;Так PostgreSQL понимает, что речь идёт о частичном уникальном индексе.
Это уже более продвинутый сценарий, но он важен:
ON CONFLICTдолжен совпадать с реальным уникальным индексом, включая его условие, если индекс частичный.DO NOTHING и конкурентные вставки
ON CONFLICT DO NOTHINGхорошо работает при параллельных вставках.Например, два воркера одновременно пытаются вставить одно и то же событие:
INSERT INTO processed_events (event_id, processed_at) VALUES ('evt_123', now()) ON CONFLICT (event_id) DO NOTHING;Если
event_idуникальный, один воркер вставит строку, второй пропустит.Оба запроса завершатся без ошибки.
Это полезно для идемпотентной обработки событий:
Уникальное ограничение защищает таблицу от дублей, а
DO NOTHINGзащищает приложение от падения на ожидаемом повторе.Пример: таблица обработанных событий
Допустим, есть таблица:
CREATE TABLE processed_events ( event_id text PRIMARY KEY, processed_at timestamptz NOT NULL DEFAULT now() );Перед обработкой события можно попытаться вставить его id:
INSERT INTO processed_events (event_id) VALUES ('evt_123') ON CONFLICT (event_id) DO NOTHING RETURNING event_id;Если
RETURNINGвернул строку — событие новое, можно обрабатывать.Если
RETURNINGпустой — событие уже было, его можно пропустить.Пример логики:
Это простой паттерн для идемпотентных воркеров.
Как понять, сколько строк вставилось
На стороне приложения драйвер обычно показывает количество затронутых строк.
Для
ON CONFLICT DO NOTHING:Для массовой вставки:
Также можно использовать
RETURNING, чтобы получить именно вставленные строки:INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada'), ('bob@example.com', 'Bob') ON CONFLICT (email) DO NOTHING RETURNING id, email;В результате будут только новые пользователи.
Практические шаблоны
Вставить пользователя, если email свободен
INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada') ON CONFLICT (email) DO NOTHING;Вставить строку по primary key
INSERT INTO employees (id, name, dept) VALUES (1, 'Grace', 'eng') ON CONFLICT (id) DO NOTHING;Вставить настройку пользователя
INSERT INTO user_settings (user_id, key, value) VALUES (1, 'language', 'ru') ON CONFLICT (user_id, key) DO NOTHING;Использовать имя ограничения
INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada') ON CONFLICT ON CONSTRAINT users_email_key DO NOTHING;Массовый сидинг ролей
INSERT INTO roles (id, code, title) VALUES (1, 'admin', 'Administrator'), (2, 'manager', 'Manager'), (3, 'student', 'Student') ON CONFLICT (id) DO NOTHING;Перелить данные из staging
INSERT INTO users (email, name, country) SELECT email, name, country FROM staging_users ON CONFLICT (email) DO NOTHING;Узнать, вставилась строка или уже была
INSERT INTO processed_events (event_id) VALUES ('evt_123') ON CONFLICT (event_id) DO NOTHING RETURNING event_id;Игнорировать любой уникальный конфликт
INSERT INTO users (id, email, name) VALUES (1, 'ada@example.com', 'Ada') ON CONFLICT DO NOTHING;Этот вариант лучше использовать осторожно, потому что он гасит конфликт по любому уникальному ограничению.
MySQL: INSERT IGNORE
В MySQL ближайший похожий инструмент:
INSERT IGNORE INTO users (email, name) VALUES ('ada@example.com', 'Ada');Но это не полный аналог PostgreSQL
ON CONFLICT DO NOTHING.INSERT IGNOREв MySQL может подавлять не только дубликаты, но и некоторые другие ошибки или предупреждения, например проблемы с данными в зависимости от режима SQL.Поэтому он менее точный.
В PostgreSQL
ON CONFLICT DO NOTHINGявно говорит:Для обновления при конфликте в MySQL часто используют:
INSERT ... ON DUPLICATE KEY UPDATEНо это уже аналог upsert, а не
DO NOTHING.ClickHouse
В ClickHouse другая модель.
Обычно ClickHouse не проверяет уникальность при каждой вставке так, как PostgreSQL.
Дедупликация зависит от движка таблицы и настроек.
Например,
ReplacingMergeTreeможет схлопывать версии строк при фоновых merge-процессах, но это происходит не как строгий уникальный constraint в моментINSERT.То есть PostgreSQL-паттерн:
ON CONFLICT DO NOTHINGв ClickHouse напрямую не переносится.
Если нужна идемпотентность, её обычно проектируют через:
Поэтому при переносе из PostgreSQL в ClickHouse важно не просто заменить синтаксис, а пересмотреть модель записи.
Частые ошибки
Используют DO NOTHING и ждут обновления
Запрос:
INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada New') ON CONFLICT (email) DO NOTHING;не обновит
name.Если строка уже есть, она останется как была.
Для обновления нужен:
ON CONFLICT (email) DO UPDATEНе указали conflict target
Так можно:
ON CONFLICT DO NOTHINGНо это может скрыть конфликт не по тому ограничению, которое вы ожидали.
Лучше явно:
ON CONFLICT (email) DO NOTHINGили:
ON CONFLICT (id) DO NOTHINGДумают, что RETURNING вернёт существующую строку
Не вернёт.
INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada') ON CONFLICT (email) DO NOTHING RETURNING id;Если email уже существует, результат будет пустым.
Нет уникального ограничения
Такой запрос требует уникальности на
email:ON CONFLICT (email) DO NOTHINGЕсли уникального индекса или ограничения нет, PostgreSQL выдаст ошибку.
ON CONFLICTне ищет дубли магически. Ему нужен реальный уникальный механизм в схеме.Используют DO NOTHING вместо нормальной модели данных
DO NOTHING— полезный инструмент, но он не должен скрывать проблемы.Если дубли появляются из-за ошибки бизнес-логики, лучше разобраться с причиной.
DO NOTHINGхорош для ожидаемых повторов:Но если дубли «случайно откуда-то берутся», не стоит просто молча их гасить.
Что важно запомнить
ON CONFLICT DO NOTHINGделаетINSERTбезопасным при конфликте уникальности.Пример:
INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada') ON CONFLICT (email) DO NOTHING;Главные правила:
RETURNINGвозвращает только реально вставленные строки;RETURNINGбудет пустым;ON CONFLICT ... DO UPDATE;DO NOTHINGне глушит ошибкиNOT NULL,CHECKили внешних ключей.Короткий вывод
INSERT ... ON CONFLICT DO NOTHINGнужен, когда вставка должна быть повторяемой и безопасной.Например:
INSERT INTO processed_events (event_id) VALUES ('evt_123') ON CONFLICT (event_id) DO NOTHING RETURNING event_id;Если событие новое — строка вставится.
Если событие уже было — PostgreSQL ничего не сделает и не упадёт.
Главная мысль:
Это отличный инструмент для сидеров, миграций, фоновых задач, webhook-обработчиков и любых сценариев, где одна и та же вставка может произойти повторно.
Но используйте его осознанно: указывайте конкретный conflict target, не ждите обновления существующей строки и не забывайте, что
RETURNINGпокажет только реально вставленные данные.