【SQL】サブクエリでUPDATE完全ガイド|相関サブクエリ・UPDATE JOIN・CTE・MERGE・集約値更新・RDBMS別構文まで

「別テーブルから値を取得してUPDATEしたい」という要件はSQL開発で頻出します。しかし、サブクエリを使ったUPDATEはRDBMSごとに構文が異なり、パフォーマンスにも差が出やすいポイントです。

本記事では、MySQL・PostgreSQL・SQL Server・Oracleでサブクエリを使ったUPDATEの書き方を体系的に解説します。どの方法をいつ使うべきかの判断基準も示します。

この記事で分かること

  • SET句に相関サブクエリを使って別テーブルの値で更新する方法
  • WHERE句にEXISTS / INを使って更新対象を絞り込む方法
  • UPDATE JOIN(FROM句)でサブクエリを使わずに更新する方法
  • CTE(WITH句)を使ったUPDATEの書き方
  • MERGE文で条件付きUPDATE/INSERTを1文で行う方法
  • 集約関数の結果で更新する方法(SUM/AVG/MAXなど)
  • 複数列を同時にサブクエリで更新する方法
スポンサーリンク

方法の選び方

サブクエリでのUPDATEにはいくつかの方法があります。状況に応じて最適な方法を選びましょう。

方法 使いどころ 対応RDBMS
SET句の相関サブクエリ 標準SQL。どのRDBMSでも使える基本形 全RDBMS
UPDATE JOIN(FROM句) JOINで結合した結果で更新。高速・直感的 MySQL / PostgreSQL / SQL Server
CTE(WITH句)+ UPDATE 複雑な集計結果を使って更新 PostgreSQL / SQL Server / Oracle(一部)
MERGE文 一致行は更新・不一致行は挿入(UPSERT) SQL Server / Oracle / PostgreSQL(15+)

SET句の相関サブクエリ — 全RDBMS共通

最も基本的な方法です。SET句のサブクエリが更新対象の各行に対して実行され、別テーブルの値を取得して更新します。標準SQLに準拠しているため、どのRDBMSでも動作します。

基本形:別テーブルの値で更新
-- 給与変更テーブルの最新値で従業員テーブルを更新
UPDATE employees e
SET e.salary = (
    SELECT sc.new_salary
    FROM salary_changes sc
    WHERE sc.employee_id = e.employee_id
    ORDER BY sc.change_date DESC
    LIMIT 1                          -- MySQL: 最新の1件
)
WHERE e.employee_id IN (
    SELECT sc.employee_id FROM salary_changes sc
);

WHERE句を忘れない:WHERE句がないと、salary_changesに対応する行がない従業員のsalaryがNULLになります。WHERE IN / WHERE EXISTS で更新対象を絞り込むのが鉄則です。

WHERE EXISTS で更新対象を絞り込む
-- EXISTS のほうが IN より高速になることが多い(特に大量データ)
UPDATE employees e
SET e.salary = (
    SELECT sc.new_salary
    FROM salary_changes sc
    WHERE sc.employee_id = e.employee_id
    ORDER BY sc.change_date DESC
    LIMIT 1
)
WHERE EXISTS (
    SELECT 1 FROM salary_changes sc
    WHERE sc.employee_id = e.employee_id
);

Oracle / SQL Server での相関サブクエリ

Oracle では LIMIT 1 の代わりに ROWNUMFETCH FIRST を使います。SQL Server は TOP 1 です。

Oracle — 最新値で更新
-- Oracle: FETCH FIRST 1 ROW ONLY(12c+)
UPDATE employees e
SET e.salary = (
    SELECT sc.new_salary
    FROM salary_changes sc
    WHERE sc.employee_id = e.employee_id
    ORDER BY sc.change_date DESC
    FETCH FIRST 1 ROW ONLY
)
WHERE EXISTS (
    SELECT 1 FROM salary_changes sc
    WHERE sc.employee_id = e.employee_id
);

-- Oracle: ROWNUM(11g以前)
UPDATE employees e
SET e.salary = (
    SELECT new_salary FROM (
        SELECT sc.new_salary
        FROM salary_changes sc
        WHERE sc.employee_id = e.employee_id
        ORDER BY sc.change_date DESC
    ) WHERE ROWNUM = 1
)
WHERE EXISTS (
    SELECT 1 FROM salary_changes sc
    WHERE sc.employee_id = e.employee_id
);
SQL Server — TOP 1 で最新値を取得
UPDATE e
SET e.salary = (
    SELECT TOP 1 sc.new_salary
    FROM salary_changes sc
    WHERE sc.employee_id = e.employee_id
    ORDER BY sc.change_date DESC
)
FROM employees e
WHERE EXISTS (
    SELECT 1 FROM salary_changes sc
    WHERE sc.employee_id = e.employee_id
);

UPDATE JOIN(FROM句) — 結合して更新

サブクエリの代わりにJOINで結合した結果を直接SETに使う方法です。相関サブクエリより読みやすく、パフォーマンスも良い場合が多いです。

UPDATE JOIN はRDBMSごとに構文が異なります。MySQL は UPDATE ... JOIN ... SET、PostgreSQL / SQL Server は UPDATE ... SET ... FROM です。

MySQL — UPDATE JOIN
-- MySQL: UPDATE ... JOIN ... SET の構文
UPDATE employees e
INNER JOIN salary_changes sc
    ON e.employee_id = sc.employee_id
SET e.salary     = sc.new_salary,
    e.updated_at = NOW()
WHERE sc.change_date = (
    SELECT MAX(sc2.change_date)
    FROM salary_changes sc2
    WHERE sc2.employee_id = sc.employee_id
);
PostgreSQL — UPDATE … SET … FROM
-- PostgreSQL: FROM句にJOIN先を書く
UPDATE employees e
SET salary     = sc.new_salary,
    updated_at = NOW()
FROM salary_changes sc
WHERE e.employee_id = sc.employee_id
  AND sc.change_date = (
    SELECT MAX(sc2.change_date)
    FROM salary_changes sc2
    WHERE sc2.employee_id = sc.employee_id
  );
SQL Server — UPDATE … SET … FROM
-- SQL Server: PostgreSQLと似た FROM句構文
UPDATE e
SET e.salary     = sc.new_salary,
    e.updated_at = GETDATE()
FROM employees e
INNER JOIN salary_changes sc
    ON e.employee_id = sc.employee_id
WHERE sc.change_date = (
    SELECT MAX(sc2.change_date)
    FROM salary_changes sc2
    WHERE sc2.employee_id = sc.employee_id
);
Oracle — UPDATE JOIN はインラインビューで実現
-- Oracle: 更新可能なインラインビュー(キー保存表が必要)
UPDATE (
    SELECT e.salary    AS old_salary,
           sc.new_salary AS new_salary
    FROM employees e
    INNER JOIN salary_changes sc
        ON e.employee_id = sc.employee_id
)
SET old_salary = new_salary;
-- ※ sc.employee_id に一意制約/PKが必要(キー保存表の条件)

Oracle のインラインビューUPDATE:結合先テーブルの結合キーに一意制約(PK / UNIQUE)がないと「ORA-01779: キー保存されていない表にマップする列は変更できません」エラーになります。この制限がある場合は MERGE 文を使います。

CTE(WITH句)で更新する

複雑な集計結果をサブクエリで使いたい場合、CTE(WITH句)で先に結果セットを定義してからUPDATEに使うと読みやすくなります。

PostgreSQL — CTE + UPDATE
-- 部署ごとの平均給与で従業員テーブルを更新
WITH dept_avg AS (
    SELECT department_id,
           AVG(salary) AS avg_salary
    FROM employees
    GROUP BY department_id
)
UPDATE employees e
SET salary = da.avg_salary
FROM dept_avg da
WHERE e.department_id = da.department_id
  AND e.role = 'junior';  -- ジュニアの給与を部署平均に揃える
SQL Server — CTE + UPDATE
-- SQL Serverでも同様にCTEでUPDATE可能
WITH dept_avg AS (
    SELECT department_id,
           AVG(salary) AS avg_salary
    FROM employees
    GROUP BY department_id
)
UPDATE e
SET e.salary = da.avg_salary
FROM employees e
INNER JOIN dept_avg da
    ON e.department_id = da.department_id
WHERE e.role = 'junior';
MySQL — CTE + UPDATE(8.0+)
-- MySQL 8.0+ でCTEが使用可能
WITH dept_avg AS (
    SELECT department_id,
           AVG(salary) AS avg_salary
    FROM employees
    GROUP BY department_id
)
UPDATE employees e
INNER JOIN dept_avg da
    ON e.department_id = da.department_id
SET e.salary = da.avg_salary
WHERE e.role = 'junior';

MERGE文 — 条件付き更新と挿入を同時に

MERGE文は「一致すれば更新、不一致なら挿入」を1つのSQLで行えます。いわゆるUPSERT処理に最適です。

SQL Server — MERGE
-- salary_changes をもとに employees を更新(なければ挿入)
MERGE INTO employees AS target
USING salary_changes AS source
    ON target.employee_id = source.employee_id
WHEN MATCHED THEN
    UPDATE SET target.salary = source.new_salary,
               target.updated_at = GETDATE()
WHEN NOT MATCHED THEN
    INSERT (employee_id, salary, updated_at)
    VALUES (source.employee_id, source.new_salary, GETDATE());
Oracle — MERGE
MERGE INTO employees target
USING salary_changes source
    ON (target.employee_id = source.employee_id)
WHEN MATCHED THEN
    UPDATE SET target.salary = source.new_salary,
               target.updated_at = SYSDATE
WHEN NOT MATCHED THEN
    INSERT (employee_id, salary, updated_at)
    VALUES (source.employee_id, source.new_salary, SYSDATE);
MySQL — INSERT ON DUPLICATE KEY UPDATE(MERGE の代替)
-- MySQL には MERGE がないが、INSERT ON DUPLICATE KEY UPDATE で代替
INSERT INTO employees (employee_id, salary, updated_at)
SELECT employee_id, new_salary, NOW()
FROM salary_changes
ON DUPLICATE KEY UPDATE
    salary     = VALUES(salary),
    updated_at = VALUES(updated_at);
-- employee_id に一意制約が必要

集約値で更新する

SUM / AVG / MAX / COUNT の結果で別テーブルを更新するパターンは、集計テーブルの定期更新などで頻出します。

注文合計で顧客テーブルを更新(相関サブクエリ)
-- 全RDBMS共通
UPDATE customers c
SET c.total_spent = (
    SELECT SUM(o.amount)
    FROM orders o
    WHERE o.customer_id = c.customer_id
)
WHERE EXISTS (
    SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id
);
注文合計で顧客テーブルを更新(UPDATE JOIN)
-- PostgreSQL: FROM句で集約結果をJOIN
UPDATE customers c
SET total_spent = o_sum.total_amount
FROM (
    SELECT customer_id,
           SUM(amount) AS total_amount
    FROM orders
    GROUP BY customer_id
) o_sum
WHERE c.customer_id = o_sum.customer_id;

-- MySQL: JOINでサブクエリを結合
UPDATE customers c
INNER JOIN (
    SELECT customer_id,
           SUM(amount) AS total_amount
    FROM orders
    GROUP BY customer_id
) o_sum ON c.customer_id = o_sum.customer_id
SET c.total_spent = o_sum.total_amount;
注文件数と最終注文日を同時更新
-- 複数の集約値を一度に更新する
UPDATE customers c
SET c.order_count = (
        SELECT COUNT(*)
        FROM orders o WHERE o.customer_id = c.customer_id
    ),
    c.last_order_date = (
        SELECT MAX(o.order_date)
        FROM orders o WHERE o.customer_id = c.customer_id
    )
WHERE EXISTS (
    SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id
);

複数列を同時にサブクエリで更新する

複数の列を別テーブルの値で一度に更新する場合、SET句にサブクエリを複数書く方法と、行コンストラクタを使う方法があります。

複数列の同時更新(個別サブクエリ)
-- 全RDBMS共通: SET句にサブクエリを複数書く
UPDATE products p
SET p.price = (
        SELECT s.new_price FROM price_updates s
        WHERE s.product_id = p.product_id
    ),
    p.updated_at = (
        SELECT s.effective_date FROM price_updates s
        WHERE s.product_id = p.product_id
    )
WHERE EXISTS (
    SELECT 1 FROM price_updates s WHERE s.product_id = p.product_id
);
-- ※ サブクエリが2回実行されるためパフォーマンスに注意
Oracle — 行コンストラクタで効率的に複数列更新
-- Oracle: (列1, 列2) = (サブクエリ) で1回のサブクエリで済む
UPDATE products p
SET (p.price, p.updated_at) = (
    SELECT s.new_price, s.effective_date
    FROM price_updates s
    WHERE s.product_id = p.product_id
)
WHERE EXISTS (
    SELECT 1 FROM price_updates s WHERE s.product_id = p.product_id
);
-- サブクエリの実行が1回で済むため効率的
UPDATE JOIN で複数列更新(推奨)
-- MySQL
UPDATE products p
INNER JOIN price_updates s ON p.product_id = s.product_id
SET p.price      = s.new_price,
    p.updated_at = s.effective_date;

-- PostgreSQL
UPDATE products p
SET price      = s.new_price,
    updated_at = s.effective_date
FROM price_updates s
WHERE p.product_id = s.product_id;

-- SQL Server
UPDATE p
SET p.price      = s.new_price,
    p.updated_at = s.effective_date
FROM products p
INNER JOIN price_updates s ON p.product_id = s.product_id;

複数列の更新には UPDATE JOIN が最も効率的:相関サブクエリを複数書くと列の数だけサブクエリが実行されます。UPDATE JOIN なら1回の結合で済みます。

自テーブルのサブクエリで更新する

同じテーブルの別の行の値を使って更新する場合も、サブクエリが使えます。

同じテーブルの値で更新(自己参照)
-- 上司の給与を部下のMAX給与 + 10% に設定
UPDATE employees e
SET e.salary = (
    SELECT MAX(sub.salary) * 1.1
    FROM employees sub
    WHERE sub.manager_id = e.employee_id
)
WHERE e.employee_id IN (
    SELECT DISTINCT manager_id FROM employees WHERE manager_id IS NOT NULL
);

-- MySQL の場合: 同一テーブルのサブクエリは直接UPDATE+SELECTできない制限あり
-- → サブクエリをさらにサブクエリで包む
UPDATE employees e
SET e.salary = (
    SELECT max_salary FROM (
        SELECT manager_id, MAX(salary) * 1.1 AS max_salary
        FROM employees
        WHERE manager_id IS NOT NULL
        GROUP BY manager_id
    ) tmp
    WHERE tmp.manager_id = e.employee_id
)
WHERE e.employee_id IN (
    SELECT DISTINCT manager_id FROM employees WHERE manager_id IS NOT NULL
);

MySQL の制限:MySQL では UPDATE 文のサブクエリで更新対象と同じテーブルを直接参照すると「ERROR 1093: You can’t specify target table for update in FROM clause」が発生します。サブクエリをさらにサブクエリで囲む(派生テーブル化する)ことで回避できます。

パフォーマンスの注意点

注意点 対処方法
相関サブクエリは行ごとに実行される UPDATE JOIN や MERGE に書き換えて結合1回で済ませる
サブクエリの結合キーにインデックスがない WHERE句の結合キーにインデックスを追加する
大量行のUPDATEでロックが広がる バッチ分割(WHERE id BETWEEN … AND …)でコミット
NOT INサブクエリがNULLで誤動作 NOT EXISTS に書き換える(NOT IN完全ガイドを参照)
大量データのバッチ更新
-- 10万行を1000行ずつバッチ更新(MySQL例)
SET @batch = 0;
REPEAT
    UPDATE employees e
    INNER JOIN salary_changes sc ON e.employee_id = sc.employee_id
    SET e.salary = sc.new_salary
    WHERE e.employee_id > @batch
    ORDER BY e.employee_id
    LIMIT 1000;
    SET @batch = @batch + 1000;
UNTIL ROW_COUNT() = 0 END REPEAT;

まとめ

方法 メリット デメリット 対応RDBMS
SET句の相関サブクエリ 標準SQL・全RDBMS対応 行ごとに実行・遅い場合あり 全RDBMS
UPDATE JOIN 高速・直感的 構文がRDBMSごとに異なる MySQL / PG / SQL Server
CTE + UPDATE 複雑な集計が読みやすい Oracle非対応(一部) PG / SQL Server / MySQL 8.0+
MERGE UPSERT が1文で完結 MySQL非対応 SQL Server / Oracle / PG 15+
  • 全RDBMSで動く基本形はSET句の相関サブクエリ + WHERE EXISTS
  • パフォーマンスを優先するならUPDATE JOIN(構文はRDBMS別に覚える)
  • 複雑な集計結果で更新するならCTE(WITH句)で可読性を確保
  • 「あれば更新・なければ挿入」にはMERGE(MySQL は INSERT ON DUPLICATE KEY UPDATE)
  • WHERE句の絞り込みを忘れるとNULLで上書きされるので必ず EXISTS / IN で対象を限定する
  • UPDATE文の基礎についてはUPDATE完全ガイドを、CASE WHENによる条件付き更新はCASE WHEN完全ガイドを参照してください