2つのテーブルにまたがるUPDATEとDELETE

別テーブルを基準に UPDATE や DELETE を行う構文は、エンジンごとに異なります。まずどこでも使えるサブクエリの形を見てから、結合構文を比べます。

このページはデータを変更します。変わるのはブラウザにあるあなた自身のサンプルデータベースです。やり直したいときはウェルカム画面から読み込み直してください。

どこでも動く形

UPDATE の WHERE は普通の WHERE なので、サブクエリで行を選べます。これに特別な構文を要求するエンジンはありません。

1

-- Works on every engine in this list, unchanged.
UPDATE customers
SET loyalty_points = loyalty_points + 50
WHERE id IN (
  SELECT customer_id FROM orders WHERE status = 'refunded' AND total > 2000
);
Planche で実行

242 行を変更

ブラウザの別タブで Planche が開き、サンプルデータベースを読み込んだ状態でこのクエリが入ります。このページの例はどれも同じタブを使い回します。F9 を押すまで実行はされません。

新しい値そのものが別のテーブルから来るなら、SET の中の相関サブクエリがそれをやります。

2

-- The same shape when the new value comes from the other table.
UPDATE products
SET stock_quantity = stock_quantity + (
  SELECT COALESCE(SUM(change_qty), 0)
  FROM inventory_logs il
  WHERE il.product_id = products.id
    AND il.reason = 'return'
)
WHERE id IN (SELECT product_id FROM inventory_logs WHERE reason = 'return');
Planche で実行

203 行を変更

条件が2回書かれていることに注目してください。SET のサブクエリが値を決め、WHERE のほうがそもそもどの行に触るかを決めています。後者がないと全商品が更新対象になり、対応するログがない商品は COALESCE(..., 0) で何も足さないまま、それでも行が書き込まれます。

この形は移植でき、読みやすくもあります。ただし常に最速ではありません。エンジンが相関サブクエリを行ごとに実行することもあり、そうなっているかは実行計画が教えてくれます。以下の結合構文が存在するのは、大量更新ではその差が移植性を捨てるだけの価値を持つからです。

MySQL と MariaDB

MySQL / MariaDB

-- The join goes between UPDATE and SET.
UPDATE customers c
  JOIN orders o ON o.customer_id = c.id
SET c.loyalty_points = c.loyalty_points + 10
WHERE o.status = 'refunded';

-- DELETE names which table's rows to remove, before FROM.
DELETE c
FROM customers c
  JOIN orders o ON o.customer_id = c.id
WHERE o.status = 'refunded';

MySQL / MariaDB — 比較のために載せています。ブラウザ内のサンプルは SQLite なので、この文はここでは実行できません。

結合が UPDATE と SET のあいだに入ります。ここに挙げた他のどのエンジンとも違う形です。DELETE では対象を FROM の前に書く必要があります(DELETE c FROM customers c JOIN ...)。2つのテーブルを結合していると、どちらの行を消すのかが曖昧になるからです。この別名を書き忘れると構文エラーになりますが、それはまだ良いほうです。間違ったほうを書いた場合はエラーになりません。

PostgreSQL

PostgreSQL

-- The second table goes in FROM, and the join condition in WHERE.
UPDATE customers
SET loyalty_points = loyalty_points + 10
FROM orders o
WHERE o.customer_id = customers.id
  AND o.status = 'refunded';

-- DELETE uses USING rather than FROM for the extra table.
DELETE FROM wishlists
USING products p
WHERE p.id = wishlists.product_id
  AND p.stock_quantity = 0;

PostgreSQL — 比較のために載せています。ブラウザ内のサンプルは SQLite なので、この文はここでは実行できません。

追加のテーブルは FROM に置き、結合条件は WHERE に移ります。間違えやすい点が2つ。対象のテーブルは FROM に書きません(書くと自分自身と結合してしまいます)。そして UPDATE が FROM を使うのに対し、DELETE は USING を使います。

SQLite は 3.33(2020年)で PostgreSQL と同じ意味の UPDATE … FROM を取り入れました。このページの3つめの例がここで実行できるのはそのためです。

3

-- SQLite 3.33 (2020) and later, with PostgreSQL's spelling.
UPDATE customers
SET loyalty_points = loyalty_points + 10
FROM orders o
WHERE o.customer_id = customers.id
  AND o.status = 'refunded'
  AND o.total > 2500;
Planche で実行

224 行を変更

SQLite に DELETE … USING はないので、削除は今もサブクエリで書きます。

SQL Server

SQL Server

-- The alias is updated, and the FROM clause repeats the target table.
UPDATE c
SET c.loyalty_points = c.loyalty_points + 10
FROM customers c
  JOIN orders o ON o.customer_id = c.id
WHERE o.status = 'refunded';

DELETE c
FROM customers c
  JOIN orders o ON o.customer_id = c.id
WHERE o.status = 'refunded';

SQL Server — 比較のために載せています。ブラウザ内のサンプルは SQLite なので、この文はここでは実行できません。

更新されるのは別名で、FROM が対象テーブルを繰り返して、その別名に定義を与えます。1つの文でテーブル名が2回出てくるので妙に見えますが、T-SQL が受け付けるのはこの形だけで、DELETE も同じパターンに従います。

Oracle

Oracle

-- Oracle has no UPDATE ... FROM and no UPDATE ... JOIN.
-- EXISTS is the portable answer, and the one to use.
UPDATE customers c
SET c.loyalty_points = c.loyalty_points + 10
WHERE EXISTS (
  SELECT 1 FROM orders o
  WHERE o.customer_id = c.id AND o.status = 'refunded'
);

DELETE FROM wishlists w
WHERE EXISTS (
  SELECT 1 FROM products p
  WHERE p.id = w.product_id AND p.stock_quantity = 0
);

Oracle — 比較のために載せています。ブラウザ内のサンプルは SQLite なので、この文はここでは実行できません。

Oracle には UPDATE … JOIN も UPDATE … FROM もありません。相関サブクエリを使った EXISTS が通常の答えで、これは他の4つのエンジンでもそのまま動く文です。

もうひとつの選択肢として、更新可能な結合ビュー(UPDATE (SELECT a.col, b.col FROM a JOIN b ON …) SET …)がありますが、Oracle は結合がキーを保存すると証明できるときにしか受け付けません。そのためには結合した列に一意制約か主キーが必要です。証明できない場合は ORA-01779 になり、直し方は EXISTS に戻ることです。

実行する前に

Planche が UPDATE と DELETE を実行前に対応する SELECT へ書き換えているのは、まさにこういう文があるからです。結合が入ると、影響する行の範囲は素の WHERE よりずっと読み取りにくくなります。設定 › 安全性 › 影響行のプレビューを有効にすれば、件数とサンプルが先に出ます。この書き換えは上の5つの形をすべて理解していて、リポジトリには、導いた件数が実際の文で変わる行数と一致するかを MySQL、PostgreSQL、SQL Server、SQLite、Oracle で検証するスクリプトも入っています。