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 句で更新結果を受け取る方法
・マスタ反映・ステージング取り込みの実務パターン
サンプルテーブル(以降の例で使用)
-- 社員テーブル(更新対象)
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 句にサブクエリを記述し、外側の行と結合条件で紐づけます。
-- 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 を省略すると、departments に対応する行がない employees の dept_name がNULL に上書きされます。意図しない NULL 化を防ぐため、WHERE EXISTS は必ず付けるのが安全です。
複数列を同時に更新する
-- 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 に相当する機能ですが、「キー保存表」の制約がある点が大きく異なります。
-- インラインビューを 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 が簡潔になります。
複数列の更新
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 が発生します。
-- 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 */ ヒントで制約チェックを回避 | 非推奨(非公開ヒント。将来削除の可能性あり。データ不整合リスク) |
-- 参照テーブルの結合キーにユニークインデックスを作成
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
結合結果において、そのテーブルの主キーが結合後も一意性を保っているテーブルのことです。参照テーブル(JOIN 相手)の結合キーにユニーク制約があれば、更新対象テーブルの各行は結合後も 1 行のままなので「キー保存」されます。結合が 1:N になる可能性があるとキー保存されず、ORA-01779 が発生します。
方式③:MERGE INTO を使った結合更新
MERGE INTO はもともと「あれば更新、なければ挿入(UPSERT)」のための構文ですが、WHEN MATCHED のみを使えば結合更新としても利用できます。ORA-01779 エラーが発生しないため、ユニーク制約を追加できない場合に特に有用です。
-- 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;
-- 複数列の更新 + 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 で更新対象を絞り込む
-- マスタに存在する部門のみ 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
);
-- マスタに存在しない部門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
);
-- 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 で変更された行の値を変数やコレクションに取得できます。更新後の確認クエリが不要になり、処理効率が向上します。
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;
/
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;
/
実践パターン集
パターン①:マスタ変更をトランザクションテーブルに反映
-- マスタの 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;
パターン②:ステージングテーブルから本番テーブルへ一括取り込み
-- 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);
パターン③:集計結果を元テーブルに書き戻す
-- 各部門の平均給与を 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 のおかげ)
パターン④:ランク・順位を更新する
-- 部門内の給与順位を 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 を分割する |
よくある質問
UPDATE t1 JOIN t2 ON ... SET t1.col = t2.col の構文は使えません。代わりに、更新可能結合ビュー(UPDATE (SELECT ... FROM ... JOIN ...))、相関サブクエリ、または MERGE INTO を使います。WHEN MATCHED THEN UPDATE のみを書けば、結合条件に一致する行だけが更新され、一致しない行は無視されます。結合更新専用として MERGE を使う場合は WHEN NOT MATCHED を省略するのが一般的です。n rows updated.」と表示されます。PL/SQL では SQL%ROWCOUNT で直前の UPDATE の影響行数を取得できます。更新前後の値を取得したい場合は RETURNING INTO 句を使います。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文の活用法」も併せて参照してください。

