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:本当に?説明が参考になります。
Tony Andrews

15
問題は、挿入と更新の間にウィンドウがあり、別のプロセスが削除を正常に起動できることです。ただし、このパターンは、削除が発生していないテーブルで使用しました。
chotchki

2
はい、同意します。なぜそれが私には明白ではなかったのか分からない。
トニーアンドリュース

4
私はChotchkiに同意しません。「ロック期間:トランザクション内のステートメントによって取得されたすべてのロックは、トランザクションの期間中保持され、ダーティリード、更新の喪失、同時トランザクションからの破壊的なDDL操作などの破壊的な干渉を防ぎます。」
ソース

5
@yohannc:ポイントは、行を挿入しようとして失敗しただけでは、ロックを取得していないということです。
トニーアンドリュース

211

MERGE文は、 2つのテーブル間のデータをマージします。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では、「ログエラー」句を使用して、エラーが発生したときに残りの行を継続させ、問題のある行を停止するだけでなく、別のテーブルに記録することができます。
Tim Sylvester、

1
こんにちは、私は私のクエリで同じクエリパターンを使用しようとしましたが、どういうわけか私のクエリは重複する行を挿入しています。DUALテーブルに関する詳細情報が見つかりません。誰かがDUALの情報やマージ構文についてどこで入手できるか教えてもらえますか?
シェカール

5
@Shekhar Dualは、1行1列のダミーテーブルです。adp
gmbh.ch

7
@TimSylvester-Oracleはトランザクションを使用するため、トランザクション開始時のデータのスナップショットがトランザクション全体で一貫していることを保証し、トランザクション内で行われた変更を保存します。データベースへの同時呼び出しは、取り消しスタックを使用します。したがって、Oracleは、並行トランザクションがいつ開始/完了したかの順序に基づいて最終状態を管理します。したがって、同じSQLコードに対して同時に行われる呼び出しの数に関係なく、挿入の前に制約チェックが行われると、競合状態が発生することはありません。最悪の場合、多くの競合が発生する可能性があり、Oracleが最終状態に到達するまでにさらに長い時間がかかります。
Neo

2
@RandyMagruderその2015年と私たちがOracleで確実にアップサートを行うことができないのは事実ですか?並行安全ソリューションを知っていますか?
dan b

105

上記のPL / SQLでの2つの例は、私が同様のことをしたかったのですばらしいですが、クライアント側にしたかったので、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
ここに戻って、このパターンをもう一度確認しました。同時挿入が試行されると、通知なしで失敗します。1つの挿入が有効になり、2番目のマージでは挿入も更新も行われません。ただし、2つの別々のステートメントを実行するより高速なアプローチは安全です。
Synesso、2011年

3
私のような口頭の初心者は、このデュアルテーブルとは何かを尋ねるかもしれません:stackoverflow.com/q/73751/808698
Hajo Thelen

5
このパターンでは、データ 2回書き込む必要があるのは残念です(John、Smith ...)。この場合、私が使用して何を獲得していないMERGE、と私ははるかに簡単な使用して好むDELETE、その後INSERT
Nicolas Barbulesco 2013年

:この答えは2倍のデータを記述する必要はありません@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);
2019

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 2011年

レコードがすでに存在し、値が同じだったために更新が0行の変更を返した場合
Adriano Varoli Piazza

10
@Adriano:更新によって実際に行のデータが変更されない場合でも、WHERE句が行と一致する場合、sql%rowcountは> 0を返します。
トニーアンドリュース

機能しません:PLS-00207:暗黙カーソルSQLに適用された識別子 'COUNT'は有効なカーソル属性ではありません
Patrik Beck

ここでの構文エラー:(
ilmirons '12 / 11/19

27
  1. 存在しない場合は挿入
  2. 更新:
    
mytableに挿入(id1、t1) 
  デュアルから11、「x1」を選択 
  存在しない場所(mytbleからid1を選択しますWHERE id1 = 11); 

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

26

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が発生する可能性が あります。代わりに、このトランザクション例外のアクセスをシリアル化できません


3
優れた!最後に、同時アクセスは安全な答えです。クライアントから(たとえば、Javaクライアントから)このような構造を使用する方法はありますか?
Sebien 2014

1
ストアドプロシージャを呼び出す必要がないという意味ですか?その場合、特定のJava例外をキャッチして、Javaループで再試行することもできます。OracleのSQLよりもJavaの方がはるかに便利です。
Eugene Beresovsky 14

すみません:私は十分に具体的ではありませんでした。しかし、あなたは正しい方法を理解しました。あなたが言ったように私は辞任した。しかし、SQLクエリやクライアント/サーバーのラウンドトリップが増えるため、100%満足していません。これは、パフォーマンスの点で優れたソリューションではありません。しかし、私の目標は、プロジェクトのJava開発者がメソッドを使用して任意のテーブルにアップサートできるようにすることです(テーブルごとに1つのPLSQLストアドプロシージャ、またはアップサートタイプごとに1つのプロシージャを作成することはできません)。
Sebien 2014

@セビアンそうですね、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

承知しました。ありがとう。修繕。
ハビタス2015

ありがたいことに、あなたはあなたの答えを編集しました!:)私の編集は残念ながら拒否しました stackoverflow.com/review/suggested-edits/7555674
Matteo

9

提案する2つのソリューションに関するメモ:

1)挿入、例外の場合は更新、

または

2)更新、sql%rowcount = 0の場合、挿入

最初に挿入するか更新するかという問題もアプリケーションに依存します。より多くの挿入またはより多くの更新を期待していますか?成功する可能性が最も高いものを最初に実行する必要があります。

間違ったものを選択すると、不必要なインデックスの読み取りが大量に発生します。大したことではありませんが、それでも考慮すべき点があります。


sql%notfoundは私の個人的な好みです
Arturo Hernandez

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は暗黙カーソルを開き、それを使用して対応する挿入をラップします。これにより、挿入はキーが存在しない場合にのみ行われることがわかります。ただし、挿入は独立したコマンドであり、2番目の検索を実行する必要があります。mergeコマンドの内部動作はわかりませんが、コマンドは単一のユニットであるため、Oracleは単一のインデックスルックアップで正しい挿入または更新を実行できた可能性があります。

いくつかのテーブルからデータを取得してテーブルを更新し、場合によっては行を挿入または削除するという処理が必要な場合は、マージの方が良いと思います。ただし、単一行の場合は、構文がより一般的であるため、最初のケースを検討できます。


0

MERGEを使用して、1つのテーブルを別のテーブルにアップサートする例をコピーして貼り付けます。

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. a 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は1つのステートメントでこのタスクを実行できます。」

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残念ですが、これはテーブルへの挿入です。ここには「アップサート」はありません。
トニーアンドリュース
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.