【完全ガイド】Oracle MERGE 文 使い方|UPSERT・WHEN MATCHED・DELETE 句・差分同期まで徹底解説
- 作成日 2026.07.21
- その他
- 1.
- 1.1. 結論:まず動く最小構成
- 1.2. MERGE 文の基本構文
- 1.3. USING 句のバリエーション
- 1.4. WHEN MATCHED 句の詳細
- 1.5. WHEN NOT MATCHED 句の詳細
- 1.6. 実践パターン 8選
- 1.7. パフォーマンス最適化
- 1.8. よくあるエラーと対処
- 1.9. Rails / Java / Python 対応
- 1.10. 実践シナリオ
- 1.11. 予防のベストプラクティス
- 1.12. トラブルシューティング
- 1.13. よくある質問(FAQ)
- 1.13.1. Q1. INSERT … SELECT vs MERGE
- 1.13.2. Q2. MERGE と UPDATE の性能
- 1.13.3. Q3. ON 句で複数条件
- 1.13.4. Q4. WHEN MATCHED / NOT MATCHED の順序
- 1.13.5. Q5. DELETE 句の使い所
- 1.13.6. Q6. WHERE 句の使い所
- 1.13.7. Q7. 並列度の目安
- 1.13.8. Q8. Direct Path INSERT の副作用
- 1.13.9. Q9. Rails での MERGE 直接発行
- 1.13.10. Q10. トリガーとの相互作用
- 1.13.11. Q11. RETURNING 句
- 1.13.12. Q12. Autonomous DB / RDS
- 1.14. 参考リンク
- 1.15. まとめ
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つの主要要素
- INTO 句: 更新対象テーブル
- USING 句: ソース(サブクエリ or テーブル)
- ON 句: マッチング条件
- 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';
動作:
- UPDATE 実行
- 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';
動作:
- マッチ行を UPDATE
- UPDATE 後、INACTIVE のものを DELETE
- 新規 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つの実践パターン
- 単純 UPSERT
- バルク UPSERT
- 差分同期
- 日次集計
- SCD Type 1(上書き)
- SCD Type 2(履歴保持)
- ソフトデリート
- MERGE + DELETE(12c+)
エラー対処
| エラー | 原因 | 対処 |
|---|---|---|
| ORA-30926 | ソース重複 | GROUP BY / ROW_NUMBER |
| ORA-38104 | ON カラム 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)もあわせてご確認ください。
-
前の記事
【完全ガイド】ORA-00001: unique constraint violated の原因と解決方法|MERGE・IGNORE_ROW_ON_DUPKEY_INDEX・IDENTITY まで徹底解説 2026.07.21
-
次の記事
【完全ガイド】ORA-01830: date format picture ends before converting の原因と解決方法|TO_TIMESTAMP・ISO 8601・タイムゾーンまで徹底解説 2026.07.22
コメントを書く