Insert or update. Upsert

Редактировал(а) Alexandr Fokin 2026/05/28 10:57

 

Insert if not exist. Waiting lock

insert on conflict
If not exist
INSERT INTO table1
    (id)
SELECT 1
WHERE
   NOT EXISTS (
       SELECT 1
       FROM table1
       WHERE id = 1
       -- for share
   )
ON CONFLICT DO NOTHING
Избыточная генерация PK.

Конструкция ON CONFLICT не защищает от обращения к генератору ключа (sequence). И это может привести к тому, что счетчик будет крутиться впустую.
Использование выборки с условием вроде не убирает полностью ложные срабатывания (если они проходя одновременно), но после вставки записи, обращения к генератору прекратятся.

PostgreSQL Antipatterns: накручиваем себе проблемы
https://habr.com/ru/companies/tensor/articles/507688/

 Postgres: INSERT if does not exist already
https://stackoverflow.com/questions/4069718/postgres-insert-if-does-not-exist-already
Взаимодействие с блокировкамиПоведение параллельных транзакций (Read Commited):
Параллельные транзакцииselect for updateupdate или delete
Без where existsНе блокируется.Блокируется.
С where exsistsНе блокируетсяНе блокируется
с where exsists и for shareБлокируетсяБлокируется.

Важно подчеркнуть, select for update не всегда блокирует insert on conflict do nothing (как показано в таблице).
В некоторых случаях может потребоваться явное ручное обновление записи для блокировки, чтобы заблокировать параллельные вызовы upsert (чтобы они они дождались завершения текущей транзакции), причем в том числе возможно перед проверкой условий.

  
Merge Не ждет блокировок.
   

Get or insert

WITH insert_cte AS (
 insert into test_table (id)
 values (1)
 on conflict do nothing
)

select *
from insert_cte

union all

select id
from test_table
where id = 1;
  • Не работает:
    • Это union - один statement:
      • select не увидит результат текущего insert.
      • select не увидит результаты параллельных транзакций (если 
  • Без флага.
  • 1 result set.
 
WITH insert_cte AS (
 insert into test_table (id)
 values (1)
 on conflict do nothing
  returning id, true as is_new
)

select *
from insert_cte

union all

select id, false as is_new
from test_table
where id = 1;
  • Не работает:
    • Это union - один statement:
      • select не увидит результаты параллельных транзакций (если текущая транзакция будет ожидать блокировку на insert)
  • Возвращает флаг.
  • 1 result set.
    • Возвращается 1 строка т.к. один statement и мы видим либо только результат insert-returning, либо только результат select.
 
insert into test_table (id)
values (1)
on conflict do nothing
returning id, true as is_new;

select id, false as is_new
from test_table
where id = 1;
  • Работает.
  • Возвращает флаг.
  • 2 result set
    ((нужно делать 2 DbCommand)
    или (multiple result sets)).

NpgsqlBatch

await using (var batch = new NpgsqlBatch(...)
{
   var insertCommand = new NpgsqlBatchCommand(
       $@"
insert into test_table (id) values (1)
on conflict do nothing
returning id, true as is_new;"

        );
   var selectCommand = new NpgsqlBatchCommand(
       $@"
select id, false as is_new
from test_table
where id = 1;"

        );

    batch.BatchCommands.Add(insertCommand);
    batch.BatchCommands.Add(selectCommand);

   await using (var reader = await batch.ExecuteReaderAsync())
    {
       do
        {
           while (await reader.ReadAsync())
            {
               var id = await reader.GetFieldValueAsync<int>("id");
               var isNew = await reader.GetFieldValueAsync<bool>("is_new");
            }
        } while (await reader.NextResultAsync());
    }
   
}
insert into test_table (id)
values (1)
on conflict do nothing;

select id
from test_table
where id = 1;
  • Работает.
  • Флага нет явно, но его можно вычислить через счетчик.
  • 1 result set.
// Флаг можно получить через счетчик
// statement[0] - insert, statement[1] - select.
var isNew = reader.Statements[0].RecordsAffected != 0;
insert into test_table (id)
values (1)
on conflict do nothing;

select id, xmin = pg_current_xact_id()::xid as is_new
from test_table
where id = 1;
  • Работает.
    • Минусы:
      • Вроде не работает, если выше есть хотя бы один savepoint.
  • Возвращает флаг.
  • 1 result set.
 
insert into test_table (id, insert_key)
values (1, @insert_key)
on conflict do nothing;

select id, insert_key
from test_table
where id = 1;
  • Работает.
    • Минус: требует отдельный столбец (insert_key)
    • Плюс: легко встраивается (например там где ORM).
  • Возвращает флаг.
  • 1 result set.
// Вычисляем флаг
var currentInsertKey = Guid.NewGuid();
var insertedRow = await ExecuteInsertQueryAsync(data, currentInsertKey);
var isNew = insertedRow.InsertKey == currentInsertKey;
insert into test_table (id, is_new)
values (1, true)
on conflict (id) do update
SET
    is_new = false
returning id, is_new;
  • Работает.
    • Минус:
      • Требует отдельный столбец (is_new)
      • Избыточная запись на update.
    • Плюс: легко встраивается (например там где ORM).
  • Возвращает флаг.
  • 1 result set.
 
  

Внутренние ссылки:

Дочерние страницы:
Обратные ссылки: