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语句,执行此操作的最佳方法是什么?

Answers:


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:真的吗?一个解释会有所帮助。
Tony Andrews

15
问题是您在插入和更新之间有一个窗口,其中另一个进程可以成功触发删除。但是,我确实在从未针对其触发删除操作的表上使用了这种模式。
chotchki 2011年

2
好的我同意。不知道为什么对我来说不那么明显。
托尼·安德鲁斯

4
我不同意Chotchki。“锁定持续时间:事务内的语句所获取的所有锁定都将在事务持续时间内保持,以防止破坏性干扰,包括脏读,丢失的更新以及并发事务中的破坏性DDL操作。” 来源:链接
yohannc 2014年

5
@yohannc:我想指出的是,我们并没有因为尝试插入失败而获得任何锁。
Tony Andrews

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 errors”子句在发生错误时使它在其余行中继续,并将有问题的行记录到另一个表中,而不仅仅是停止。
Tim Sylvester,2009年

1
嗨,我尝试在查询中使用相同的查询模式,但是以某种方式我的查询正在插入重复的行。我找不到有关DUAL表的更多信息。谁能告诉我在哪里可以获得DUAL的信息以及合并语法?
Shekhar

5
@Shekhar Dual是一个虚拟表,具有一个单行和一个列adp-gmbh.ch/ora/misc/dual.html
YogoZuno 2010年

7
@TimSylvester-Oracle使用事务,因此可以确保事务开始时的数据快照在整个事务中保持一致,并保存其中的任何更改。对数据库的并发调用使用撤消堆栈。因此,Oracle将根据并发事务开始/完成的时间顺序来管理最终状态。因此,无论在同一个SQL代码上进行了多少次并发调用,如果在插入之前进行了约束检查,就永远不会有竞争条件。最坏的情况是,您可能会引起很多争论,并且Oracle将花费更长的时间才能达到最终状态。
2012年

2
@RandyMagruder是2015年的情况,我们仍然无法在Oracle中可靠地进行升级!您知道并发安全解决方案吗?
丹b

105

上面的PL / SQL中的双重示例非常棒,因为我想做类似的事情,但是我希望它在客户端...所以这是我用来直接从某些C#发送类似语句的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" )

但是,从C#的角度来看,这比进行更新并查看受影响的行是否为0并进行插入(如果是)要慢。


10
我回到这里再次查看此模式。尝试并发插入时,它会静默失败。一次插入生效,第二次合并既不插入也不更新。但是,执行两个单独的语句的更快方法是安全的。
Synesso,2011年

3
像我这样的口头新手可能会问这个双重表是什么,请参见:stackoverflow.com/q/73751/808698
Hajo Thelen

5
不幸的是,使用这种模式,我们需要写入 两次数据(John,Smith ...)。在这种情况下,使用不会赢得任何收益MERGE,我更喜欢使用,DELETE然后再使用INSERT
Nicolas Barbulesco

@NicolasBarbulesco这个答案不需要两次写入数据:stackoverflow.com/a/4015315/8307814
Whyer

@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);
Whyer

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行已修改,因为记录已经存在并且值相同,该怎么办?
2011年

10
@Adriano:即使WHERE子句与任何行匹配,sql%rowcount仍将返回> 0,即使更新实际上并未更改这些行上的任何数据。
托尼·安德鲁斯

不起作用:PLS-00207:应用于隐式游标SQL的标识符'COUNT'不是合法的游标属性
Patrik Beck 2013年

此处的语法错误:(
ilmirons

27
  1. 如果不存在则插入
  2. 更新:
    
插入mytable(id1,t1) 
  从双选中选择11,“ x1” 
  不存在的地方(从mytble id1 = 11中选择id1); 

在id1 = 11的地方更新mytable SET t1 ='x1';

26

正如蒂姆·西尔维斯特(Tim Sylvester)的评论所指出的那样,面对并发访问,到目前为止给出的答案都不是安全的,并且在比赛中会引发例外情况。为了解决这个问题,必须将insert / update组合包裹在某种循环语句中,以便在发生异常的情况下重试整个过程。

例如,以下是如何将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; 

注意:在事务模式下SERIALIZABLE(我不推荐使用btw),您可能会遇到 ORA-08177:不能序列化对此事务异常的访问


3
优秀的!最后,并发访问安全答案。从客户端(例如从Java客户端)使用这种构造的任何方法吗?
塞比恩

1
您的意思是不必调用存储的proc吗?好吧,在这种情况下,您还可以捕获特定的Java异常并在Java循环中重试。用Java比用Oracle的SQL方便得多。
Eugene Beresovsky

抱歉:我不够具体。但是您了解正确的方法。我辞职是为了像你刚才所说的那样。但是我不是100%满意,因为它会生成更多的SQL查询,更多的客户端/服务器往返。从性能角度来看,这不是一个好的解决方案。但是我的目标是让项目的Java开发人员使用我的方法在任何表中进行向上插入(我不能为每个表创建一个PLSQL存储过程,也不可以为每个向上插入类型创建一个过程)。
Sebien 2014年

@Sebien我同意,将它封装在SQL领域会更好,我认为您可以做到。我只是不是自愿为您解决这个问题... :)另外,实际上,这些例外在蓝色月亮中发生的次数可能少于一次,因此在99.9%的情况下您都不会看到对性能的影响。当然除了进行负载测试时...
Eugene Beresovsky 2014年

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

2
你是说INSERT (B.CILT, B.SAYFA, B.KUTUK, B.MERNIS_NO) VALUES (E.CILT, E.SAYFA, E.KUTUK, E.MERNIS_NO);
Matteo

当然。谢谢。固定。
Hubbitus 2015年

幸好您编辑了答案!:)我的编辑遗憾地拒绝了 stackoverflow.com/review/suggested-edits/7555674
Matteo

9

关于以下两种解决方案的说明:

1)插入,如果有异常,则进行更新,

要么

2)更新,如果sql%rowcount = 0,则插入

首先插入还是更新的问题也取决于应用程序。您是否期望更多插入或更多更新?最有可能成功的人应该先走。

如果您选择了错误的索引,则会得到很多不必要的索引读取。没什么大不了的,但仍然要考虑。


sql%notfound是我的个人喜好
Arturo Hernandez 2015年

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打开了一个隐式游标,并且我们使用它来包装相应的插入,因此我们知道该插入仅在键不存在时才会发生。但是insert是一个独立的命令,它必须进行第二次查找。我不知道merge命令的内部工作原理,但是由于该命令是单个单元,因此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. b 4 5
  2. c 3 3
  3. 1 1

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

-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典型的Don Burleson cr @ p恐怕-这是一张或一张桌子的插入物,这里没有“ upsert”!
托尼·安德鲁斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.