Статьи Утилиты Telegram YouTube VK Видео RuTube Отзывы

MERGE и UPSERT в PostgreSQL

25 февраля 2026

Тэги: PostgreSQL, SQL.

Содержание

  1. Пример UPSERT-логики
  2. UPSERT до Postgres 15
  3. MERGE в Postgres 15
  4. Заглушка для одной из веток условия
  5. MERGE в Postgres 17
  6. MERGE vs INSERT ON CONFLICT

Оператор MERGE появился в Postgres 15, затем в Postgres 17 он был улучшен. До этого момента в PostgreSQL не было полноценной реализации стандартного SQL-оператора MERGE, хотя в Oracle и MS SQL Server он существовал уже много лет. Его добавление заметно облегчило написание UPSERT-скриптов (обновление данных, комбинированное со вставкой).

Пример UPSERT-логики

Предположим, у нас есть таблица с пользователями, которая называется person. В этой таблице есть автоинкрементный id, а также login с ограничением уникальности. Помимо этого есть текстовые поля first_name (имя) и last_name (фамилия):

create table person
(
    id serial constraint person_pk primary key not null,
    login varchar not null
        constraint person_login_uk unique,
    first_name varchar not null,
    last_name  varchar not null
);

Также предположим, что мы хотим организовать синхронизацию пользователей по следующей логике: если пользователь с таким логином уже есть в таблице - нужно обновить эту строку. Если пользователя с таким логином ещё нет - нужно добавить новую запись. По сути это и есть UPSERT.

UPSERT до Postgres 15

До появления оператора MERGE самый популярный способ реализовать эту логику — INSERT ... ON CONFLICT, который появился в PostgreSQL 9.5. Он решает задачу UPSERT, но только по полю с уникальными значениями.

insert into person (login, first_name, last_name)
values ('sidorov', 'Пётр', 'Сидоров')
on conflict (login) do update
set
    first_name = excluded.first_name,
    last_name  = excluded.last_name;

Здесь excluded — это строка, которую пытались вставить. Если login уже занят, вместо ошибки произойдёт UPDATE целевой строки. Если такого логина нет в таблице - произойдёт insert.

MERGE в Postgres 15

Теперь рассмотрим эквивалент с MERGE:

merge into person as d
using (
    values ('sidorov', 'Пётр', 'Сидоров')
) as s(login, first_name, last_name)
on d.login = s.login
when matched then -- есть такой логин в таблице - обновляем
  update set
    first_name = s.first_name,
    last_name  = s.last_name
when not matched then -- логина в таблице нет - добавляем
    insert (login, first_name, last_name)
    values (s.login, s.first_name, s.last_name);

Как видите, он более многословный. Здесь мы для наглядности выбираем алиас s (сокращение от «source») для источника данных, и алиас d («destination») для целевой таблицы person, куда хотим сохранять данные. Источник не обязательно должен быть набором статичных значений - это может быть и выборка из другой таблицы.

Для сравнения мы используем поле login в источнике и приёмнике. А далее у нас имеются две ветки WHEN MATCHED и WHEN NOT MATCHED. Если в таблице-приёмнике уже есть такой же логин, как в источнике - выполняем update. Если такого логина нет - выполняем insert.

Заглушка для одной из веток условия

В некоторых кейсах синхронизации данных нам важно только вставлять новые записи, но при этом не требуется обновлять существующие. В таком случае мы можем использовать выражение DO NOTHING:

insert into person (login, first_name, last_name)
values ('sidorov1', 'Пётр', 'Сидоров')
on conflict (login) do nothing;

То же самое возможно и в случае с MERGE:

merge into person as d
using (
    values ('sidorov', 'Пётр', 'Сидоров')
) as s(login, first_name, last_name)
on d.login = s.login
when matched then -- есть такой логин в таблице - ничего не делаем
    do nothing
when not matched then -- логина в таблице нет - добавляем
    insert (login, first_name, last_name)
    values (s.login, s.first_name, s.last_name);

Аналогичным образом можно «заглушать» и вставку.

MERGE в Postgres 17

Однако многословность оператора MERGE компенсируется его читаемостью и универсальностью. Начиная с Postgres 17, помимо явного обозначения двух равнозначных веток, вы можете использовать выражения NOT MATCHED BY TARGET (нет в таблице-приёмнике - вставляем) и NOT MATCHED BY SOURCE (нет в источнике - удаляем). Это позволяет добавить третью ветку с удалением из таблицы-приёмника:

merge into person as d
using (
    values ('sidorov', 'Пётр', 'Сидоров')
) as s(login, first_name, last_name)
on d.login = s.login
when matched then -- есть такой логин в таблице - обновляем
    update set
    first_name = s.first_name,
    last_name  = s.last_name
when not matched by target then -- логина в таблице нет - добавляем
    insert (login, first_name, last_name)
    values (s.login, s.first_name, s.last_name)
when not matched by source then -- логина в источнике нет - удаляем
    delete;

Поскольку в данном примере в качестве источника у нас фиксированные значения (по сути одна строка), то у нас автоматически удалятся все остальные записи из таблицы. В реальном запросе у вас здесь будет скорее всего select.

MERGE vs INSERT ON CONFLICT

Конструкция INSERT ... ON CONFLICT не позволяет удалять записи. Она всегда сначала пытается вставить. И только если вставка не удалась - обновляет. Побочным эффектом такого поведения является рост значений sequence для автоинкрементного поля, даже если вставка не потребовалась. То есть в таблице значения primary key будут идти не подряд, а с пропусками.

Кроме того, INSERT ... ON CONFLICT является специфичным расширением Postgres, тогда как оператор MERGE входит в стандарт SQL и поддерживается другими популярными СУБД.

При этом для простых однострочных вставок INSERT ... ON CONFLICT будет работать быстрее. Тогда как MERGE позволяет реализовать более сложную логику с несколькими ветками условий.

Отсюда следует вывод:

  • Используйте MERGE, если синхронизируете таблицу целиком, оперируете множеством строк из этой таблицы или если у вас сложная логика синхронизации. Также используйте этот вариант, если вам важна совместимость с другими СУБД.
  • Используйте INSERT ... ON CONFLICT, если вставляете только 1 строку и если вам не требуется удаление.

См. также


Комментарии

Добавить комментарий

×

devmark.ru