SQLで別のテーブルの値を使ってUPDATEするには、MySQLでは UPDATE ... JOIN、PostgreSQLとSQL Serverでは UPDATE ... FROM、Oracleではサブクエリか MERGE を使います。どのデータベースでも使える書き方として、SET句に相関サブクエリを書く方法もあります。

ただし、この処理にはエラーにならずにデータを壊す落とし穴があります。特に、対応する行がないと、値がNULLで上書きされる事故はよく起きます。この記事では、データベース別の書き方と、こうした落とし穴、更新前の確認手順を、PostgreSQLで実際に実行した結果とあわせて解説します。

例に使うテーブル

商品の価格を、価格改定のテーブルの内容で更新します。

products(商品)

id name price
1 ノート 200
2 ペン 100
3 消しゴム 80

price_updates(価格改定)

product_id new_price
1 220
2 110

消しゴム(id=3)は、価格改定の対象ではありません。更新後は、ノート220円、ペン110円、消しゴムは80円のままになるのが正しい結果です。

データベース別の書き方

MySQL:UPDATE ... JOIN

UPDATE products p
JOIN price_updates u ON u.product_id = p.id
SET p.price = u.new_price;

PostgreSQL:UPDATE ... FROM

UPDATE products p
SET price = u.new_price
FROM price_updates u
WHERE u.product_id = p.id;

PostgreSQLでは、SET句の左側の列に、テーブルの別名(p.price)を付けられない点に注意します。

SQL Server:UPDATE ... FROM ... JOIN

UPDATE p
SET p.price = u.new_price
FROM products p
JOIN price_updates u ON u.product_id = p.id;

Oracle:MERGE(またはサブクエリ)

MERGE INTO products p
USING price_updates u
ON (p.id = u.product_id)
WHEN MATCHED THEN
  UPDATE SET p.price = u.new_price;

どのデータベースでも使える書き方:相関サブクエリ

UPDATE products p
SET price = (
  SELECT u.new_price FROM price_updates u WHERE u.product_id = p.id
)
WHERE EXISTS (
  SELECT 1 FROM price_updates u WHERE u.product_id = p.id
);

WHERE EXISTS の部分が非常に重要です。次の「落とし穴1」で理由を説明します。

データベース 書き方
MySQL UPDATE ... JOIN ... SET
PostgreSQL UPDATE ... SET ... FROM ... WHERE
SQL Server UPDATE 別名 SET ... FROM ... JOIN
Oracle MERGE、または相関サブクエリ
共通 相関サブクエリ+WHERE EXISTS

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「SQLで別テーブルの値を使ってUPDATEする方法【DB別】」

落とし穴1:対応する行がないと、NULLで上書きされる

相関サブクエリで、WHERE EXISTS を書き忘れると、次のようになります。

UPDATE products p
SET price = (SELECT u.new_price FROM price_updates u WHERE u.product_id = p.id);
id name price
1 ノート 220
2 ペン 110
3 消しゴム (NULL)

価格改定の対象ではない消しゴムの価格が、NULLになってしまいました。UPDATEにWHEREがないため、すべての行が更新対象になり、サブクエリで対応する行が見つからない消しゴムには、NULLが設定されたのです。エラーにはならないため、気づかないまま本番のデータが壊れる、典型的な事故です。

対策

  • 相関サブクエリを使うときは、必ず WHERE EXISTS(または WHERE id IN (...))で対象を絞る
  • UPDATE ... JOIN や UPDATE ... FROM の書き方なら、対応する行があるものだけが更新される
  • 列に NOT NULL制約を付けておくと、NULLでの上書きはエラーで止まる

落とし穴2:対応する行が複数あると、結果が決まらない

価格改定のテーブルに、同じ商品の行が2つあった場合です。

-- price_updates に (1, 220) と (1, 999) の2行がある

UPDATE products p
SET price = u.new_price
FROM price_updates u
WHERE u.product_id = p.id;

PostgreSQLで実行すると、エラーにならずに、どちらか一方の値(この例では220)で更新されました。どちらが使われるかは決まっておらず、データの並びや実行計画によって変わる可能性があります。MySQLのUPDATE ... JOINでも同じように、どちらの値になるかは保証されません。

一方、相関サブクエリの書き方では、エラーで止まります。

UPDATE products p
SET price = (SELECT u.new_price FROM price_updates u WHERE u.product_id = p.id)
WHERE EXISTS (SELECT 1 FROM price_updates u WHERE u.product_id = p.id);
-- ERROR: more than one row returned by a subquery used as an expression

対策

  • 更新の前に、参照するテーブルに重複がないかを確認する
SELECT product_id, COUNT(*) FROM price_updates
GROUP BY product_id HAVING COUNT(*) > 1;
  • 複数ある場合にどれを使うか(最新の日付のものなど)を決め、サブクエリで1行に絞ってから結合する
  • 参照するテーブルの結合キーに、ユニーク制約を付けておく

更新する前に、必ずSELECTで確認する

UPDATEは、実行する前に、同じ条件でSELECTして、更新前と更新後の値を並べて確認します。

SELECT p.id, p.name, p.price AS 現在の価格, u.new_price AS 新しい価格
FROM products p
JOIN price_updates u ON u.product_id = p.id;
id name 現在の価格 新しい価格
1 ノート 200 220
2 ペン 100 110

件数と値が想定どおりかを確かめてから、トランザクションの中でUPDATEを実行します。

BEGIN;

UPDATE products p
SET price = u.new_price
FROM price_updates u
WHERE u.product_id = p.id;
-- UPDATE 2  ← 更新件数がSELECTの件数と一致するか確認

SELECT * FROM products ORDER BY id;   -- 結果を確認

COMMIT;      -- 問題なければ確定
-- ROLLBACK; -- 想定と違えば取り消す

更新件数(UPDATE 2)が、事前のSELECTの件数と一致するかが、最も簡単で確実なチェックです。

登録と更新をまとめる:MERGE・UPSERT

「対応する行があれば更新、なければ登録」をまとめて行うには、MERGEやUPSERTを使います。

MERGE(PostgreSQL 15以降・SQL Server・Oracle)

MERGE INTO products p
USING price_updates u
ON p.id = u.product_id
WHEN MATCHED THEN
  UPDATE SET price = u.new_price
WHEN NOT MATCHED THEN
  INSERT (id, name, price) VALUES (u.product_id, '新商品', u.new_price);

PostgreSQLのMERGEでは、同じ行が複数回更新される場合はエラーになるため、落とし穴2の重複にも気づけます。

UPSERT(PostgreSQL・MySQL)

-- PostgreSQL
INSERT INTO products (id, name, price) VALUES (4, 'のり', 150)
ON CONFLICT (id) DO UPDATE SET price = EXCLUDED.price;

-- MySQL 8.0.19以降(それより前は price = VALUES(price) と書く)
INSERT INTO products (id, name, price) VALUES (4, 'のり', 150) AS new
ON DUPLICATE KEY UPDATE price = new.price;

UPSERTは、主キーやユニーク制約の重複をきっかけに更新に切り替えるため、対象の列に制約があることが前提です。

同じテーブルの別の行の値で更新する

同じテーブル内の値を使う場合も、考え方は同じです。

-- PostgreSQL:同じカテゴリの最安値に合わせる例
UPDATE products p
SET price = m.min_price
FROM (
  SELECT category_id, MIN(price) AS min_price FROM products GROUP BY category_id
) m
WHERE m.category_id = p.category_id;

MySQLで、更新するテーブルと同じテーブルをサブクエリで参照するとエラー(1093)になる場合は、派生テーブル(FROM句のサブクエリ)でいったん結果を作ってからJOINします。

大量の行を更新するとき

  • 一度に数百万行を更新すると、ロックが長く続き、他の処理が止まることがある
  • 主キーの範囲で分けて、数千〜数万行ずつ更新する
  • 更新する列にインデックスがある場合、インデックスの更新にも時間がかかる
  • 更新後は、統計情報を更新する(PostgreSQLなら ANALYZE)

よくある質問

Q. UPDATEで複数の列を同時に更新できますか? A. できます。SET price = u.new_price, updated_at = now() のように、カンマで区切って並べます。

Q. 更新した行を確認しながらUPDATEしたいです。 A. PostgreSQLでは UPDATE ... RETURNING * で、更新後の行をそのまま表示できます。SQL Serverでは OUTPUT 句が使えます。

Q. WHEREのないUPDATEを防ぐ方法はありますか? A. MySQLでは SET SQL_SAFE_UPDATES = 1; で、キーを使った条件のないUPDATE・DELETEを禁止できます。クライアントツールによっては、同様の設定があります。

Q. 更新前の値を残しておきたいです。 A. 事前に対象の行を別のテーブルにコピーするか、更新履歴を記録するテーブルやトリガーを用意します。本番での一括更新では、元に戻す手順も用意しておくと安心です。

まとめ

  • MySQLは UPDATE ... JOIN、PostgreSQL・SQL Serverは UPDATE ... FROM、Oracleは MERGE かサブクエリ
  • 相関サブクエリは WHERE EXISTS を必ず書く。書かないと、対応のない行がNULLで上書きされる
  • 対応する行が複数あると、どの値で更新されるか決まらない。事前に重複を確認する
  • 更新前にSELECTで件数と値を確認し、トランザクションの中で実行して件数を照合する
  • 登録と更新をまとめるなら MERGE・UPSERT

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「SQLで別テーブルの値を使ってUPDATEする方法【DB別】」

株式会社RJCでは、データを更新する処理ほど、慎重な確認の手順を大切にしています。こうした知識を仲間と共有しながら、一緒に成長を楽しんでいます。

株式会社RJC ― SI事業・SES事業・AI駆動開発。未経験からエンジニアに育てるのが得意な会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?