- データベース
- 技術 💻
SQLで別テーブルの値を使ってUPDATEする方法【DB別】
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期連続の黒字決算。