ChatGPT解决这个技术问题 Extra ChatGPT

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

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 语句,那么最好的方法是什么?


M
Mark Harrison

MERGE statement 合并两个表之间的数据。使用 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

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

上面的 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 表是什么,请参阅:stackoverflow.com/q/73751/808698
太糟糕了,使用这种模式我们需要写入 两次数据(John,Smith...)。在这种情况下,我使用 MERGE 没有任何收获,我更喜欢使用更简单的 DELETE 而不是 INSERT
@NicolasBarbulesco 这个答案不需要两次写入数据:stackoverflow.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);
T
Tony Andrews

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:我认为关键是我们并没有仅仅通过尝试插入一行而未能获得任何锁。
B
Brian Schmitt

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

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,不是合法的游标属性
这里的语法错误:(
s
slavoo

如果不存在则插入更新:

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;

C
Community

正如 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 开发人员使用我的方法在任何表中进行更新插入(我不能为每个表创建一个 PLSQL 存储过程,或者每个更新插入类型一个过程)。
@Sebien我同意,将它封装在SQL领域会更好,我认为你可以做到。我只是没有自愿为你解决这个问题...... :) 另外,实际上这些例外情况在蓝月亮中可能不会发生一次,所以在 99.9% 的情况下你不应该看到对性能的影响。当然,除了在进行负载测试时...
H
Hubbitus

我想要 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); 吗?
谢天谢地,您编辑了答案! :) 我的编辑遗憾地被拒绝了 stackoverflow.com/review/suggested-edits/7555674
A
Arturo Hernandez

多年来我一直在使用第一个代码示例。注意未找到而不是计数。

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 可以通过单个索引查找执行正确的插入或更新。

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


A
AnthonyVO

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

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

或者

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

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

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


A
Andro Selva

尝试这个,

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

A
Anon

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”!