Oracle: как сделать UPSERT (обновить или вставить в таблицу?)

293

Операция UPSERT обновляет или вставляет строку в таблицу, в зависимости от того, есть ли в таблице строка, соответствующая данным:

if table t has a row exists that has key X:
    update t set mystuff... where mykey=X
else
    insert into t mystuff...

Поскольку у Oracle нет конкретного оператора UPSERT, каков наилучший способ сделать это?

Марк Харрисон
источник

Ответы:

60

Альтернатива MERGE («старомодный путь»):

begin
   insert into t (mykey, mystuff) 
      values ('X', 123);
exception
   when dup_val_on_index then
      update t 
      set    mystuff = 123 
      where  mykey = 'X';
end;   
Тони Эндрюс
источник
3
@chotchki: правда? Объяснение было бы полезно.
Тони Эндрюс
15
Проблема в том, что у вас есть окно между вставкой и обновлением, где другой процесс может успешно запустить удаление. Однако я использовал этот шаблон на столе, который никогда не удалял по нему.
чотки
2
Хорошо я согласен. Не знаю, почему это не было очевидно для меня.
Тони Эндрюс
4
Я не согласен с Chotchki. «Длительность блокировки: все блокировки, полученные операторами в транзакции, удерживаются на протяжении транзакции, предотвращая деструктивные помехи, включая грязное чтение, потерянные обновления и деструктивные операции DDL от одновременных транзакций». Souce: ссылка
yohannc
5
@yohannc: Я думаю, дело в том, что мы не получили никаких блокировок, просто пытаясь и не вставляя строку.
Тони Эндрюс
211

Оператор MERGE объединяет данные между двумя таблицами. Использование DUAL позволяет нам использовать эту команду. Обратите внимание, что это не защищено от одновременного доступа.

create or replace
procedure ups(xa number)
as
begin
    merge into mergetest m using dual on (a = xa)
         when not matched then insert (a,b) values (xa,1)
             when matched then update set b = b+1;
end ups;
/
drop table mergetest;
create table mergetest(a number, b number);
call ups(10);
call ups(10);
call ups(20);
select * from mergetest;

A                      B
---------------------- ----------------------
10                     2
20                     1
Марк Харрисон
источник
57
По-видимому, утверждение «слияния с» не является атомарным. Это может привести к «ORA-0001: уникальное ограничение» при одновременном использовании. Проверка на наличие совпадения и вставка новой записи не защищены блокировкой, поэтому возникает состояние гонки. Чтобы сделать это надежно, вам нужно перехватить это исключение и либо выполнить повторное объединение, либо выполнить простое обновление. В Oracle 10 вы можете использовать предложение «log error», чтобы оно продолжало работу с остальными строками при возникновении ошибки и записывало строку с ошибками в другую таблицу, а не просто останавливало ее.
Тим Сильвестр
1
Привет, я пытался использовать тот же шаблон запроса в моем запросе, но каким-то образом мой запрос вставляет повторяющиеся строки. Я не могу найти больше информации о ДВОЙНОЙ таблице. Может кто-нибудь сказать мне, где я могу получить информацию о DUAL, а также о синтаксисе слияния?
Шехар
5
@Shekhar Dual - фиктивная таблица с одной строкой и столбцом adp-gmbh.ch/ora/misc/dual.html
YogoZuno
7
@TimSylvester - Oracle использует транзакции, поэтому гарантирует, что моментальный снимок данных в начале транзакции будет одинаковым на протяжении всей транзакции, за исключением изменений, внесенных в нее. Параллельные вызовы в базу данных используют стек отмены; поэтому Oracle будет управлять конечным состоянием в зависимости от того, когда параллельные транзакции начинаются / завершаются. Таким образом, у вас никогда не будет условия состязания, если перед вставкой выполняется проверка ограничения, независимо от того, сколько одновременных вызовов сделано в одном и том же коде SQL. В худшем случае вы можете получить много разногласий, и Oracle потребуется гораздо больше времени, чтобы достичь окончательного состояния.
Neo
2
@RandyMagruder Это тот случай, когда 2015 год, и мы все еще не можем сделать надежный переход в Oracle! Знаете ли вы о параллельном безопасном решении?
Дан б
105

Вышеприведенный двойной пример, который находится в PL / SQL, был великолепен, потому что я хотел сделать что-то подобное, но я хотел, чтобы это было на стороне клиента ... так вот SQL, который я использовал для отправки аналогичного оператора прямо из некоторого C #

MERGE INTO Employee USING dual ON ( "id"=2097153 )
WHEN MATCHED THEN UPDATE SET "last"="smith" , "name"="john"
WHEN NOT MATCHED THEN INSERT ("id","last","name") 
    VALUES ( 2097153,"smith", "john" )

Однако с точки зрения C # это обеспечивает более медленную скорость, чем выполнение обновления и определение, было ли затронуто строк 0, и выполнение вставки, если это так.

MyDeveloperDay
источник
10
Я вернулся сюда, чтобы снова проверить эту схему. При попытке одновременной вставки происходит сбой. Одна вставка вступает в силу, вторая объединяет ни вставки, ни обновления. Однако более быстрый подход к выполнению двух отдельных операторов безопасен.
Synesso
3
Новички устно, как я, могут спросить, что это за двойная таблица: stackoverflow.com/q/73751/808698
Хаджо Телен
5
Жаль, что с этим шаблоном нам нужно дважды записать данные (Джон, Смит ...). В этом случае, я ничего не выигрываю , используя , и я предпочитаю использовать гораздо проще тогда . MERGEDELETEINSERT
Николас Барбулеско
@NicolasBarbulesco этот ответ не должен записывать данные дважды: stackoverflow.com/a/4015315/8307814
почему
@NicolasBarbulescoMERGE INTO mytable d USING (SELECT 1 id, 'x' name from dual) s ON (d.id = s.id) WHEN MATCHED THEN UPDATE SET d.name = s.name WHEN NOT MATCHED THEN INSERT (id, name) VALUES (s.id, s.name);
почему
46

Еще одна альтернатива без проверки исключений:

UPDATE tablename
    SET val1 = in_val1,
        val2 = in_val2
    WHERE val3 = in_val3;

IF ( sql%rowcount = 0 )
    THEN
    INSERT INTO tablename
        VALUES (in_val1, in_val2, in_val3);
END IF;
Брайан Шмитт
источник
Ваше решение не работает для меня. % Rowcount работает только с явными курсорами?
Synesso
Что, если обновление вернуло 0 измененных строк, поскольку запись уже была там и значения были одинаковыми?
Адриано Вароли Пьяцца
10
@Adriano: sql% rowcount будет по-прежнему возвращать> 0, если предложение WHERE соответствует каким-либо строкам, даже если обновление фактически не изменяет данные в этих строках.
Тони Эндрюс
Не работает: PLS-00207: идентификатор «COUNT», применяемый к неявному курсору SQL, не является допустимым атрибутом курсора
Патрик Бек,
Ошибки синтаксиса здесь :(
ilmirons
27
  1. вставить, если не существует
  2. Обновить:
    
INSERT INTO mytable (id1, t1) 
  ВЫБЕРИТЕ 11, 'x1' ОТ ДВОЙНОГО 
  ГДЕ НЕ СУЩЕСТВУЕТ (ВЫБЕРИТЕ id1 ИЗ mytble WHERE id1 = 11); 

ОБНОВЛЕНИЕ mytable SET t1 = 'x1' ГДЕ id1 = 11;
test1
источник
26

Ни один из ответов, данных до сих пор, не является безопасным перед лицом одновременного доступа , как указано в комментарии Тима Сильвестра, и не вызовет исключений в случае гонок. Чтобы это исправить, комбинация вставки / обновления должна быть заключена в какой-то оператор цикла, чтобы в случае исключения все повторялось.

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

PROCEDURE MyProc (
 ...
) IS
BEGIN
 LOOP
  BEGIN
    MERGE INTO Employee USING dual ON ( "id"=2097153 )
      WHEN MATCHED THEN UPDATE SET "last"="smith" , "name"="john"
      WHEN NOT MATCHED THEN INSERT ("id","last","name") 
        VALUES ( 2097153,"smith", "john" );
    EXIT; -- success? -> exit loop
  EXCEPTION
    WHEN NO_DATA_FOUND THEN -- the entry was concurrently deleted
      NULL; -- exception? -> no op, i.e. continue looping
    WHEN DUP_VAL_ON_INDEX THEN -- an entry was concurrently inserted
      NULL; -- exception? -> no op, i.e. continue looping
  END;
 END LOOP;
END; 

NB. В режиме транзакции SERIALIZABLE, который я не рекомендую, кстати, вы можете столкнуться с ORA-08177: вместо этого нельзя сериализовать доступ для исключений этой транзакции .

Евгений Бересовский
источник
3
Превосходно! Наконец, параллельный доступ к безопасному ответу. Любой способ использовать такую ​​конструкцию из клиента (например, из клиента Java)?
Себьен
1
Вы имеете в виду не вызывать хранимый процесс? Ну, в этом случае вы также можете просто перехватить определенные исключения Java и повторить цикл Java. В Java это намного удобнее, чем в Oracle.
Евгений Бересовский
Я извиняюсь: я не был достаточно конкретен. Но вы правильно поняли. Я подал в отставку, чтобы сделать, как вы только что сказали. Но я не удовлетворен на 100%, потому что он генерирует больше запросов SQL, больше обращений клиента / сервера. Это не хорошее решение с точки зрения производительности. Но моя цель - позволить разработчикам Java моего проекта использовать мой метод для переноса в любую таблицу (я не могу создать одну хранимую процедуру PLSQL для каждой таблицы или одну процедуру для каждого типа upsert).
Себьен
@Sebien Я согласен, было бы лучше, если бы он был инкапсулирован в царство SQL, и я думаю, что вы можете это сделать. Я просто не собираюсь выяснять это для вас ... :) Кроме того, на самом деле эти исключения, вероятно, будут встречаться не чаще одного раза в голубой луне, поэтому вы не должны видеть влияния на производительность в 99,9% случаев. За исключением случаев, когда мы проводим нагрузочное тестирование, конечно ...
Евгений Бересовский
24

Я бы хотел, чтобы Grommit ответил, за исключением того, что он требует двойных значений. Я нашел решение, где оно может появиться один раз: http://forums.devshed.com/showpost.php?p=1182653&postcount=2

MERGE INTO KBS.NUFUS_MUHTARLIK B
USING (
    SELECT '028-01' CILT, '25' SAYFA, '6' KUTUK, '46603404838' MERNIS_NO
    FROM DUAL
) E
ON (B.MERNIS_NO = E.MERNIS_NO)
WHEN MATCHED THEN
    UPDATE SET B.CILT = E.CILT, B.SAYFA = E.SAYFA, B.KUTUK = E.KUTUK
WHEN NOT MATCHED THEN
    INSERT (  CILT,   SAYFA,   KUTUK,   MERNIS_NO)
    VALUES (E.CILT, E.SAYFA, E.KUTUK, E.MERNIS_NO); 
Hubbitus
источник
2
Вы имели в виду INSERT (B.CILT, B.SAYFA, B.KUTUK, B.MERNIS_NO) VALUES (E.CILT, E.SAYFA, E.KUTUK, E.MERNIS_NO); ?
Маттео
Конечно. Спасибо. Исправлена.
Hubbitus
К счастью, вы отредактировали свой ответ! :) мое изменение было, к сожалению, отклонено stackoverflow.com/review/suggested-edits/7555674
Matteo
9

Примечание относительно двух решений, которые предлагают:

1) Вставьте, если исключение, затем обновить,

или

2) Обновить, если sql% rowcount = 0, затем вставить

Вопрос о том, вставлять или обновлять первым, также зависит от приложения. Вы ожидаете больше вставок или больше обновлений? Тот, кто, скорее всего, добьется успеха, должен идти первым.

Если вы выберете неправильный вариант, вы получите кучу ненужных индексов. Не огромная сделка, но все еще кое-что, чтобы рассмотреть.

AnthonyVO
источник
sql% notfound - это мое личное предпочтение
Артуро Эрнандес
8

Я использовал первый пример кода за годы. Обратите внимание не найден, а считать.

UPDATE tablename SET val1 = in_val1, val2 = in_val2
    WHERE val3 = in_val3;
IF ( sql%notfound ) THEN
    INSERT INTO tablename
        VALUES (in_val1, in_val2, in_val3);
END IF;

Код ниже является возможно новым и улучшенным кодом

MERGE INTO tablename USING dual ON ( val3 = in_val3 )
WHEN MATCHED THEN UPDATE SET val1 = in_val1, val2 = in_val2
WHEN NOT MATCHED THEN INSERT 
    VALUES (in_val1, in_val2, in_val3)

В первом примере обновление выполняет поиск по индексу. Это необходимо для того, чтобы обновить правую строку. Oracle открывает неявный курсор, и мы используем его для переноса соответствующей вставки, поэтому мы знаем, что вставка произойдет только тогда, когда ключ не существует. Но вставка является независимой командой, и она должна выполнить второй поиск. Я не знаю внутреннюю работу команды слияния, но так как команда представляет собой единое целое, Oracle может выполнить правильную вставку или обновление с одним поиском по индексу.

Я думаю, что слияние лучше, когда вам нужно выполнить некоторую обработку, то есть взять данные из некоторых таблиц и обновить таблицу, возможно, вставив или удалив строки. Но для случая с одной строкой вы можете рассмотреть первый случай, поскольку синтаксис более распространен.

Артуро Эрнандес
источник
0

Пример копирования и вставки для переноса одной таблицы в другую с помощью MERGE:

CREATE GLOBAL TEMPORARY TABLE t1
    (id VARCHAR2(5) ,
     value VARCHAR2(5),
     value2 VARCHAR2(5)
     )
  ON COMMIT DELETE ROWS;

CREATE GLOBAL TEMPORARY TABLE t2
    (id VARCHAR2(5) ,
     value VARCHAR2(5),
     value2 VARCHAR2(5))
  ON COMMIT DELETE ROWS;
ALTER TABLE t2 ADD CONSTRAINT PK_LKP_MIGRATION_INFO PRIMARY KEY (id);

insert into t1 values ('a','1','1');
insert into t1 values ('b','4','5');
insert into t2 values ('b','2','2');
insert into t2 values ('c','3','3');


merge into t2
using t1
on (t1.id = t2.id) 
when matched then 
  update set t2.value = t1.value,
  t2.value2 = t1.value2
when not matched then
  insert (t2.id, t2.value, t2.value2)  
  values(t1.id, t1.value, t1.value2);

select * from t2

Результат:

  1. б 4 5
  2. с 3 3
  3. 1 1
Bechyňák Petr
источник
-3

Попробуй это,

insert into b_building_property (
  select
    'AREA_IN_COMMON_USE_DOUBLE','Area in Common Use','DOUBLE', null, 9000, 9
  from dual
)
minus
(
  select * from b_building_property where id = 9
)
;
r4bitt
источник
-6

С http://www.praetoriate.com/oracle_tips_upserts.htm :

«В Oracle9i UPSERT может выполнить эту задачу в одном выражении:»

INSERT
FIRST WHEN
   credit_limit >=100000
THEN INTO
   rich_customers
VALUES(cust_id,cust_credit_limit)
   INTO customers
ELSE
   INTO customers SELECT * FROM new_customers;
скоро
источник
14
-1 Типичный дон Берлесон cr @ p Боюсь - это вставка в тот или иной стол, здесь нет «упертости»!
Тони Эндрюс