sqlpostgresqlinsertupsert

INSERT ... ON CONFLICT DO NOTHING en PostgreSQL: inserciones idempotentes sin errores

Como ON CONFLICT DO NOTHING vuelve idempotente un INSERT, en que se diferencia un conflict target del nombre de una restriccion, por que RETURNING calla las filas omitidas y como sembrar datos por lotes.

11 min de lecturaReferencesql · postgresql · insert · upsert · idempotency
Este artículo está actualmente en ruso — la traducción está en curso.

Обычный 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 конфликтующая строка не вставляется и не обновляется.

Если нужно получить строку независимо от того, была она вставлена или уже существовала, есть два варианта:

  1. сделать отдельный SELECT;
  2. использовать 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 покажет только реально вставленные данные.

Practica con ejercicios reales

Resuelve ejercicios en el entrenador de SQL con corrección instantánea y pistas.

Abrir el entrenador