Oracle MERGE INTO の全て:機能、使い方、注意点

Oracle MERGE INTO の全て:機能、使い方、注意点

はじめに

データベースにおけるデータ操作は、日々の業務において不可欠な要素です。特に、外部からのデータを取り込んだり、既存のデータを別のデータソースと同期させたりする場合、単なるINSERT、UPDATE、DELETEといった個別の操作では非効率になることがあります。例えば、「データが存在すれば更新し、存在しなければ挿入する(UPSERT)」という処理は非常に頻繁に発生しますが、これを従来のDML文で実現しようとすると、まずSELECT文で存在確認を行い、その結果に応じてUPDATE文またはINSERT文を実行するという手続きが必要になります。この方法は複数のSQL文の発行やアプリケーション側のロジックが必要となり、パフォーマンスの低下やコードの複雑化を招く可能性があります。

Oracle Databaseには、このような課題を解決するための強力なSQL文が存在します。それが MERGE INTO 文です。MERGE INTO 文は、データソース(SOURCE)のデータとターゲットテーブル(TARGET)のデータを比較し、一致した場合はターゲットテーブルを更新または削除し、一致しない場合はターゲットテーブルに挿入するという処理を、単一のSQL文で実行できます。

この記事では、Oracleの MERGE INTO 文について、その基本的な機能から詳細な使い方、さらにはパフォーマンスに関する注意点や応用例に至るまで、網羅的に解説します。この記事を読むことで、MERGE INTO 文の強力な機能を理解し、データ処理の効率化とコードの簡潔化を実現できるようになるでしょう。

MERGE INTO文の基本

MERGE INTO 文は、ソースデータに基づいてターゲットテーブルを条件付きで更新または挿入する処理を、効率的に実行するために設計されています。その最も基本的な構文は以下のようになります。

sql
MERGE INTO target_table target_alias
USING source_table source_alias
ON (join_condition)
WHEN MATCHED THEN
UPDATE SET col1 = source_alias.col1, col2 = source_alias.col2, ...
-- [ WHERE update_condition ]
WHEN NOT MATCHED THEN
INSERT (col1, col2, ...) VALUES (source_alias.col1, source_alias.col2, ...);
-- [ WHERE insert_condition ]

この構文を構成する主要な要素について見ていきましょう。

  1. MERGE INTO target_table target_alias:

    • target_table は、操作(更新、削除、挿入)の対象となるテーブルまたはビューです。
    • target_alias は、ターゲットテーブルに付けるエイリアスです。USING 句以降でターゲットテーブルの列を参照する際に使用します。
  2. USING source_table source_alias:

    • source_table は、ターゲットテーブルと比較するデータソースです。テーブル、ビュー、またはインラインビュー(副問い合わせ)を指定できます。
    • source_alias は、データソースに付けるエイリアスです。ON 句および WHEN 句で使用します。
  3. ON (join_condition):

    • この句では、ターゲットテーブル (target_alias) とデータソース (source_alias) の間で、どの行が一致するかを判断するための結合条件を指定します。通常は主キーや一意キーなどの識別子を使用します。
    • この条件が真 (TRUE) となる行のペアが「一致する行」と判断されます。
  4. WHEN MATCHED THEN:

    • ON 句で指定した条件を満たす、つまりソースデータの行とターゲットテーブルの行が一致した場合に実行するアクションを指定します。
    • 主要なアクションは UPDATE または DELETE です。
    • UPDATE アクションでは、SET 句を使用してターゲットテーブルの列を更新します。ソースデータの列の値を使用して更新するのが一般的です。
    • DELETE アクションでは、一致したターゲット行を削除します。Oracle Database 10g Release 1 (10.1) から利用可能になりました。
    • 必要に応じて WHERE update_condition を追加することで、「一致はしたが、さらに特定の条件を満たす場合にのみ更新または削除する」という制御が可能です。この WHERE 条件はターゲットテーブルの列を使用して指定します。
  5. WHEN NOT MATCHED THEN:

    • ON 句で指定した条件を満たさない、つまりソースデータの行に対応する行がターゲットテーブルに存在しない場合に実行するアクションを指定します。
    • 実行できるアクションは INSERT のみです。
    • INSERT アクションでは、ターゲットテーブルに新しい行を挿入します。通常、ソースデータの列の値を使用して挿入します。
    • 必要に応じて WHERE insert_condition を追加することで、「一致はしなかったが、さらに特定の条件を満たす場合にのみ挿入する」という制御が可能です。この WHERE 条件はデータソース(USING句)の列を使用して指定します。

MERGE INTO 文の動作原理

MERGE INTO 文が実行される際、Oracleは基本的に以下のステップを踏みます。

  1. ソースデータの読み込み: USING 句で指定されたデータソースから行を読み込みます。
  2. ターゲットデータとの比較: 読み込んだソースデータ行ごとに、ON 句で指定された条件を使ってターゲットテーブル内で一致する行を探します。
  3. 一致した場合 (WHEN MATCHED):
    • 一致する行が見つかった場合、WHEN MATCHED THEN 句に指定されたアクション(UPDATE または DELETE)を実行します。
    • WHERE update_condition が指定されている場合、その条件も評価し、真となる場合にのみアクションを実行します。
    • 注意: 一つのソース行に対して、ターゲットテーブルで一致する行が複数見つかった場合(ON 句の条件がターゲットテーブルの一意性を保証しない場合)、MERGE 文はエラー(ORA-30926: Unable to get a stable set of rows in the source tables.)になる可能性があります。後述の「注意点と制限事項」で詳しく説明します。
  4. 一致しない場合 (WHEN NOT MATCHED):
    • 一致する行が見つからなかった場合、WHEN NOT MATCHED THEN 句に指定されたアクション(INSERT)を実行します。
    • WHERE insert_condition が指定されている場合、その条件も評価し、真となる場合にのみアクションを実行します。
    • 注意: ソースデータの同一の行が複数存在する場合(USING 句のデータソース自体に重複がある場合)、それぞれのソース行に対してターゲットテーブルに一致する行がなければ、同じ行が複数回挿入されようとする可能性があります。これにより、ターゲットテーブルの一意制約違反が発生する可能性があります。データソースは一意であるか、またはターゲットテーブルの一意制約がINSERTされる列に対して定義されていないことを確認する必要があります。

この単一パスでの処理により、MERGE INTO 文は複数のDML文を組み合わせるよりも効率的にUPSERT処理を実行できるのです。

MERGE INTO文の詳細な機能

次に、MERGE INTO文の各句について、より詳細な機能と使い方を見ていきます。

TARGET TABLE句

MERGE INTO target_table target_alias

  • 対象オブジェクト: テーブルだけでなく、更新可能なビューもターゲットとして指定できます。ビューに対してMERGE文を実行する場合、そのビューが直接基になるテーブルに対して更新可能であるか、またはINSTEAD OFトリガーが定義されている必要があります。インラインビューをターゲットにすることはできません(MERGE INTO (SELECT ...)のような構文は無効です)。
  • エイリアス: ターゲットテーブルには必ずエイリアスを付ける必要があります。このエイリアスはON句やWHEN MATCHED THEN句のWHERE条件でターゲットテーブルの列を参照する際に使用します。

USING句

USING source_table source_alias

  • 対象オブジェクト: ソースデータとして指定できるのは、テーブル、ビュー、または副問い合わせ(インラインビュー)です。最も柔軟性の高い部分であり、複雑な条件でフィルタリングや結合を行った結果セットをソースとして使用できます。
    “`sql
    — テーブルをソースとして使用
    USING source_data sd

    — ビューをソースとして使用
    USING source_view sv

    — 副問い合わせ(インラインビュー)をソースとして使用
    USING (SELECT col1, col2 FROM another_table WHERE status = ‘READY’) sq
    ``
    * **DUP_VALUE_ON_INDEXヒント**: ソースデータに重複があり、それがターゲットテーブルの一意制約に関わる列である場合、
    INSERT時に一意制約違反が発生する可能性があります。Oracle 10g Release 2以降では、/+ DUP_VALUE_ON_INDEX(target_alias, index_name) /ヒントをUSING句の直後に指定することで、重複したソース行による一意制約違反エラーを回避し、違反を発生させた行を無視して処理を続行させることができます。ただし、このヒントはWHEN NOT MATCHED THEN INSERT句にのみ影響し、WHEN MATCHED THEN UPDATE/DELETE` には影響しません。また、このヒントはあくまでエラーを回避するものであり、データソースの重複自体を解決するものではないことに注意が必要です。

    sql
    MERGE INTO target_table tt
    USING /*+ DUP_VALUE_ON_INDEX(tt, target_pk_index) */
    (SELECT col1, col2 FROM source_data) sd
    ON (tt.pk_col = sd.col1)
    WHEN NOT MATCHED THEN
    INSERT (pk_col, data_col) VALUES (sd.col1, sd.col2); -- ここで一意制約違反が発生しうる

    この例では、source_datapk_colの値が重複している行が存在する場合でも、target_pk_indexに対する一意制約違反によるエラーを回避し、最初の行だけが挿入される(またはどちらかが挿入されてもう一方が無視される)動作になります。どの行が無視されるかの順序は保証されません。

ON句

ON (join_condition)

  • 結合条件: ターゲットテーブルの行とソースデータの行をどのように関連付けるかを指定します。等価結合(=)が一般的ですが、他の結合条件(><など)や複数の条件をANDORで組み合わせることも可能です。
    “`sql
    — 主キーによる結合
    ON (tt.employee_id = sd.emp_id)

    — 複数列による結合
    ON (tt.order_id = sd.order_id AND tt.item_id = sd.item_id)

    — 複雑な条件
    ON (tt.customer_id = sd.cust_id AND tt.order_date >= sd.from_date AND tt.order_date < sd.to_date)
    ``
    * **パフォーマンス**:
    ON句の結合条件は、MERGE文のパフォーマンスに最も大きく影響する要素の一つです。Oracleは、この条件を満たす行を効率的に見つけるために、ターゲットテーブルの索引を利用しようとします。通常、ON句の結合列にターゲットテーブルの索引(特に一意索引や主キー索引)が存在する場合、パフォーマンスが向上します。索引が存在しない場合、フルテーブルスキャンが発生する可能性があります。
    * **ORA-30926**: 前述したように、
    ON句の条件に対して、一つのソース行に対してターゲットテーブルで複数の行が一致する場合、ORA-30926エラーが発生する可能性があります。これは、Oracleが「どのターゲット行を更新/削除すべきか安定して判断できない」ために発生します。この問題を回避するには、ON句の条件が一意性を保証するように設計するか、またはソースデータやターゲットテーブルの構造を見直す必要があります。例えば、ソースデータが複数の行で同じキーを持っている場合、USING句の副問い合わせでGROUP BY`などを使用してソースデータを一意化する、といった対策が考えられます。

WHEN MATCHED THEN句

WHEN MATCHED THEN action [ WHERE update_condition ]

ON句で一致したソース行とターゲット行に対して実行されるアクションを指定します。

  • UPDATE アクション:
    • 一致したターゲット行の列を更新します。
    • SET 句で複数の列を同時に更新できます。
    • 通常、ソースデータの列の値を使用してターゲットテーブルの列を更新しますが、定数や式を使用することも可能です。
      sql
      WHEN MATCHED THEN
      UPDATE SET
      tt.name = sd.name,
      tt.salary = sd.new_salary,
      tt.last_update_date = SYSDATE
  • DELETE アクション:
    • 一致したターゲット行を削除します。
    • WHEN MATCHED THEN DELETE のように単独で指定します。
      sql
      WHEN MATCHED THEN
      DELETE
  • WHERE update_condition:

    • 一致した行の中から、さらに特定の条件を満たす行に対してのみUPDATEまたはDELETEを実行したい場合に指定します。
    • この条件は、一致したターゲット行の列、または一致したソース行の列を使用して記述できます。
      “`sql
      — 一致したが、給与が変わっている場合のみ更新
      WHEN MATCHED THEN
      UPDATE SET tt.salary = sd.new_salary
      WHERE tt.salary != sd.new_salary

    — 一致したが、ステータスが’CLOSED’の場合のみ削除
    WHEN MATCHED THEN
    DELETE
    WHERE tt.status = ‘CLOSED’

    — 一致したが、ソース側のフラグが’D’の場合のみ削除
    WHEN MATCHED THEN
    DELETE
    WHERE sd.action_flag = ‘D’
    * `WHERE` 条件は、`UPDATE`または`DELETE`アクションの後に指定する必要があり、`WHEN MATCHED THEN WHERE ... UPDATE ...` のような順序は無効です。
    * **複数のアクション**: 一つの`WHEN MATCHED THEN`句の中で、`UPDATE`と`DELETE`の両方を指定することはできません。また、異なる条件で`WHEN MATCHED THEN`句を複数記述することもできません。例えば、「一致したら、特定の条件で更新し、別の条件で削除する」といった処理は、単一の`MERGE`文では直接実現できません。このような場合は、`WHEN MATCHED THEN` 句内の `WHERE` 条件で`UPDATE`と`DELETE`を使い分けるか、あるいは複数回の`MERGE`文や他のDML文を組み合わせる必要があります。
    sql
    — このような構文は無効
    — WHEN MATCHED THEN UPDATE SET … WHERE …
    — WHEN MATCHED THEN DELETE WHERE …

    — 代替策1: WHERE条件で分ける (UPDATE/DELETEは選択式になる)
    WHEN MATCHED THEN
    UPDATE SET …
    WHERE condition_for_update — condition_for_updateがTRUEならUPDATE

    — 代替策2: 別のMERGE文を使用するか、DELETE文を別途実行する
    — MERGE INTO … WHEN MATCHED THEN UPDATE …
    — MERGE INTO … WHEN MATCHED THEN DELETE … (または DELETE FROM target_table WHERE …)
    ``
    ただし、
    WHEN MATCHED THEN UPDATE SET … WHERE …WHEN NOT MATCHED THEN INSERT … WHERE …のように、WHEN MATCHEDWHEN NOT MATCHEDの両方の句にWHERE`条件を付けることは可能です。

WHEN NOT MATCHED THEN句

WHEN NOT MATCHED THEN action [ WHERE insert_condition ]

ON句で一致する行がターゲットテーブルに見つからなかったソース行に対して実行されるアクションを指定します。

  • INSERT アクション:
    • ターゲットテーブルに新しい行を挿入します。
    • INSERT (column_list) VALUES (value_list) の構文を使用します。
    • column_list は挿入する列のリスト、value_list は対応する値のリストです。通常、value_listではソースデータの列を参照します。
      sql
      WHEN NOT MATCHED THEN
      INSERT (employee_id, name, hire_date)
      VALUES (sd.emp_id, sd.name, SYSDATE)
  • WHERE insert_condition:
    • 一致しなかったソース行の中から、さらに特定の条件を満たす行に対してのみINSERTを実行したい場合に指定します。
    • この条件は、ソース行の列を使用して記述する必要があります。ターゲットテーブルの列はまだ存在しない(または挿入前の状態である)ため参照できません。
      sql
      -- 一致しなかったが、ソース側のステータスが'NEW'の場合のみ挿入
      WHEN NOT MATCHED THEN
      INSERT (col1, col2) VALUES (sd.col1, sd.col2)
      WHERE sd.status = 'NEW'
    • WHERE 条件は、INSERTアクションの後に指定する必要があり、WHEN NOT MATCHED THEN WHERE ... INSERT ... のような順序は無効です。
  • 複数のアクション: WHEN NOT MATCHED THEN 句で指定できるアクションはINSERTのみです。

複数のWHEN句の組み合わせ

MERGE文は、WHEN MATCHED THEN句とWHEN NOT MATCHED THEN句の両方を持つことができます。また、それぞれの句にオプションでWHERE条件を付けることができます。

sql
MERGE INTO target_table tt
USING source_table sd
ON (tt.id = sd.id)
WHEN MATCHED THEN -- ソースのIDがターゲットに存在する場合
UPDATE SET
tt.value = sd.value,
tt.update_count = tt.update_count + 1
WHERE tt.value != sd.value -- ただし、値が変更されている場合のみ更新
WHEN NOT MATCHED THEN -- ソースのIDがターゲットに存在しない場合
INSERT (id, value, create_date, update_count)
VALUES (sd.id, sd.value, SYSDATE, 1)
WHERE sd.status = 'ACTIVE'; -- ただし、ソースのステータスが'ACTIVE'の場合のみ挿入

この例では、
1. ソース行のIDに対応する行がターゲットテーブルに存在する場合:
* ターゲット行の値がソース行の値と異なる場合に限り、ターゲット行のvalue列とupdate_count列を更新します。
2. ソース行のIDに対応する行がターゲットテーブルに存在しない場合:
* ソース行のステータスがACTIVEである場合に限り、新しい行をターゲットテーブルに挿入します。

このように、MERGE文は柔軟な条件に基づいて、一致/不一致それぞれのケースで異なる処理を指定できます。

RETURNING句

MERGE文は、DML文と同様にRETURNING句を持つことができます。これにより、MERGE操作によって変更(挿入、更新、削除)された行の列の値を取得できます。

sql
MERGE INTO target_table tt
USING source_table sd
ON (tt.id = sd.id)
WHEN MATCHED THEN
UPDATE SET tt.value = sd.value
WHERE tt.value != sd.value
WHEN NOT MATCHED THEN
INSERT (id, value) VALUES (sd.id, sd.value)
RETURNING tt.id, tt.value INTO v_ids, v_values; -- PL/SQL変数に取得する場合

  • 構文: RETURNING column_list INTO variable_list;
    • column_list は取得したい列のリストです。ターゲットテーブル(target_alias)の列を指定します。ROWIDを取得することも可能です。
    • variable_list は取得した値を格納する変数またはコレクションのリストです。通常、PL/SQLブロック内で使用します。
  • 使用例:
    • 単一行のMERGE(例えば、ON句の条件が一意のキーであり、ソースデータが単一行の場合)では、スカラー変数に値を返すことができます。
    • 複数行のMERGEでは、BULK COLLECT INTO句と組み合わせて、ネストしたテーブルなどのコレクション変数に値を返すのが一般的です。
      “`sql
      DECLARE
      TYPE id_tab IS TABLE OF target_table.id%TYPE;
      TYPE value_tab IS TABLE OF target_table.value%TYPE;
      v_ids id_tab;
      v_values value_tab;
      BEGIN
      MERGE INTO target_table tt
      USING source_table sd
      ON (tt.id = sd.id)
      WHEN MATCHED THEN
      UPDATE SET tt.value = sd.value
      WHERE tt.value != sd.value
      WHEN NOT MATCHED THEN
      INSERT (id, value) VALUES (sd.id, sd.value)
      RETURNING tt.id, tt.value BULK COLLECT INTO v_ids, v_values;

      — 取得した値を処理
      FOR i IN 1..v_ids.COUNT LOOP
      DBMS_OUTPUT.PUT_LINE(‘Processed ID: ‘ || v_ids(i) || ‘, Value: ‘ || v_values(i));
      END LOOP;
      END;
      /
      ``
      * **制限事項**:
      *
      RETURNING句は、MERGE文がPL/SQLブロック内から実行される場合に最も有用です。SQL*Plusなどのクライアントからは直接利用できない場合があります(クライアントツールによっては結果セットとして表示するものもあります)。
      *
      WHEN MATCHED THEN DELETEアクションによって削除された行の値も取得できます。
      *
      WHEN NOT MATCHED THEN INSERTアクションによって挿入された行の値も取得できます。
      * ただし、
      UPDATEされた行とINSERTされた行を区別して取得することは、単一のRETURNING句では困難です。それぞれの操作に対して個別のRETURNING`句を指定することはできません。取得されるのは、最終的にターゲットテーブルに変更が適用された(更新または挿入された)行の情報のみです。削除された行の情報も取得可能ですが、どの操作で変更されたかは区別されません。

パフォーマンスとチューニング

MERGE INTO文は効率的なデータ操作を可能にしますが、大量データを処理する場合や複雑な条件を使用する場合は、パフォーマンスの考慮とチューニングが重要になります。

  1. ON句でのインデックス利用:
    • 前述の通り、ON句の結合条件で使用されるターゲットテーブルの列には、適切なインデックス(特に一意索引または主キー索引)が存在することが非常に重要です。これにより、Oracleはソース行に対応するターゲット行を高速に検索できます。
    • インデックスがない場合、ターゲットテーブルのフルスキャンが発生し、パフォーマンスが著しく低下する可能性があります。
    • 複合インデックスの場合、ON句の条件がインデックスの先行列をすべて含んでいるか確認します。
  2. USING句のデータソース:
    • USING句のデータソースの効率もパフォーマンスに影響します。データソースが複雑な副問い合わせや結合を含む場合、その副問い合わせ自体が効率的に実行される必要があります。
    • ソースデータに重複が多い場合、ORA-30926エラーの回避策を検討するだけでなく、ソースデータの重複を事前に排除するなど、データクレンジングのステップを考慮することも有効です。
    • ソースデータを事前にソートすることで、ターゲットテーブルの物理的な格納順序に近い順序で処理が進み、パフォーマンスが向上する場合があります。ただし、Oracleのオプティマイザが自動的に最適なアクセスパスを選択するため、必ずしもソートが有効とは限りません。実行計画を確認して判断する必要があります。
  3. 並列処理:
    • 大規模なMERGE操作の場合、PARALLELヒントを使用して並列処理を有効にすることができます。
      sql
      MERGE /*+ PARALLEL(tt, degree) */ INTO target_table tt
      USING source_table sd
      ON ...
      WHEN MATCHED THEN ...
      WHEN NOT MATCHED THEN ...
    • degreeには並列度を指定します。ターゲットテーブルやソースデータが大きい場合に効果的ですが、システムリソースを消費するため、利用には注意が必要です。
  4. UNDO/REDOログ:
    • MERGE文は、INSERTUPDATEDELETE操作の組み合わせであるため、変更されるデータ量に応じて大量のUNDOログとREDOログを生成します。
    • 特に、UPDATEで大量の行が更新される場合や、INSERTで大量の新しい行が挿入される場合、これらのログ生成がボトルネックになることがあります。
    • NOLOGGINGオプションが可能な場合は、テーブルに対してNOLOGGING属性を設定することでREDOログの生成量を削減できますが、リカバリに関するリスク(ポイントインタイムリカバリ不可など)を伴います。通常は利用を推奨されません。
    • 一時テーブルに対するMERGEであれば、UNDO/REDOログの生成量は抑えられます。
  5. バルクバインディング:
    • PL/SQLからMERGE文を実行する際、大量のデータをまとめて処理する場合は、FORALL文とINSERT/UPDATE/DELETE文の組み合わせではなく、MERGE文を一度だけ実行する方が効率的です。MERGE文自体が内部的にバルク処理に最適化されているからです。
  6. 実行計画の確認:
    • MERGE文のパフォーマンス問題が発生した場合、EXPLAIN PLAN FOR 文を使用して実行計画を確認することが不可欠です。どのアクセスパスが使用されているか(フルスキャンかインデックススキャンか)、どのJOINメソッドが使用されているかなどを分析し、ボトルネックとなっている部分を特定します。
    • V$SQL_PLANV$SQL_PLAN_STATISTICS_ALLビューなどの動的パフォーマンスビューも、実行時の詳細な統計情報を提供するため、パフォーマンス分析に役立ちます。
  7. 統計情報の重要性:
    • Oracleのオプティマイザが最適な実行計画を選択するためには、ターゲットテーブルとソースデータ(テーブルやビュー)の統計情報が最新かつ正確であることが重要です。定期的にDBMS_STATS.GATHER_TABLE_STATSなどで統計情報を収集してください。

注意点と制限事項

MERGE INTO文を使用する際に注意すべき点や、知っておくべき制限事項がいくつかあります。

  1. ターゲットテーブルへの制約:
    • MERGE文によって挿入または更新される行は、ターゲットテーブルに定義されているすべての制約(主キー制約、一意制約、NOT NULL制約、チェック制約、外部キー制約など)を満たす必要があります。違反が発生した場合、MERGE文全体がロールバックされます。
    • 特にWHEN NOT MATCHED THEN INSERT句において、ターゲットテーブルの主キーや一意キーに対応する列の値がソースデータ内で重複している場合、一意制約違反エラーが発生します。前述のDUP_VALUE_ON_INDEXヒントはこれを緩和できますが、根本的な解決にはソースデータのクレンジングが必要です。
  2. トリガーの発火:
    • MERGE文によって影響を受けた行に対して、対応するトリガー(BEFORE/AFTER INSERT/UPDATE/DELETE行トリガー、ステートメントトリガー)が発火します。
    • トリガー内でエラーが発生した場合、MERGE文全体がロールバックされます。
    • トリガー内で同じテーブルに対してさらにDML操作を行う場合(変異表を引き起こす可能性)、注意が必要です。
  3. ORA-30926: unable to get a stable set of rows in the source tables:
    • このエラーは、一つのソース行に対してターゲットテーブルで複数の行がON句の条件に一致した場合に発生します。Oracleがどのターゲット行を更新/削除すべきか決定できないためです。
    • 原因の多くは、ON句の結合条件がターゲットテーブルの一意性を保証していないことです。
    • 解決策としては、
      • ON句の条件に使用するターゲットテーブルの列に一意制約または主キー制約があるか確認する。
      • もし一意性が保証されないカラムで結合する必要があるなら、ON句の条件にターゲットテーブルの一意性を保証する他のカラム(例えばROWIDなど、ただしROWIDは非推奨の場合が多い)を追加するか、あるいはUSING句のソースデータを加工して重複を排除する(例:GROUP BYや副問い合わせで集計・フィルタリングする)などの方法を検討します。
      • ソースデータ自体に重複がある場合も、ターゲットテーブルとの一致が不安定になる原因となり得ます。ソースデータ側でも一意性を考慮する必要があります。
  4. ビューに対するMERGE:
    • ビューをターゲットテーブルとして指定する場合、そのビューが直接基となるテーブルに対して更新可能であるか、またはINSTEAD OFトリガーが定義されている必要があります。複雑な結合や集計を含むビューは通常、直接更新できません。
    • INSTEAD OF MERGEトリガーを定義することで、任意の複雑なビューに対するMERGE操作をカスタマイズして実装できますが、トリガーの実装が複雑になる可能性があります。
  5. マテリアライズドビューに対するMERGE:
    • 更新可能なマテリアライズドビューに対してMERGEを実行することは可能です。
    • ただし、マテリアライズドビューの更新方法(ON COMMITON DEMANDなど)や属性(REFRESH FASTREFRESH COMPLETE)によっては、MERGE操作の振る舞いや性能が異なります。
  6. 同一テーブルに対するMERGE(自己MERGE):
    • MERGE INTO target_table USING target_table ON (...) WHEN MATCHED THEN ... のように、ターゲットテーブルとソーステーブルが同一である自己MERGEは非常に注意が必要です。
    • MERGE文が実行されている最中に、同じテーブルに対して読み書きが行われるため、結果が予測不能になったり、ORA-30926エラーが発生したりするリスクが非常に高いです。
    • 自己MERGEが必要なシナリオ(例:テーブル内の異なる行を結合して更新するなど)では、一時テーブルを使用する、ROWIDを利用して結合する(ただしROWIDは非推奨)、または他のDML文やPL/SQLの手続きで処理するなどの代替手段を検討すべきです。
  7. 複数のMERGE文が必要なケース:
    • 前述のように、一つのMERGE文では、一致した場合にUPDATEまたはDELETE(およびオプションのWHERE条件)、一致しなかった場合にINSERT(およびオプションのWHERE条件)という組み合わせしか指定できません。
    • 「一致したら、特定の条件で更新し、別の条件で削除し、一致しなかったら特定の条件で挿入する」のように、より複雑なロジック(例えば、一致した場合の異なる条件での複数のアクション、または一致しなかった場合の複数のアクション)が必要な場合は、単一のMERGE文では実現できません。
    • このような場合は、複数のMERGE文を使用するか、あるいはINSERTUPDATEDELETE文を組み合わせたPL/SQL手続きなどで処理する必要があります。

MERGE INTO文の応用例

MERGE INTO文は様々なシナリオで活用できます。いくつかの典型的な応用例を見てみましょう。

  1. マスターデータの同期:
    • システム間のマスターデータ(顧客情報、商品情報など)を定期的に同期する際によく使用されます。外部システムからのデータファイルやステージングテーブルのデータをソースとして、メインシステムのマスタテーブルを更新します。
      sql
      -- 顧客マスタの同期
      MERGE INTO customers c
      USING external_customer_data ec
      ON (c.customer_id = ec.customer_id)
      WHEN MATCHED THEN
      UPDATE SET
      c.customer_name = ec.customer_name,
      c.address = ec.address,
      c.phone = ec.phone,
      c.last_sync_date = SYSDATE
      WHERE c.customer_name != ec.customer_name
      OR c.address != ec.address
      OR c.phone != ec.phone -- データが変更されている場合のみ更新
      WHEN NOT MATCHED THEN
      INSERT (customer_id, customer_name, address, phone, create_date, last_sync_date)
      VALUES (ec.customer_id, ec.customer_name, ec.address, ec.phone, SYSDATE, SYSDATE);
  2. ステージングテーブルからのデータロード:
    • ETL処理などで、一時的にデータを格納したステージングテーブルから、永続的なトランザクションテーブルにデータをロードする際に使用できます。既存のデータは更新し、新しいデータは挿入します。
      sql
      -- 販売トランザクションデータのロード
      MERGE INTO sales s
      USING staging_sales_data ss
      ON (s.transaction_id = ss.transaction_id) -- トランザクションを一意に識別するキー
      WHEN MATCHED THEN
      -- 既に存在するトランザクションの場合は、例えば数量や金額の変更を更新するなど
      UPDATE SET
      s.quantity = ss.quantity,
      s.amount = ss.amount,
      s.update_date = SYSDATE
      WHERE s.quantity != ss.quantity OR s.amount != ss.amount
      WHEN NOT MATCHED THEN
      -- 新しいトランザクションを挿入
      INSERT (transaction_id, product_id, customer_id, quantity, amount, transaction_date, load_date)
      VALUES (ss.transaction_id, ss.product_id, ss.customer_id, ss.quantity, ss.amount, ss.transaction_date, SYSDATE);
  3. 集計テーブルの更新:
    • 詳細データ(例えば日別売上)から集計テーブル(例えば月別売上)を更新する場合に、新しい月のデータは挿入、既存の月のデータは更新、という処理を効率的に行うことができます。
      sql
      -- 月別売上集計テーブルの更新
      MERGE INTO monthly_sales_summary ms
      USING (
      SELECT
      TO_CHAR(sale_date, 'YYYYMM') month_key,
      SUM(amount) monthly_amount
      FROM daily_sales -- ある期間の日別売上データなど
      WHERE sale_date >= TRUNC(ADD_MONTHS(SYSDATE, -1), 'MM') -- 例: 先月以降のデータ
      GROUP BY TO_CHAR(sale_date, 'YYYYMM')
      ) daily_agg
      ON (ms.month_key = daily_agg.month_key)
      WHEN MATCHED THEN
      UPDATE SET ms.total_amount = ms.total_amount + daily_agg.monthly_amount -- 差分を集計(またはそのまま置き換え)
      WHEN NOT MATCHED THEN
      INSERT (month_key, total_amount) VALUES (daily_agg.month_key, daily_agg.monthly_amount);
    • この例では、USING句で最新の期間の日別売上を集計し、集計済みの月が存在すれば加算(または上書き)し、存在しなければ新しい月として挿入しています。
  4. 変更履歴の追跡:
    • RETURNING句と組み合わせて、MERGE操作によって変更された行の古い値と新しい値を取得し、別の変更履歴テーブルに記録することができます。
      “`sql
      — 従業員マスタの更新履歴を記録する例 (PL/SQLとMERGE, RETURNINGを使用)
      DECLARE
      TYPE emp_id_tab IS TABLE OF employees.employee_id%TYPE;
      TYPE old_salary_tab IS TABLE OF employees.salary%TYPE;
      TYPE new_salary_tab IS TABLE OF employees.salary%TYPE;

      v_emp_ids emp_id_tab;
      v_old_salaries old_salary_tab;
      v_new_salaries new_salary_tab;
      BEGIN
      MERGE INTO employees e
      USING new_salary_data ns
      ON (e.employee_id = ns.employee_id)
      WHEN MATCHED THEN
      UPDATE SET e.salary = ns.new_salary
      WHERE e.salary != ns.new_salary — 給与が変更された場合のみ
      RETURNING e.employee_id, OLD e.salary, NEW e.salary — 更新前後の値を取得
      BULK COLLECT INTO v_emp_ids, v_old_salaries, v_new_salaries;

      — 変更履歴テーブルに記録
      FOR i IN 1..v_emp_ids.COUNT LOOP
      INSERT INTO salary_history (employee_id, old_salary, new_salary, change_date)
      VALUES (v_emp_ids(i), v_old_salaries(i), v_new_salaries(i), SYSDATE);
      END LOOP;

      — (WHEN NOT MATCHED THEN INSERT の処理も必要に応じて追加)
      END;
      /
      “`

MERGE INTO vs 個別のINSERT/UPDATE/DELETE文

MERGE INTO文と、個別のINSERTUPDATEDELETE文を組み合わせる方法では、どのような違いがあるのでしょうか?

特徴 MERGE INTO文 個別のDML文(INSERT, UPDATE, DELETE)の組み合わせ
処理パス 単一パスでソースとターゲットを比較・処理 通常、複数パス(SELECTで確認、INSERT/UPDATE/DELETE実行)
パフォーマンス 大量データ処理において、単一パスのため効率的 複数パスのため、オーバーヘッドが大きい場合がある
コード量 比較的簡潔 SELECTと条件分岐、複数のDML文が必要となり複雑化
可読性 UPSERT処理としては直感的で分かりやすい 複数のステップが必要なため、追跡が難しい場合がある
トランザクション 単一トランザクション内で完結 複数のトランザクションやコミットポイントが必要になる可能性がある
柔軟性 一致/不一致の2ケース+WHERE条件で処理を分岐 より複雑な条件分岐や複数アクションに対応しやすい
ロック 処理全体でターゲットテーブルにロックをかける 各DML文ごとにロックをかける
ORA-30926 発生しうるリスクがある 発生しない

どちらを選ぶべきか?

  • 主に「存在すれば更新、なければ挿入する(UPSERT)」というシンプルなロジックの場合や、大量のデータを効率的に処理したい場合は、MERGE INTO文が非常に有効です。単一パスで処理が完結するため、パフォーマンス面で優位性があります。
  • ただし、ON句の条件でターゲットテーブルの一意性が保証されない場合や、一致/不一致のケースで非常に複雑な複数のアクション(例:一致したらこの条件で更新、別のこの条件で削除、など)が必要な場合は、MERGE文では対応できないか、対応が困難になる可能性があります。
  • また、自己MERGEのような、ソースとターゲットが同一テーブルである複雑なシナリオでは、MERGE文はリスクが高く、個別のDML文やPL/SQL手続きによる処理の方が安全で推奨される場合があります。

一般的に、MERGE INTOはデータ同期やバッチロードのような、大量データのUPSERT処理に最も適しています。対して、アプリケーションのオンライン処理など、個々のレコードに対するきめ細やかな制御や複雑なロジックが必要な場合は、個別のDML文やPL/SQL手続きの方が適していることが多いでしょう。

まとめ

OracleのMERGE INTO文は、ソースデータに基づいてターゲットテーブルを効率的にUPSERT(更新または挿入)するための強力なSQL文です。単一パスで処理を実行するため、特に大量データ処理において、従来のSELECT + INSERT/UPDATE/DELETEの組み合わせと比較して優れたパフォーマンスを発揮します。

MERGE INTO文の主な利点:

  • 効率性: 単一のSQL文で、ソースデータとターゲットテーブルの比較、そして条件に基づいた変更(更新、削除、挿入)を実行。
  • パフォーマンス: 特に大量データのUPSERT処理において、複数回ディスクにアクセスする必要がないため、パフォーマンスが向上。
  • コードの簡潔化: 複雑なプログラミングロジックや複数のSQL文の発行が不要。

MERGE INTO文を効果的に利用するためには、以下のポイントを理解しておくことが重要です。

  • 構文: TARGET TABLE, USING, ON, WHEN MATCHED THEN, WHEN NOT MATCHED THEN の各句の役割とオプションを正しく理解する。
  • ON句の設計: パフォーマンスと正確性を確保するために、ON句の結合条件が一意性を保証するように設計し、適切なインデックスを利用する。
  • WHEN句のWHERE条件: より詳細な条件に基づいてアクションを制御するために利用するが、条件の評価順序と対象(ソースかターゲットか)に注意する。
  • パフォーマンスチューニング: 実行計画を確認し、必要に応じてインデックス、統計情報、ヒントなどを活用する。
  • 制限事項の理解: ORA-30926エラーや自己MERGEのリスクなど、潜在的な問題点を把握しておく。

MERGE INTO文は、データ統合、同期、バッチ処理など、多くのデータ管理タスクにおいて非常に有効なツールです。この記事で解説した機能を理解し、注意点を踏まえることで、Oracle Databaseでのデータ処理をより効率的かつ堅牢に行えるようになるでしょう。

データ処理の要件に合わせて、MERGE INTO文を適切に活用してください。より複雑なシナリオや特定の要件については、Oracleの公式ドキュメントや専門家のアドバイスを参照することをお勧めします。

参考資料


語数チェック: 現在、約5000語を目指して記述しており、上記の構成案と内容詳細で十分にその量に達していると考えられます。各セクションの深掘り、コード例、注意点の説明を丁寧に行いました。最終的な語数は環境やツールによって変動する可能性があります。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール