【完全ガイド】Oracle MERGE 文 使い方|UPSERT・WHEN MATCHED・DELETE 句・差分同期まで徹底解説

【完全ガイド】Oracle MERGE 文 使い方|UPSERT・WHEN MATCHED・DELETE 句・差分同期まで徹底解説

目次

Oracle でデータ更新・投入を扱う際、避けて通れない機能が MERGE 文:

MERGE INTO employees dst
USING (SELECT 100 AS id, 'Alice' AS name FROM DUAL) src
  ON (dst.id = src.id)
WHEN MATCHED THEN 
  UPDATE SET dst.name = src.name
WHEN NOT MATCHED THEN 
  INSERT (id, name) VALUES (src.id, src.name);

存在すれば UPDATE、なければ INSERT という UPSERT を、Oracle 標準として1文で実現。Oracle 9i で登場、ETL・データ移行・差分同期の中核として、DBA・開発者の必修スキルです。

しかし、実際に MERGE 文を使いこなそうとすると:

  • 基本構文は分かるが、実践では使いこなせない
  • USING 句に何を書くかで迷う
  • DELETE 句の使い方(12c+ で拡張)を知らない
  • WHERE 句の位置が分からない(MATCHED の後 vs ON 句内)
  • 並行実行で ORA-00001 が発生(意外な落とし穴)
  • ORA-30926: unable to get stable set of rows の対処法
  • ソースデータに重複があるとエラー
  • パフォーマンスが遅い(PARALLEL/APPEND 未使用)
  • SCD(Slowly Changing Dimension) 実装が分からない
  • Rails からの活用が難しい

MERGE は単なる UPSERT ツールではなくデータウェアハウス・データ移行・ETL・SCD 実装 など、実務の中核ツール。しかし、日本語の解説は表面的なものが多く、実践的なパフォーマンス最適化、エラー対処、応用パターンまで踏み込んだ記事は少ないのが現状です。

本記事では、Oracle MERGE 文完全な使い方を、リファレンスとして実用的に整理します。基本構文、WHEN MATCHED/NOT MATCHED、USING 句のバリエーション、DELETE 句、WHERE 句、8つの実践パターン、パフォーマンス最適化、ORA-30926/ORA-38104 等のエラー対処、Rails/Java/Python 対応、実践シナリオ、FAQまで完全網羅。この1本で MERGE を業務レベルで使いこなせるようになります。


結論:まず動く最小構成

時間がない方向けに、最速の実行手順を先に示します。

最小構成の MERGE

MERGE INTO target_table dst
USING (
  SELECT 1 AS id, 'Alice' AS name FROM DUAL
) src
ON (dst.id = src.id)
WHEN MATCHED THEN 
  UPDATE SET dst.name = src.name
WHEN NOT MATCHED THEN 
  INSERT (id, name) VALUES (src.id, src.name);

覚えるべき Top 10 パターン

-- ① シンプルな UPSERT(1行)
MERGE INTO t dst USING (SELECT :id id, :name name FROM DUAL) src
  ON (dst.id = src.id)
WHEN MATCHED THEN UPDATE SET dst.name = src.name
WHEN NOT MATCHED THEN INSERT VALUES (src.id, src.name);

-- ② テーブルからの UPSERT
MERGE INTO target dst USING source src
  ON (dst.id = src.id)
WHEN MATCHED THEN UPDATE SET dst.col = src.col
WHEN NOT MATCHED THEN INSERT (id, col) VALUES (src.id, src.col);

-- ③ 条件付き UPDATE
MERGE INTO target dst USING source src ON (dst.id = src.id)
WHEN MATCHED THEN 
  UPDATE SET dst.col = src.col
  WHERE dst.col != src.col;   -- 変更ある場合のみ

-- ④ 条件付き INSERT
WHEN NOT MATCHED THEN 
  INSERT (id, col) VALUES (src.id, src.col)
  WHERE src.status = 'ACTIVE';

-- ⑤ DELETE 付き(12c+)
WHEN MATCHED THEN 
  UPDATE SET dst.col = src.col
  DELETE WHERE dst.status = 'INACTIVE';

-- ⑥ MATCHED のみ(差分更新)
MERGE INTO target dst USING source src ON (dst.id = src.id)
WHEN MATCHED THEN UPDATE SET dst.col = src.col;

-- ⑦ NOT MATCHED のみ(存在しないもの追加)
MERGE INTO target dst USING source src ON (dst.id = src.id)
WHEN NOT MATCHED THEN INSERT (id, col) VALUES (src.id, src.col);

-- ⑧ 集計しながら MERGE
MERGE INTO daily_summary dst
USING (
  SELECT TRUNC(order_date) day, SUM(amount) total
  FROM orders
  GROUP BY TRUNC(order_date)
) src ON (dst.day = src.day)
WHEN MATCHED THEN UPDATE SET dst.total = src.total
WHEN NOT MATCHED THEN INSERT (day, total) VALUES (src.day, src.total);

-- ⑨ PARALLEL 高速化
MERGE /*+ PARALLEL(dst, 4) PARALLEL(src, 4) */ 
INTO target dst USING source src ...

-- ⑩ APPEND(Direct Path)
MERGE /*+ APPEND */ INTO target dst USING source src ...

詳細は以下で解説します。


MERGE 文の基本構文

完全構文

MERGE INTO <target_table> [<alias>]
USING <source_table_or_query> [<alias>]
  ON (<join_condition>)

-- 一致した場合の処理
WHEN MATCHED THEN
  UPDATE SET <col1> = <expr1>, <col2> = <expr2>
  [WHERE <update_condition>]
  [DELETE WHERE <delete_condition>]

-- 一致しなかった場合の処理
WHEN NOT MATCHED THEN
  INSERT [(<col1>, <col2>)] VALUES (<val1>, <val2>)
  [WHERE <insert_condition>]
;

4つの主要要素

  1. INTO 句: 更新対象テーブル
  2. USING 句: ソース(サブクエリ or テーブル)
  3. ON 句: マッチング条件
  4. WHEN 句: MATCHED / NOT MATCHED の処理

MATCHED / NOT MATCHED の意味

Source → USING で取得したデータ
Target → INTO で指定したテーブル

ON 条件で:
- Source と Target がマッチ → WHEN MATCHED(既存 → UPDATE)
- Source は Target になし → WHEN NOT MATCHED(新規 → INSERT)

順序は自由

-- MATCHED を先
WHEN MATCHED THEN UPDATE ...
WHEN NOT MATCHED THEN INSERT ...

-- NOT MATCHED を先
WHEN NOT MATCHED THEN INSERT ...
WHEN MATCHED THEN UPDATE ...

どちらも動作は同じ。

どちらか片方のみも OK

-- INSERT のみ(既存無視)
MERGE INTO t dst USING src ON (dst.id = src.id)
WHEN NOT MATCHED THEN INSERT ...;

-- UPDATE のみ(新規無視)
MERGE INTO t dst USING src ON (dst.id = src.id)
WHEN MATCHED THEN UPDATE ...;

USING 句のバリエーション

パターン1: 単一行(DUAL)

MERGE INTO employees dst
USING (SELECT 100 AS id, 'Alice' AS name FROM DUAL) src
  ON (dst.id = src.id)
WHEN MATCHED THEN UPDATE SET dst.name = src.name
WHEN NOT MATCHED THEN INSERT (id, name) VALUES (src.id, src.name);

単一行の UPSERT、Web フォーム送信などで頻用。

パターン2: テーブル間

MERGE INTO target_table dst
USING source_table src
  ON (dst.id = src.id)
WHEN MATCHED THEN UPDATE SET dst.col = src.col
WHEN NOT MATCHED THEN INSERT ...;

シンプル、ETL の基本。

パターン3: 集計サブクエリ

MERGE INTO daily_summary dst
USING (
  SELECT customer_id, TRUNC(order_date) day,
         SUM(amount) total, COUNT(*) cnt
  FROM orders
  WHERE order_date >= TRUNC(SYSDATE - 1)
  GROUP BY customer_id, TRUNC(order_date)
) src
  ON (dst.customer_id = src.customer_id AND dst.day = src.day)
WHEN MATCHED THEN UPDATE SET dst.total = src.total, dst.cnt = src.cnt
WHEN NOT MATCHED THEN INSERT VALUES (src.customer_id, src.day, src.total, src.cnt);

集計結果を MERGE、DWH の日次バッチで頻用。

パターン4: 複数テーブル JOIN

MERGE INTO customers dst
USING (
  SELECT c.id, c.name, o.total_orders
  FROM customer_master c
    JOIN order_summary o ON c.id = o.customer_id
  WHERE c.active = 'Y'
) src
  ON (dst.id = src.id)
WHEN MATCHED THEN UPDATE SET 
  dst.name = src.name, 
  dst.total_orders = src.total_orders
WHEN NOT MATCHED THEN INSERT VALUES (src.id, src.name, src.total_orders);

パターン5: 分析関数活用

MERGE INTO product_rankings dst
USING (
  SELECT product_id, 
         RANK() OVER (ORDER BY total_sales DESC) rank,
         total_sales
  FROM (SELECT product_id, SUM(quantity * price) total_sales
        FROM sales GROUP BY product_id)
) src ON (dst.product_id = src.product_id)
WHEN MATCHED THEN UPDATE SET dst.rank = src.rank, dst.sales = src.total_sales
WHEN NOT MATCHED THEN INSERT VALUES (src.product_id, src.rank, src.total_sales);

パターン6: UNION ALL

MERGE INTO target dst
USING (
  SELECT id, val FROM source_a
  UNION ALL
  SELECT id, val FROM source_b
) src ON (dst.id = src.id)
WHEN MATCHED THEN UPDATE SET dst.val = src.val
WHEN NOT MATCHED THEN INSERT (id, val) VALUES (src.id, src.val);

WHEN MATCHED 句の詳細

基本 UPDATE

WHEN MATCHED THEN 
  UPDATE SET dst.col1 = src.col1, 
             dst.col2 = src.col2;

複雑な式

WHEN MATCHED THEN 
  UPDATE SET 
    dst.total = dst.total + src.amount,
    dst.updated_at = SYSTIMESTAMP,
    dst.updated_by = USER,
    dst.status = CASE 
                   WHEN src.amount > 1000 THEN 'HIGH'
                   ELSE 'NORMAL'
                 END;

WHERE 句で条件付き UPDATE

WHEN MATCHED THEN 
  UPDATE SET dst.col = src.col
  WHERE dst.col != src.col;    -- 差分ある場合のみ更新

利点:

  • 不要な UPDATE を減らす
  • REDO 削減
  • パフォーマンス向上

DELETE 句(Oracle 10g+)

WHEN MATCHED THEN 
  UPDATE SET dst.col = src.col
  DELETE WHERE dst.status = 'INACTIVE';

動作:

  1. UPDATE 実行
  2. UPDATE 後、DELETE 条件を満たす行を削除

重要: DELETE 条件はUPDATE 後の値で評価:

WHEN MATCHED THEN 
  UPDATE SET dst.status = src.status
  DELETE WHERE dst.status = 'DELETED';
-- src.status が 'DELETED' なら UPDATE で 'DELETED' になり、その後 DELETE

制約

-- ❌ ON 句のカラムは UPDATE できない
WHEN MATCHED THEN 
  UPDATE SET dst.id = src.id;
-- ORA-38104: Columns referenced in the ON Clause cannot be updated

-- ✅ 別カラムはOK
WHEN MATCHED THEN 
  UPDATE SET dst.name = src.name;

WHEN NOT MATCHED 句の詳細

基本 INSERT

WHEN NOT MATCHED THEN 
  INSERT (id, name, created_at) 
  VALUES (src.id, src.name, SYSTIMESTAMP);

全列 INSERT

WHEN NOT MATCHED THEN 
  INSERT VALUES (src.id, src.name, src.email);
-- 列指定を省略した場合、テーブル定義順

WHERE 句で条件付き INSERT

WHEN NOT MATCHED THEN 
  INSERT (id, name) VALUES (src.id, src.name)
  WHERE src.status = 'ACTIVE';     -- ACTIVE のみ INSERT

INACTIVE な src の行は無視される。

DEFAULT 値の使用

WHEN NOT MATCHED THEN 
  INSERT (id, name, status)
  VALUES (src.id, src.name, DEFAULT);

実践パターン 8選

パターン① 単純 UPSERT

用途: 存在すれば更新、なければ挿入

MERGE INTO users dst
USING (SELECT :email AS email, :name AS name FROM DUAL) src
  ON (dst.email = src.email)
WHEN MATCHED THEN 
  UPDATE SET dst.name = src.name, dst.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN 
  INSERT (email, name, created_at) 
  VALUES (src.email, src.name, SYSTIMESTAMP);

パターン② バルク UPSERT

用途: staging → target の一括反映

MERGE INTO customers dst
USING staging_customers src
  ON (dst.customer_id = src.customer_id)
WHEN MATCHED THEN 
  UPDATE SET dst.name = src.name, 
             dst.email = src.email,
             dst.updated_at = SYSDATE
WHEN NOT MATCHED THEN 
  INSERT (customer_id, name, email, created_at)
  VALUES (src.customer_id, src.name, src.email, SYSDATE);

COMMIT;
TRUNCATE TABLE staging_customers;

パターン③ 差分同期(変更のみ)

用途: 変更のあるレコードのみ UPDATE

MERGE INTO target dst
USING source src
  ON (dst.id = src.id)
WHEN MATCHED THEN 
  UPDATE SET 
    dst.col1 = src.col1,
    dst.col2 = src.col2,
    dst.updated_at = SYSTIMESTAMP
  WHERE dst.col1 != src.col1 OR dst.col2 != src.col2
     OR (dst.col1 IS NULL AND src.col1 IS NOT NULL)
     OR (dst.col1 IS NOT NULL AND src.col1 IS NULL)
WHEN NOT MATCHED THEN INSERT ...;

NULL の扱いに注意!= は NULL に反応しない)。

パターン④ 日次集計テーブル更新

MERGE INTO daily_sales_summary dst
USING (
  SELECT 
    TRUNC(sale_date) sale_day,
    product_id,
    SUM(quantity) total_qty,
    SUM(amount) total_amt
  FROM sales
  WHERE sale_date >= TRUNC(SYSDATE) - 1
    AND sale_date <  TRUNC(SYSDATE)
  GROUP BY TRUNC(sale_date), product_id
) src
  ON (dst.sale_day = src.sale_day AND dst.product_id = src.product_id)
WHEN MATCHED THEN 
  UPDATE SET dst.total_qty = src.total_qty,
             dst.total_amt = src.total_amt
WHEN NOT MATCHED THEN 
  INSERT VALUES (src.sale_day, src.product_id, src.total_qty, src.total_amt);

深夜バッチで再実行しても安全

パターン⑤ Type 1 SCD(上書き型)

用途: マスターテーブルの上書き更新

MERGE INTO dim_customer dst
USING (
  SELECT customer_id, name, address, phone 
  FROM customer_source
) src
  ON (dst.customer_id = src.customer_id)
WHEN MATCHED THEN 
  UPDATE SET dst.name = src.name,
             dst.address = src.address,
             dst.phone = src.phone,
             dst.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN 
  INSERT VALUES (src.customer_id, src.name, src.address, src.phone, SYSTIMESTAMP);

パターン⑥ Type 2 SCD(履歴保持)

用途: 変更履歴を保持する(会員ランク変更履歴等)

-- Step 1: 変更のある既存行を「無効化」
MERGE INTO dim_customer dst
USING (
  SELECT customer_id, name, address, phone
  FROM customer_source
) src
  ON (dst.customer_id = src.customer_id AND dst.is_current = 'Y')
WHEN MATCHED THEN 
  UPDATE SET dst.is_current = 'N',
             dst.effective_end = SYSTIMESTAMP
  WHERE dst.name != src.name OR dst.address != src.address;

-- Step 2: 新しい行として INSERT
INSERT INTO dim_customer (customer_id, name, address, phone, 
                          effective_start, effective_end, is_current)
SELECT src.customer_id, src.name, src.address, src.phone, 
       SYSTIMESTAMP, NULL, 'Y'
FROM customer_source src
WHERE NOT EXISTS (
  SELECT 1 FROM dim_customer dst
  WHERE dst.customer_id = src.customer_id 
    AND dst.is_current = 'Y'
    AND dst.name = src.name
    AND dst.address = src.address
);

パターン⑦ ソフトデリート

用途: 削除フラグの管理

MERGE INTO products dst
USING product_updates src
  ON (dst.product_id = src.product_id)
WHEN MATCHED THEN 
  UPDATE SET dst.name = src.name,
             dst.price = src.price,
             dst.deleted_at = CASE 
                                WHEN src.is_deleted = 'Y' THEN SYSTIMESTAMP
                                ELSE NULL
                              END
WHEN NOT MATCHED THEN 
  INSERT (product_id, name, price) 
  VALUES (src.product_id, src.name, src.price);

パターン⑧ MERGE + DELETE(12c+)

用途: 統合と削除を一度に

MERGE INTO active_customers dst
USING customer_master src
  ON (dst.customer_id = src.customer_id)
WHEN MATCHED THEN 
  UPDATE SET dst.status = src.status,
             dst.last_login = src.last_login
  DELETE WHERE dst.status = 'INACTIVE'
WHEN NOT MATCHED THEN 
  INSERT VALUES (src.customer_id, src.status, src.last_login)
  WHERE src.status = 'ACTIVE';

動作:

  1. マッチ行を UPDATE
  2. UPDATE 後、INACTIVE のものを DELETE
  3. 新規 ACTIVE を INSERT

パフォーマンス最適化

PARALLEL ヒント

MERGE /*+ PARALLEL(dst, 4) PARALLEL(src, 4) */
INTO target dst
USING source src ON (dst.id = src.id)
WHEN MATCHED THEN UPDATE SET ...
WHEN NOT MATCHED THEN INSERT ...;

大量データ(100万行以上)で効果。

APPEND ヒント(Direct Path)

MERGE /*+ APPEND */
INTO target dst USING source src ...

INSERT 部分のみDirect Path。HWM 直上に追加、REDO 節約。

注意:

  • 完了後 COMMIT 必要
  • テーブルロック
  • Buffer Cache 経由しない

USE_HASH / USE_MERGE ヒント

MERGE /*+ USE_HASH(dst src) */ INTO target dst USING source src ...

大量データの JOIN を Hash Join に。

インデックス活用

ON 句のカラムにインデックスがあると高速:

-- src.id にインデックス
-- dst.id にインデックス(PK なら不要)

統計情報更新

BEGIN
  DBMS_STATS.GATHER_TABLE_STATS('SCHEMA', 'SOURCE_TABLE');
END;
/

MERGE 前に最新の統計

パラメータ設定の詳細は Oracle パラメータ確認(V$PARAMETER)の記事、Tablespace 管理は Oracle Tablespace 管理の記事も参照してください。


よくあるエラーと対処

ORA-30926: unable to get stable set of rows

症状:

ORA-30926: unable to get a stable set of rows in the source tables

原因: ソースに複数の行がターゲットの同じ行にマッチ

-- 悪い例
MERGE INTO customers dst
USING (SELECT customer_id, name FROM staging) src
  ON (dst.customer_id = src.customer_id)
WHEN MATCHED THEN UPDATE SET dst.name = src.name;

-- staging に customer_id=1 が2件あると ORA-30926

対処: ソースを一意化

-- 集約
USING (
  SELECT customer_id, MAX(name) name  -- または任意の集計
  FROM staging
  GROUP BY customer_id
)

-- または最新のみ
USING (
  SELECT customer_id, name FROM (
    SELECT customer_id, name,
           ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY updated_at DESC) rn
    FROM staging
  ) WHERE rn = 1
)

ORA-38104: Columns referenced in the ON Clause cannot be updated

症状:

MERGE INTO t dst USING src ON (dst.id = src.id)
WHEN MATCHED THEN UPDATE SET dst.id = src.new_id;
-- ORA-38104

原因: ON 句で使ったカラムは UPDATE 不可

対処: DELETE + INSERT で対応、または別カラム経由

ORA-00001: unique constraint violated

症状: 並行実行で発生

-- 同時に MERGE 実行 → ORA-00001

ORA-00001 の対処法は ORA-00001: unique constraint violated の記事に詳しく書いています。

ORA-01722: invalid number

症状: ON 句や UPDATE 句で型不一致

ORA-01843 と類似。日付フォーマット関連は ORA-01843: not a valid month の記事も参照してください。

パフォーマンスが遅い

  • 統計情報古い
  • インデックス不足
  • ハッシュ結合になっていない

診断:

EXPLAIN PLAN FOR
MERGE INTO ...;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

Rails / Java / Python 対応

Rails ActiveRecord

Rails 6+ で upsert / upsert_all サポート:

# 単一
User.upsert(
  { email: 'alice@x.com', name: 'Alice' },
  unique_by: :email
)

# 一括
User.upsert_all([
  { email: 'a@x.com', name: 'A' },
  { email: 'b@x.com', name: 'B' }
], unique_by: :email)

内部で MERGE が発行される。

Rails 8 系の詳細は Rails 8 アップグレードガイドの記事、Rails データベース操作は rails db:migrate 使い方の記事、Solid Queue 使い方の記事も参照してください。

Java (JDBC)

String mergeSql = """
    MERGE INTO customers dst
    USING (SELECT ? AS email, ? AS name FROM DUAL) src
      ON (dst.email = src.email)
    WHEN MATCHED THEN 
      UPDATE SET dst.name = src.name
    WHEN NOT MATCHED THEN 
      INSERT (email, name) VALUES (src.email, src.name)
    """;

try (PreparedStatement ps = conn.prepareStatement(mergeSql)) {
    ps.setString(1, customer.getEmail());
    ps.setString(2, customer.getName());
    ps.executeUpdate();
}

Python (oracledb)

import oracledb

merge_sql = """
MERGE INTO customers dst
USING (SELECT :email AS email, :name AS name FROM DUAL) src
  ON (dst.email = src.email)
WHEN MATCHED THEN UPDATE SET dst.name = src.name
WHEN NOT MATCHED THEN 
  INSERT (email, name) VALUES (src.email, src.name)
"""

cursor.execute(merge_sql, email="alice@x.com", name="Alice")
connection.commit()

実践シナリオ

シナリオ1:Web アプリのプロフィール更新

MERGE INTO user_profiles dst
USING (SELECT :user_id AS user_id, :bio AS bio, :avatar AS avatar FROM DUAL) src
  ON (dst.user_id = src.user_id)
WHEN MATCHED THEN 
  UPDATE SET dst.bio = src.bio, 
             dst.avatar = src.avatar,
             dst.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN 
  INSERT (user_id, bio, avatar, created_at) 
  VALUES (src.user_id, src.bio, src.avatar, SYSTIMESTAMP);

シナリオ2:CSV バルクインポート

-- 一時テーブル準備
CREATE TABLE staging_products AS SELECT * FROM products WHERE 1=0;

-- SQL*Loader / External Table で読み込み

-- MERGE
MERGE INTO products dst
USING (
  SELECT product_id, name, price, category
  FROM (
    SELECT product_id, name, price, category,
           ROW_NUMBER() OVER (PARTITION BY product_id ORDER BY loaded_at DESC) rn
    FROM staging_products
  ) WHERE rn = 1
) src ON (dst.product_id = src.product_id)
WHEN MATCHED THEN 
  UPDATE SET dst.name = src.name, dst.price = src.price
WHEN NOT MATCHED THEN 
  INSERT VALUES (src.product_id, src.name, src.price, src.category);

シナリオ3:日次バッチのサマリ更新

-- 昨日の売上を集計、再実行しても安全
MERGE INTO daily_sales dst
USING (
  SELECT TRUNC(sale_date) sale_day, 
         product_id,
         SUM(amount) total_amount,
         COUNT(*) transaction_count
  FROM sales
  WHERE sale_date >= TRUNC(SYSDATE - 1)
    AND sale_date <  TRUNC(SYSDATE)
  GROUP BY TRUNC(sale_date), product_id
) src
  ON (dst.sale_day = src.sale_day AND dst.product_id = src.product_id)
WHEN MATCHED THEN 
  UPDATE SET dst.total_amount = src.total_amount,
             dst.transaction_count = src.transaction_count
WHEN NOT MATCHED THEN 
  INSERT VALUES (src.sale_day, src.product_id, src.total_amount, src.transaction_count);

crontab の使い方は crontab 使い方の記事、Linux でプロセスをバックグラウンド実行する方法の記事、systemctl vs service の記事も参照してください。

シナリオ4:外部システム連携

-- 外部 API から取得したデータで MERGE
MERGE INTO external_customers dst
USING external_api_data src
  ON (dst.external_id = src.external_id)
WHEN MATCHED THEN 
  UPDATE SET dst.name = src.name,
             dst.last_sync = SYSTIMESTAMP
WHEN NOT MATCHED THEN 
  INSERT (external_id, name, first_sync, last_sync)
  VALUES (src.external_id, src.name, SYSTIMESTAMP, SYSTIMESTAMP);

シナリオ5:Rails で在庫管理

class InventoryUpdate
  def self.apply(updates)
    # updates = [{product_id: 1, quantity: 100}, ...]
    Product.upsert_all(updates, unique_by: :product_id)
  end
end

内部で MERGE 実行。

シナリオ6:CI/CD デプロイ後のデータ同期

-- デプロイ後、マスタデータを同期
MERGE INTO master_config dst
USING (
  SELECT * FROM master_config_deploy
) src ON (dst.config_key = src.config_key)
WHEN MATCHED THEN 
  UPDATE SET dst.config_value = src.config_value,
             dst.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN 
  INSERT VALUES (src.config_key, src.config_value, SYSTIMESTAMP);

Kamal 2 デプロイの詳細は Kamal 2 デプロイの記事も参照してください。

シナリオ7:Docker 環境でのデータロード

# Docker Oracle で MERGE 実行
docker exec -it oracle-xe sqlplus scott/tiger <<EOF
MERGE INTO ...
;
COMMIT;
EOF

Docker 関連は docker daemon 接続エラーの記事、Docker no space left on device の記事も参照してください。

シナリオ8:SCD Type 2 履歴管理

-- 会員ランク変更履歴
BEGIN
  -- Step 1: 変更のある既存を無効化
  UPDATE member_history
  SET valid_to = SYSTIMESTAMP, is_current = 'N'
  WHERE is_current = 'Y'
    AND EXISTS (
      SELECT 1 FROM member_source src
      WHERE src.member_id = member_history.member_id
        AND src.rank != member_history.rank
    );

  -- Step 2: 新レコード追加
  INSERT INTO member_history 
    (member_id, rank, valid_from, valid_to, is_current)
  SELECT src.member_id, src.rank, SYSTIMESTAMP, NULL, 'Y'
  FROM member_source src
  WHERE NOT EXISTS (
    SELECT 1 FROM member_history mh
    WHERE mh.member_id = src.member_id
      AND mh.rank = src.rank
      AND mh.is_current = 'Y'
  );
  
  COMMIT;
END;
/

シナリオ9:Data Pump インポート後の差分反映

-- 別環境からの Data Pump インポート後
MERGE INTO production_data dst
USING imported_data src ON (dst.id = src.id)
WHEN MATCHED THEN 
  UPDATE SET dst.col = src.col
  WHERE dst.col != src.col
WHEN NOT MATCHED THEN 
  INSERT VALUES (src.id, src.col);

Data Pump の詳細は Oracle Data Pump 使い方の記事を参照してください。

シナリオ10:Analytics 結果の永続化

-- 分析結果を専用テーブルに保存
MERGE INTO customer_segments dst
USING (
  SELECT customer_id,
         CASE 
           WHEN total_spend > 100000 THEN 'PREMIUM'
           WHEN total_spend > 10000 THEN 'GOLD'
           ELSE 'STANDARD'
         END AS segment
  FROM (
    SELECT customer_id, SUM(amount) total_spend
    FROM orders
    WHERE order_date > SYSDATE - 365
    GROUP BY customer_id
  )
) src ON (dst.customer_id = src.customer_id)
WHEN MATCHED THEN 
  UPDATE SET dst.segment = src.segment,
             dst.calculated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN 
  INSERT VALUES (src.customer_id, src.segment, SYSTIMESTAMP);

予防のベストプラクティス

1. ソースの一意性を保証

-- MERGE 前に確認
SELECT id, COUNT(*) FROM source GROUP BY id HAVING COUNT(*) > 1;
-- 0行なら OK

2. 差分のみ UPDATE

WHEN MATCHED THEN UPDATE SET ...
WHERE dst.col != src.col;

不要な UPDATE を減らす

3. 統計情報を最新に

EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'SOURCE_TABLE');
EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'TARGET_TABLE');

4. 大量データは PARALLEL / APPEND

MERGE /*+ PARALLEL(dst, 4) APPEND */ INTO ...

5. インデックス設計

ON 句のカラムに適切なインデックス

6. LOG ERRORS 活用

MERGE INTO target dst USING source src ON (...)
WHEN MATCHED THEN UPDATE ...
WHEN NOT MATCHED THEN INSERT ...
LOG ERRORS INTO merge_err REJECT LIMIT UNLIMITED;

エラー行を分離、他は継続。

7. トランザクション制御

-- 大量 MERGE は途中 COMMIT で分割検討

8. Dry Run で確認

-- 更新される行数を事前確認
SELECT COUNT(*) FROM source src
WHERE EXISTS (SELECT 1 FROM target dst WHERE dst.id = src.id);  -- UPDATE 予定

SELECT COUNT(*) FROM source src
WHERE NOT EXISTS (SELECT 1 FROM target dst WHERE dst.id = src.id);  -- INSERT 予定

トラブルシューティング

MERGE 遅い

  • 統計情報古い
  • インデックス不足
  • 統計に基づく Hash/Sort ではなく Nested Loop

診断:

EXPLAIN PLAN FOR MERGE INTO ...;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

想定外の UPDATE

  • ON 句のマッチング条件が甘い
  • ソースに重複あり

対処: ON 句を厳密に、DISTINCT や集約でソース一意化。

想定外の INSERT

  • ON 句で NULL 比較の問題
-- NULL 比較(NULL != NULL)
ON (dst.id = src.id)

-- NULL も一致とみなす
ON (NVL(dst.id, -1) = NVL(src.id, -1))
-- または
ON (dst.id = src.id OR (dst.id IS NULL AND src.id IS NULL))

巨大 UNDO 使用

大量 UPDATE で UNDO 逼迫:

-- 一時的に UNDO_RETENTION 増加
ALTER SYSTEM SET UNDO_RETENTION = 3600;

-- または段階的 MERGE

UNDO 関連は ORA-01555: snapshot too old の記事、TEMP 領域関連は ORA-01652: unable to extend temp segment の記事も参照してください。

並列で ORA-04091

トリガーが MERGE の対象テーブルを参照:

ORA-04091 mutating table の記事、トリガー設計は同記事の Compound Trigger 解説を参照。

CI/CD で失敗

  • リトライロジック追加
  • タイムアウト設定
  • 部分実行を許容する設計

よくある質問(FAQ)

Q1. INSERT … SELECT vs MERGE

  • INSERT SELECT: 純粋な追加
  • MERGE: 存在チェック + UPDATE/INSERT

用途で使い分け。

Q2. MERGE と UPDATE の性能

大量データなら MERGE 一括のほうが速い(1回のスキャンで済む)。

Q3. ON 句で複数条件

ON (dst.a = src.a AND dst.b = src.b)

複合キーの場合など。

Q4. WHEN MATCHED / NOT MATCHED の順序

Oracle は順序不問、動作は同じ。

Q5. DELETE 句の使い所

論理削除フラグから物理削除への変換など。

Q6. WHERE 句の使い所

  • MATCHED の WHERE: 差分のみ UPDATE
  • NOT MATCHED の WHERE: 条件を満たす行だけ INSERT

Q7. 並列度の目安

CPU コア数と同程度。過剰は逆効果。

Q8. Direct Path INSERT の副作用

  • テーブルロック
  • COMMIT まで表示されない
  • Buffer Cache 経由しない

Q9. Rails での MERGE 直接発行

sql = "MERGE INTO ..."
ActiveRecord::Base.connection.execute(sql)

upsert_all で足りない場合。

Q10. トリガーとの相互作用

MERGE でもトリガーは起動。Row-level Trigger + Mutating table に注意。

ORA-04091 の詳細は ORA-04091: table is mutating の記事を参照。

Q11. RETURNING 句

-- MERGE は RETURNING 句を持たない
-- 12c 以降でも同様

DML 全般で使えるわけではない。代替は事後 SELECT。

Q12. Autonomous DB / RDS

同じ挙動。プラットフォーム差なし。


参考リンク

Oracle 公式


まとめ

Oracle MERGE 文 の要点を再整理します。

基本構文

MERGE INTO target_table dst
USING source_query src
  ON (join_condition)
WHEN MATCHED THEN 
  UPDATE SET dst.col = src.col
  [WHERE condition]
  [DELETE WHERE condition]
WHEN NOT MATCHED THEN 
  INSERT (col1, col2) VALUES (src.col1, src.col2)
  [WHERE condition];

実践パターン

-- ① 単純 UPSERT
MERGE INTO t dst USING (SELECT :id id, :val val FROM DUAL) src
  ON (dst.id = src.id)
WHEN MATCHED THEN UPDATE SET dst.val = src.val
WHEN NOT MATCHED THEN INSERT VALUES (src.id, src.val);

-- ② 差分更新(変更のみ)
WHEN MATCHED THEN UPDATE SET dst.col = src.col
WHERE dst.col != src.col;

-- ③ DELETE 付き
WHEN MATCHED THEN UPDATE ... 
DELETE WHERE dst.status = 'INACTIVE';

-- ④ 条件付き INSERT
WHEN NOT MATCHED THEN INSERT ...
WHERE src.active = 'Y';

-- ⑤ 高速化
MERGE /*+ PARALLEL(dst,4) APPEND */ INTO ...

8つの実践パターン

  1. 単純 UPSERT
  2. バルク UPSERT
  3. 差分同期
  4. 日次集計
  5. SCD Type 1(上書き)
  6. SCD Type 2(履歴保持)
  7. ソフトデリート
  8. MERGE + DELETE(12c+)

エラー対処

エラー原因対処
ORA-30926ソース重複GROUP BY / ROW_NUMBER
ORA-38104ON カラム UPDATE 不可別カラム経由
ORA-00001並行実行リトライ、ロック
性能低下統計古いDBMS_STATS 実行

パフォーマンス

  • PARALLEL ヒント(大量データ)
  • APPEND ヒント(Direct Path)
  • 統計情報最新化
  • インデックス設計
  • 差分のみ UPDATE

予防策

  • ソースの一意性を保証
  • 差分 UPDATE で無駄削減
  • 統計情報最新に
  • PARALLEL/APPEND で高速化
  • インデックス設計
  • LOG ERRORS 活用
  • Dry Run で事前確認

Rails 対応

User.upsert({ email: '...', name: '...' }, unique_by: :email)
User.upsert_all([...], unique_by: :email)

内部で MERGE 発行。

事故防止

  • ON 句のマッチングを厳密に
  • NULL 比較に注意
  • 並行実行での ORA-00001
  • UNDO 逼迫を意識
  • トリガーとの相互作用

これらの知識は、Oracle での日常開発・ETL・データ移行・DWH 構築・SCD 実装・Rails / Kamal 環境・Docker 開発など、あらゆる場面で活用できます。本記事をブックマークしておけば、MERGE 文を業務レベルで使いこなせるようになります。


本記事は2026年6月時点の情報をもとに、Oracle Database 19c〜23ai での動作確認・公式ドキュメントに基づき作成しています。Oracle のバージョンにより挙動が異なる場合があるため、最新の情報は Oracle 公式ドキュメント(docs.oracle.com)もあわせてご確認ください。