Oracle:如何 UPSERT(更新或插入表?)

Posted

技术标签:

【中文标题】Oracle:如何 UPSERT(更新或插入表?)【英文标题】:Oracle: how to UPSERT (update or insert into a table?) 【发布时间】:2010-09-19 05:39:36 【问题描述】:

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

【问题讨论】:

【参考方案1】:

MERGE statement 合并两个表之间的数据。使用双 允许我们使用这个命令。请注意,这不受并发访问保护。

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

【讨论】:

显然“合并到”语句不是原子的。同时使用时可能导致“ORA-0001:唯一约束”。检查是否存在匹配和插入新记录不受锁保护,因此存在竞争条件。要可靠地执行此操作,您需要捕获此异常并重新运行合并或执行简单的更新。在 Oracle 10 中,您可以使用“记录错误”子句使其在发生错误时继续处理其余行并将有问题的行记录到另一个表中,而不仅仅是停止。 嗨,我尝试在查询中使用相同的查询模式,但不知何故我的查询插入了重复的行。我无法找到有关 DUAL 表的更多信息。谁能告诉我在哪里可以获得 DUAL 的信息以及合并语法的信息? @Shekhar Dual 是一个单行单列的虚拟表 adp-gmbh.ch/ora/misc/dual.html @TimSylvester - Oracle 使用事务,因此保证事务开始时的数据快照在整个事务中保持一致,保存其中所做的任何更改。对数据库的并发调用使用撤消堆栈;因此 Oracle 将根据并发事务开始/完成的顺序来管理最终状态。因此,如果在插入之前完成约束检查,则无论对相同 SQL 代码进行多少并发调用,您将永远不会出现竞争条件。最坏的情况是,您可能会遇到很多争用,而 Oracle 将需要更长的时间才能达到最终状态。 @RandyMagruder 是不是在 2015 年,我们仍然无法在 Oracle 中可靠地进行更新插入!您知道并发安全解决方案吗?【参考方案2】:

上面的 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 以及如果是则执行插入要慢。

【讨论】:

我回到这里再次检查这个模式。当尝试并发插入时,它会静默失败。一次插入生效,第二次合并既不插入也不更新。但是,执行两个单独语句的更快方法是安全的。 像我这样的口腔新手可能会问这个 dual 表是什么,请参阅:***.com/q/73751/808698 太糟糕了,使用这种模式我们需要写入 两次数据(John,Smith...)。在这种情况下,我使用MERGE 没有任何收获,我更喜欢使用更简单的DELETE 然后INSERT @NicolasBarbulesco 这个答案不需要写两次数据:***.com/a/4015315/8307814 @NicolasBarbulesco MERGE 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);【参考方案3】:

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;   

【讨论】:

问题是在插入和更新之间有一个窗口,另一个进程可以成功地触发删除。但是,我确实在从未对它触发过删除的表上使用了这种模式。 好的,我同意。不知道为什么这对我来说并不明显。 我不同意Chotchki。 “锁持续时间:事务中的语句获取的所有锁在事务期间保持,防止破坏性干扰,包括脏读、丢失更新和来自并发事务的破坏性 DDL 操作。”来源:link @yohannc:我认为关键是我们并没有仅仅通过尝试插入行但失败而获得任何锁。【参考方案4】:

没有异常检查的另一种选择:

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 是否仅适用于显式游标? 如果更新返回 0 行已修改,因为记录已经存在并且值相同,该怎么办? @Adriano:如果 WHERE 子句匹配任何行,则 sql%rowcount 仍将返回 > 0,即使更新实际上并未更改这些行上的任何数据。 不起作用:PLS-00207:标识符'COUNT',应用于隐式游标SQL,不是合法的游标属性 这里有语法错误:(【参考方案5】:
    如果不存在则插入 更新:
插入 mytable (id1, t1) 选择 11, 'x1' 从双 不存在的地方(从 mytble 中选择 id1,其中 id1 = 11); 更新 mytable SET t1 = 'x1' WHERE id1 = 11;

【讨论】:

【参考方案6】:

正如 Tim Sylvester 的评论中所指出的,到目前为止,没有一个答案是面对并发访问时是安全的,并且会在出现竞争时引发异常。为了解决这个问题,插入/更新组合必须包含在某种循环语句中,以便在发生异常时重试整个事情。

作为一个例子,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,我不推荐顺便说一句,你可能会遇到 ORA-08177: can't serialize access for this transaction 例外。

【讨论】:

太棒了!最后,一个并发访问安全的答案。有什么方法可以从客户端(例如,从 Java 客户端)使用这种构造? 您的意思是不必调用存储过程?好吧,在这种情况下,您也可以只捕获特定的 Java 异常并在 Java 循环中重试。 Java 比 Oracle 的 SQL 方便得多。 对不起:我不够具体。但你理解正确的方式。我辞职了,照你刚才说的去做。但我不是 100% 满意,因为它会生成更多的 SQL 查询、更多的客户端/服务器往返。就性能而言,这不是一个好的解决方案。但我的目标是让我项目的 Java 开发人员使用我的方法在任何表中进行 upsert(我不能为每个表创建一个 PLSQL 存储过程,或者每个 upsert 类型一个过程)。 @Sebien 我同意,将它封装在 SQL 领域会更好,我认为你可以做到。我只是没有自愿为你解决这个问题...... :) 另外,实际上这些异常可能不会在蓝月亮中发生一次,所以在 99.9% 的情况下你不应该看到对性能的影响。当然,除了在进行负载测试时......【参考方案7】:

我想要 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); 

【讨论】:

您是说INSERT (B.CILT, B.SAYFA, B.KUTUK, B.MERNIS_NO) VALUES (E.CILT, E.SAYFA, E.KUTUK, E.MERNIS_NO); 吗? 感谢您编辑了答案! :) 我的编辑遗憾地拒绝了***.com/review/suggested-edits/7555674【参考方案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 可以通过单个索引查找执行正确的插入或更新。

我认为当您确实需要完成一些处理时,合并会更好,这意味着从某些表中获取数据并更新表,可能会插入或删除行。但是对于单行情况,你可以考虑第一种情况,因为语法更常见。

【讨论】:

【参考方案9】:

关于建议的两种解决方案的说明:

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

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

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

如果你选错了,你会得到一堆不必要的索引读取。没什么大不了的,但仍然需要考虑。

【讨论】:

【参考方案10】:

试试这个,

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

【讨论】:

【参考方案11】:

来自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;

【讨论】:

-1 典型的 Don Burleson cr@p 恐怕 - 这是插入到一个或另一个表中,这里没有“upsert”!

以上是关于Oracle:如何 UPSERT(更新或插入表?)的主要内容,如果未能解决你的问题,请参考以下文章

SQLCE - Upsert(更新或插入) - 如何使用常用方法准备一行?

laravel:更新或创建(更新插入)数据透视表

SQLite UPSERT / 更新或插入

MS Access UPSERT(更新/插入)SQL [重复]

如何在 PostgreSQL 中进行 UPSERT(合并、插入……重复更新)?

提升 Apache Hudi Upsert 性能的三个建议