【Oracle】結合を用いたデータ更新の方法完全ガイド|相関サブクエリ・更新可能結合ビュー・MERGE・ORA-01779対処まで解説

【Oracle】結合を用いたデータ更新の方法完全ガイド|相関サブクエリ・更新可能結合ビュー・MERGE・ORA-01779対処まで解説 Oracle

Oracle で「別テーブルの値を参照してデータを更新したい」場面は頻繁に発生します。マスタテーブルの変更をトランザクションテーブルに反映する、ステージングテーブルから本番テーブルへ一括取り込みするなど、結合を伴う UPDATE は実務の定番パターンです。

Oracle にはこのような結合更新を実現する方法が複数あり、それぞれに構文・制約・パフォーマンス特性が異なります。本記事では、相関サブクエリ UPDATE更新可能結合ビュー(Oracle 独自構文)MERGE INTO の 3 方式を比較しながら、ORA-01779 エラーの対処法やパフォーマンスの考慮点まで体系的に解説します。

この記事でわかること
・方式① 相関サブクエリ UPDATE(SET col = (SELECT …))の書き方
・方式② 更新可能結合ビュー(UPDATE (SELECT … FROM … JOIN …))の書き方
・方式③ MERGE INTO を使った結合更新
・ORA-01779「キー保存表」エラーの原因と対処法
・EXISTS / NOT EXISTS での絞り込み UPDATE
・3 方式のパフォーマンス比較と使い分け
・RETURNING INTO 句で更新結果を受け取る方法
・マスタ反映・ステージング取り込みの実務パターン
スポンサーリンク

サンプルテーブル(以降の例で使用)

SQL(サンプルテーブル定義)
-- 社員テーブル(更新対象)
CREATE TABLE employees (
    emp_id        NUMBER PRIMARY KEY,
    emp_name      VARCHAR2(100),
    dept_id       NUMBER,
    salary        NUMBER,
    dept_name     VARCHAR2(100)   -- ← これをマスタから反映したい
);

-- 部門マスタ(参照元)
CREATE TABLE departments (
    dept_id       NUMBER PRIMARY KEY,
    dept_name     VARCHAR2(100),
    location      VARCHAR2(100)
);

方式①:相関サブクエリ UPDATE

最もオーソドックスな方法です。SET 句にサブクエリを記述し、外側の行と結合条件で紐づけます。

SQL(相関サブクエリ UPDATE 基本)
-- departments テーブルの dept_name を employees に反映
UPDATE employees e
SET e.dept_name = (
    SELECT d.dept_name
    FROM departments d
    WHERE d.dept_id = e.dept_id
)
WHERE EXISTS (
    SELECT 1
    FROM departments d
    WHERE d.dept_id = e.dept_id
);
WHERE EXISTS を付けないとどうなるか
WHERE EXISTS を省略すると、departments に対応する行がない employees の dept_name がNULL に上書きされます。意図しない NULL 化を防ぐため、WHERE EXISTS は必ず付けるのが安全です。

複数列を同時に更新する

SQL(複数列の相関サブクエリ UPDATE)
-- dept_name と location を同時に更新
UPDATE employees e
SET (e.dept_name, e.location) = (
    SELECT d.dept_name, d.location
    FROM departments d
    WHERE d.dept_id = e.dept_id
)
WHERE EXISTS (
    SELECT 1
    FROM departments d
    WHERE d.dept_id = e.dept_id
);
相関サブクエリ方式のメリット・デメリット
【メリット】
・構文がシンプルで理解しやすい
・ORA-01779 エラーが発生しない
・UPDATE 対象テーブルに主キーやユニークインデックスが不要
【デメリット】
・SET 句と WHERE 句でサブクエリが重複する(同じ結合条件を 2 回書く)
・大量データでは参照テーブルへのアクセスが行ごとに発生し、パフォーマンスが低下しやすい

方式②:更新可能結合ビュー(Oracle 独自構文)

Oracle では、インラインビュー(SELECT 文)を直接 UPDATE できるという独自構文があります。MySQL の UPDATE ... JOIN に相当する機能ですが、「キー保存表」の制約がある点が大きく異なります。

SQL(更新可能結合ビュー UPDATE)
-- インラインビューを UPDATE する
UPDATE (
    SELECT
        e.dept_name  AS emp_dept_name,   -- 更新対象の列
        d.dept_name  AS new_dept_name    -- 参照元の列
    FROM employees e
    JOIN departments d ON e.dept_id = d.dept_id
)
SET emp_dept_name = new_dept_name;

この構文は JOIN を使って 2 つのテーブルを結合し、SELECT した結果を直接 UPDATE するイメージです。サブクエリを 2 回書く必要がなく、SQL が簡潔になります。

複数列の更新

SQL(複数列の結合ビュー UPDATE)
UPDATE (
    SELECT
        e.dept_name  AS emp_dept_name,
        e.location   AS emp_location,
        d.dept_name  AS new_dept_name,
        d.location   AS new_location
    FROM employees e
    JOIN departments d ON e.dept_id = d.dept_id
)
SET emp_dept_name = new_dept_name,
    emp_location  = new_location;

ORA-01779: キー保存表エラーの原因と対処法

更新可能結合ビューを使おうとすると、以下のエラーが出ることがあります。

エラーメッセージ
ORA-01779: キー保存されない表にマップする列は変更できません
(cannot modify a column which maps to a non key-preserved table)

これは Oracle が「結合結果の各行が、更新対象テーブルの1 行と確実に対応しているか」を検証するために発生するエラーです。

発生条件

結合相手のテーブル(参照元)の結合キーにPRIMARY KEY または UNIQUE INDEX が存在しないと、結合が 1 対多になる可能性があり、「どの行の値で更新すればよいか一意に決まらない」ため ORA-01779 が発生します。

SQL(ORA-01779 が発生する例)
-- departments.dept_id に PRIMARY KEY も UNIQUE INDEX もない場合
UPDATE (
    SELECT e.salary, d.new_salary
    FROM employees e
    JOIN dept_salary_changes d ON e.dept_id = d.dept_id
    -- dept_salary_changes.dept_id にユニーク制約がない → ORA-01779
)
SET salary = new_salary;

対処法

対処法 方法 推奨度
①参照テーブルにユニーク制約を追加 結合キーに PRIMARY KEY または UNIQUE INDEX を作成する 最も推奨(データ整合性も保証される)
②相関サブクエリ方式に書き換える UPDATE … SET col = (SELECT …) WHERE EXISTS (…) に変更 推奨(構文変更のみ。制約追加が難しい場合に)
③MERGE INTO を使う MERGE INTO … USING … ON (…) WHEN MATCHED THEN UPDATE 推奨(ユニーク制約不要。ORA-01779 は発生しない)
④ヒント BYPASS_UJVC を使う /*+ BYPASS_UJVC */ ヒントで制約チェックを回避 非推奨(非公開ヒント。将来削除の可能性あり。データ不整合リスク)
SQL(対処法①: ユニークインデックスの追加)
-- 参照テーブルの結合キーにユニークインデックスを作成
CREATE UNIQUE INDEX idx_dept_salary_dept_id
ON dept_salary_changes (dept_id);

-- これで更新可能結合ビューが使えるようになる
UPDATE (
    SELECT e.salary, d.new_salary
    FROM employees e
    JOIN dept_salary_changes d ON e.dept_id = d.dept_id
)
SET salary = new_salary;  -- OK
キー保存表(key-preserved table)とは
結合結果において、そのテーブルの主キーが結合後も一意性を保っているテーブルのことです。参照テーブル(JOIN 相手)の結合キーにユニーク制約があれば、更新対象テーブルの各行は結合後も 1 行のままなので「キー保存」されます。結合が 1:N になる可能性があるとキー保存されず、ORA-01779 が発生します。

方式③:MERGE INTO を使った結合更新

MERGE INTO はもともと「あれば更新、なければ挿入(UPSERT)」のための構文ですが、WHEN MATCHED のみを使えば結合更新としても利用できます。ORA-01779 エラーが発生しないため、ユニーク制約を追加できない場合に特に有用です。

SQL(MERGE による結合更新)
-- departments の dept_name を employees に反映
MERGE INTO employees e
USING departments d
ON (e.dept_id = d.dept_id)
WHEN MATCHED THEN
    UPDATE SET e.dept_name = d.dept_name;
SQL(MERGE で複数列を更新 + 条件付き)
-- 複数列の更新 + WHERE で条件を追加
MERGE INTO employees e
USING departments d
ON (e.dept_id = d.dept_id)
WHEN MATCHED THEN
    UPDATE SET
        e.dept_name = d.dept_name,
        e.location  = d.location
    WHERE e.salary > 0;  -- 追加条件: salary が 0 より大きい行のみ更新

MERGE 文の UPSERT(INSERT + UPDATE)やバルク処理の詳細は「MERGE文の活用法」を参照してください。

EXISTS / NOT EXISTS で更新対象を絞り込む

SQL(EXISTS で対応するマスタが存在する行だけ更新)
-- マスタに存在する部門のみ dept_name を更新
UPDATE employees e
SET e.dept_name = (
    SELECT d.dept_name FROM departments d WHERE d.dept_id = e.dept_id
)
WHERE EXISTS (
    SELECT 1 FROM departments d WHERE d.dept_id = e.dept_id
);
SQL(NOT EXISTS で対応するマスタがない行を更新)
-- マスタに存在しない部門ID の dept_name を「(廃止部門)」に更新
UPDATE employees e
SET e.dept_name = '(廃止部門)'
WHERE NOT EXISTS (
    SELECT 1 FROM departments d WHERE d.dept_id = e.dept_id
);
SQL(IN を使う代替パターン)
-- IN で絞り込む(小規模データ向け)
UPDATE employees
SET dept_name = '本社'
WHERE dept_id IN (
    SELECT dept_id FROM departments WHERE location = 'Tokyo'
);

-- 大量データでは EXISTS の方がパフォーマンスが良いことが多い

3 方式のパフォーマンス比較と使い分け

項目 相関サブクエリ 更新可能結合ビュー MERGE INTO
構文のシンプルさ 普通(SET + WHERE で重複あり) 最もシンプル(JOIN 1 回) 普通(USING + ON + WHEN MATCHED)
ORA-01779 のリスク なし あり(ユニーク制約が必要) なし
参照テーブルへのユニーク制約 不要 必要 不要
パフォーマンス(大量データ) △ 行ごとにサブクエリ実行
(インデックスがあれば改善)
◎ 結合 1 回で完了 ◎ 結合 1 回で完了
UPSERT(INSERT + UPDATE) 不可 不可 可能(WHEN NOT MATCHED THEN INSERT)
DELETE 連動 不可 不可 可能(WHEN MATCHED THEN DELETE 条件付き)
Oracle 以外の RDBMS ほぼ共通 Oracle 独自 多くの RDBMS で利用可能
使い分けの判断基準
少量データ + シンプルな更新 → 相関サブクエリ(どの環境でも動く)
大量データ + 参照テーブルにユニーク制約あり → 更新可能結合ビュー(最速)
大量データ + ユニーク制約なし / UPSERT が必要 → MERGE INTO(最も柔軟)
迷ったら MERGE INTO を選べば、制約の有無に関わらず安定して動作します。

RETURNING INTO 句で更新結果を取得する

PL/SQL では RETURNING INTO 句を使うことで、UPDATE で変更された行の値を変数やコレクションに取得できます。更新後の確認クエリが不要になり、処理効率が向上します。

SQL(RETURNING INTO の使用例)
DECLARE
    v_emp_id   NUMBER;
    v_new_name VARCHAR2(100);
BEGIN
    UPDATE employees
    SET dept_name = '新事業部'
    WHERE emp_id = 1001
    RETURNING emp_id, dept_name INTO v_emp_id, v_new_name;

    DBMS_OUTPUT.PUT_LINE('Updated: ' || v_emp_id || ' → ' || v_new_name);
    DBMS_OUTPUT.PUT_LINE('Rows affected: ' || SQL%ROWCOUNT);
END;
/
SQL(BULK COLLECT で複数行の結果を取得)
DECLARE
    TYPE t_ids IS TABLE OF NUMBER;
    v_ids t_ids;
BEGIN
    UPDATE employees e
    SET e.dept_name = (
        SELECT d.dept_name FROM departments d WHERE d.dept_id = e.dept_id
    )
    WHERE EXISTS (
        SELECT 1 FROM departments d WHERE d.dept_id = e.dept_id
    )
    RETURNING e.emp_id BULK COLLECT INTO v_ids;

    DBMS_OUTPUT.PUT_LINE('Updated ' || v_ids.COUNT || ' rows');
END;
/

実践パターン集

パターン①:マスタ変更をトランザクションテーブルに反映

SQL(マスタの部門名変更を受注テーブルに一括反映)
-- マスタの dept_name が変更されたとき、orders テーブルの表示用列を更新
MERGE INTO orders o
USING departments d
ON (o.dept_id = d.dept_id)
WHEN MATCHED THEN
    UPDATE SET o.dept_name_display = d.dept_name
    WHERE o.dept_name_display <> d.dept_name  -- 変更がある行のみ更新
       OR o.dept_name_display IS NULL;

パターン②:ステージングテーブルから本番テーブルへ一括取り込み

SQL(ステージングテーブルからの一括更新)
-- CSV 取り込み用ステージングから本番テーブルを更新
MERGE INTO products p
USING staging_products s
ON (p.product_id = s.product_id)
WHEN MATCHED THEN
    UPDATE SET
        p.product_name = s.product_name,
        p.price        = s.price,
        p.updated_at   = SYSDATE
WHEN NOT MATCHED THEN
    INSERT (product_id, product_name, price, created_at, updated_at)
    VALUES (s.product_id, s.product_name, s.price, SYSDATE, SYSDATE);

パターン③:集計結果を元テーブルに書き戻す

SQL(集計値を元テーブルに UPDATE)
-- 各部門の平均給与を departments テーブルに書き戻す
UPDATE departments d
SET d.avg_salary = (
    SELECT AVG(e.salary)
    FROM employees e
    WHERE e.dept_id = d.dept_id
)
WHERE EXISTS (
    SELECT 1 FROM employees e WHERE e.dept_id = d.dept_id
);

-- 社員がいない部門は NULL のまま残る(WHERE EXISTS のおかげ)

パターン④:ランク・順位を更新する

SQL(ウィンドウ関数の結果を UPDATE)
-- 部門内の給与順位を rank 列に格納
MERGE INTO employees e
USING (
    SELECT emp_id,
           DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS salary_rank
    FROM employees
) r
ON (e.emp_id = r.emp_id)
WHEN MATCHED THEN
    UPDATE SET e.salary_rank = r.salary_rank;

結合 UPDATE の注意点

注意点 詳細 対処
NULL 上書き 相関サブクエリで WHERE EXISTS を省略すると、結合先がない行が NULL に上書きされる 必ず WHERE EXISTS を付ける。または MERGE を使う
1:N 結合で多重更新 参照テーブルに重複行があると、同じ行が複数回更新される 参照テーブルをユニークにする。または MERGE の ON 条件でユニーク性を保証
ロック競合 大量行の UPDATE は長時間ロックを取得する ROWNUM で範囲を区切ってバッチ処理する。またはパーティション単位で処理
UNDO 領域不足 大量行の一括 UPDATE で ORA-30036(UNDO 領域不足)が発生する バッチサイズを小さくして COMMIT を分割する

よくある質問

QMySQL の UPDATE … JOIN 構文は Oracle で使えますか?
AOracle では UPDATE t1 JOIN t2 ON ... SET t1.col = t2.col の構文は使えません。代わりに、更新可能結合ビューUPDATE (SELECT ... FROM ... JOIN ...))、相関サブクエリ、または MERGE INTO を使います。
QORA-01779 が出ます。どうすればいいですか?
A結合相手テーブルの結合キーに PRIMARY KEY または UNIQUE INDEX がないことが原因です。対処法は 3 つあります。①結合キーにユニークインデックスを作成する(最も推奨)、②相関サブクエリ方式に書き換える、③MERGE INTO を使う。詳細は本記事の「ORA-01779 対処法」セクションを参照してください。
Q相関サブクエリ UPDATE はパフォーマンスが悪いですか?
A更新対象が少量(数百行程度)であれば問題ありません。ただし、大量データ(数万行以上)の場合は行ごとにサブクエリが実行されるため、MERGE INTO や更新可能結合ビューに比べて遅くなります。参照テーブルの結合キーにインデックスがあればある程度改善されますが、大量データでは MERGE INTO を推奨します。
QMERGE の WHEN MATCHED のみ書いて WHEN NOT MATCHED を省略できますか?
Aできます。WHEN MATCHED THEN UPDATE のみを書けば、結合条件に一致する行だけが更新され、一致しない行は無視されます。結合更新専用として MERGE を使う場合は WHEN NOT MATCHED を省略するのが一般的です。
QUPDATE で変更された行数を確認するには?
ASQL*Plus では UPDATE 実行後に「n rows updated.」と表示されます。PL/SQL では SQL%ROWCOUNT で直前の UPDATE の影響行数を取得できます。更新前後の値を取得したい場合は RETURNING INTO 句を使います。
Q3 つのテーブルを結合して UPDATE するには?
AMERGE の USING 句にサブクエリを記述すれば、3 テーブル以上の結合も可能です。
MERGE INTO target_table t
USING (SELECT ... FROM table_a JOIN table_b ON ...) src
ON (t.key = src.key)
WHEN MATCHED THEN UPDATE SET ...

相関サブクエリでも SET 句のサブクエリ内で JOIN できます。

まとめ

Oracle での結合を用いたデータ更新の要点を整理します。

やりたいこと 推奨方式
別テーブルの値で列を更新(少量データ) 相関サブクエリ UPDATE
SET col = (SELECT …)
別テーブルの値で列を更新(大量データ + ユニーク制約あり) 更新可能結合ビュー
UPDATE (SELECT … JOIN …)
別テーブルの値で列を更新(大量データ + ユニーク制約なし) MERGE INTO … WHEN MATCHED THEN UPDATE
あれば更新、なければ挿入(UPSERT) MERGE INTO … WHEN MATCHED / NOT MATCHED
対応するマスタがある行だけ更新 WHERE EXISTS (SELECT 1 FROM …)
対応するマスタがない行を更新 WHERE NOT EXISTS (SELECT 1 FROM …)
集計結果を元テーブルに書き戻す 相関サブクエリ
SET col = (SELECT AVG(…) …)
更新された行の値を取得したい RETURNING INTO 句

SQL 全般の UPDATE 文の基本については「UPDATE文でデータを更新する方法」、MERGE 文の UPSERT 活用については「MERGE文の活用法」も併せて参照してください。