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

239

Операция 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, какой лучший способ сделать это?

Теги:
upsert
merge

12 ответов

33
Лучший ответ

Альтернатива 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;   
  • 4
    Это работает, только если вы никогда не удаляете из таблицы, о которой идет речь.
  • 3
    @chotchki: правда? Объяснение было бы полезно.
Показать ещё 4 комментария
191

оператор 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
  • 50
    По-видимому, утверждение «слияния с» не является атомарным. Это может привести к «ORA-0001: уникальное ограничение» при одновременном использовании. Проверка на наличие совпадения и вставка новой записи не защищены блокировкой, поэтому возникает состояние гонки. Чтобы сделать это надежно, вам нужно перехватить это исключение и либо выполнить повторное объединение, либо выполнить простое обновление. В Oracle 10 вы можете использовать предложение «log errors», чтобы оно продолжало работу с остальными строками при возникновении ошибки и записывало строку с ошибкой в другую таблицу, а не просто останавливало ее.
  • 1
    Привет, я пытался использовать тот же шаблон запроса в моем запросе, но каким-то образом мой запрос вставляет повторяющиеся строки. Я не могу найти больше информации о ДВОЙНОЙ таблице. Может кто-нибудь сказать мне, где я могу получить информацию о DUAL, а также о синтаксисе слияния?
Показать ещё 9 комментариев
95

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

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" )

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

  • 8
    Я вернулся сюда, чтобы снова проверить эту модель. При попытке одновременной вставки происходит сбой. Одна вставка вступает в силу, вторая объединяет ни вставки, ни обновления. Однако более быстрый подход к созданию двух отдельных операторов безопасен.
  • 0
    устные новички, как я, могут спросить, что это за двойная таблица, вот так: stackoverflow.com/q/73751/808698
Показать ещё 1 комментарий
44

Другая альтернатива без проверки исключения:

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;
  • 0
    Ваше решение не работает для меня. % Rowcount работает только с явными курсорами?
  • 0
    Что, если обновление вернуло 0 измененных строк, поскольку запись уже была там и значения были одинаковыми?
Показать ещё 3 комментария
21
  • вставить, если не существует
  • обновление:
    
INSERT INTO mytable (id1, t1) 
  SELECT 11, 'x1' FROM DUAL 
  WHERE NOT EXISTS (SELECT id1 FROM mytble WHERE id1 = 11); 

UPDATE mytable SET t1 = 'x1' WHERE id1 = 11;
19

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

В качестве примера, здесь, как код 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; 

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

  • 3
    Отлично! Наконец, параллельный доступ к безопасному ответу. Любой способ использовать такую конструкцию из клиента (например, из клиента Java)?
  • 1
    Вы имеете в виду не вызывать хранимый процесс? Ну, в этом случае вы также можете просто перехватить определенные исключения Java и повторить цикл Java. В Java это намного удобнее, чем в Oracle.
Показать ещё 2 комментария
15

Я бы хотел, чтобы Громмит ответил, за исключением того, что он требует значений обмана. Я нашел решение, где он может появиться один раз: 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); 
  • 2
    INSERT (B.CILT, B.SAYFA, B.KUTUK, B.MERNIS_NO) VALUES (E.CILT, E.SAYFA, E.KUTUK, E.MERNIS_NO); ?
  • 0
    Конечно. Благодарю. Исправлена.
Показать ещё 1 комментарий
8

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

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

или

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

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

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

  • 0
    sql% notfound - это мое личное предпочтение
6

Я использую первый пример кода в течение многих лет. Заметьте notfound, а не count.

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

Результат:

  • b 4 5
  • c 3 3
  • a 1 1
-1

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

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
)
;
-5

От 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 Боюсь - это вставка в тот или иной стол, здесь нет «упертости»!

Ещё вопросы

Сообщество Overcoder
Наверх
Меню