結論

SQL の UPDATE 文は UPDATE テーブル名 SET 列 = 値 WHERE 条件; と書きます。複数列は SET の中をカンマで区切り、WHERE を省くと全行が対象です。

この記事でわかること

  • 状況別13パターンの早見表(複数列・CASE・別テーブル)
  • 5つの DB の構文対応表(JOIN・LIMIT・件数)
  • WHERE なしの事故を ROLLBACK で戻す5ステップ

2026年10月16日更新|PostgreSQL 16・MySQL 8.0・SQLite 3.45 で確認

UPDATE 文は SET で新しい値を決め、WHERE に合う行だけを書き換えます。

flowchart TD
  U[UPDATE items SET price = 150] --> W{WHERE id = 2 を書いたか}
  W -- 書いた --> A[id = 2 の1行だけ変わる<br>UPDATE 1]
  W -- 書かない --> B[5行すべてが変わる<br>UPDATE 5]

自分の状況の行を見ると、1行の書き方と注意点が分かります。例はこの記事のテーブル items(備品)と new_prices(新しい価格)を使っています。

やりたいこと 書き方 注意点
1列を更新 UPDATE items SET price = 150 WHERE id = 2; 主キーで絞ると対象は0行か1行
複数列を同時に更新 SET price = 2300, stock = 20 SET は1回だけ。列はカンマで区切る
複数行を同じ式で更新 SET price = price + 10 WHERE category = '文具' 条件に合う行すべてが対象
別テーブルの値で更新(全DB共通) SET price = (SELECT ...) WHERE EXISTS (...) EXISTS が無いと相手のいない行が NULL に
別テーブルの値で更新(JOIN) MySQL UPDATE a JOIN b ON ... SET ... PostgreSQL は FROM、Oracle は MERGE
複数行を条件別に更新 SET 列 = CASE WHEN 条件 THEN 値 ELSE 列 END ELSE 列名 が無いと他の行が NULL に
行ごとに別の値を入れる CASE id WHEN 1 THEN 210 ... ELSE price END WHERE id IN (1, 3) と ELSE の両方を書く
全行を更新 UPDATE items SET status = 'active'; WHERE なし=全行。意図したときだけ
数値を加算・減算 SET stock = stock - 1 WHERE id = 1 右辺の列は更新前の値
上位N件だけ更新 MySQL ORDER BY ... LIMIT 2。他はサブクエリ PostgreSQL は UPDATE に LIMIT を書けない
文字列を置換 SET note = REPLACE(note, '旧仕様', '新仕様') REPLACE は5つの DB で同じ書き方
サブクエリの結果で更新 SET price = (SELECT MAX(new_price) ...) 1行1列を返す形にする
試してから確定する BEGIN; → UPDATE → SELECT で確認 → COMMIT; 戻すなら ROLLBACK;。COMMIT 後は戻らない

この記事と関連記事の分担

知りたいこと 読む記事
別テーブルの値で更新する詳細(複数一致・MERGE・UPSERT) SQLで別テーブルの値を使ってUPDATEする方法【DB別】
CASE 式そのものの書き方 SQL CASE式の使い方|用途別コピペ集とDB比較・実行結果つき
登録と更新をまとめる UPSERT SQL UPSERTの書き方|PostgreSQL・MySQL・MERGE構文比較表
BEGIN・COMMIT・ROLLBACK の仕組み SQLトランザクションとは|COMMIT・ROLLBACKの書き方と実行結果
デッドロックの原因と対策 デッドロックの原因と対策|PostgreSQLで再現した実ログで解説

確認環境:PostgreSQL 16.15(Ubuntu 24.04、psql)、MySQL 8.0.46(mysql クライアントを --default-character-set=utf8mb4 -vv で実行)、SQLite 3.45.1(sqlite3)で、この記事の SQL を実行し、出力をそのまま載せています。SQL Server と Oracle の構文・エラー文は Microsoft Learn「UPDATE (Transact-SQL)」と Oracle Database 23ai SQL Language Reference で、ORM の書き方は各公式ドキュメントで確かめました(確認日 2026年10月16日)。

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

(記載例)出典:株式会社RJC「SQL UPDATE文の書き方|複数列・別テーブル・CASE・DB別構文の早見表」

この記事で使うテーブルとデータ(コピペ用)

備品の一覧 items と、新しい価格をまとめた new_prices の2テーブルです。PostgreSQL・MySQL・SQLite のどれでも、このまま貼って実行できます。

DROP TABLE IF EXISTS new_prices;
DROP TABLE IF EXISTS items;
CREATE TABLE items (
  id         INT PRIMARY KEY,
  name       VARCHAR(30) NOT NULL,
  category   VARCHAR(20) NOT NULL,
  price      INT,
  stock      INT NOT NULL,
  status     VARCHAR(10) NOT NULL,
  note       VARCHAR(50),
  updated_at TIMESTAMP
);
INSERT INTO items (id, name, category, price, stock, status, note, updated_at) VALUES
  (1, 'ノート',     '文具', 200,  50, 'active', '旧仕様',    '2026-10-01 09:00:00'),
  (2, 'ボールペン', '文具', 120,   0, 'active', '旧仕様 黒', '2026-10-01 09:00:00'),
  (3, '付箋',       '文具', 300,  15, 'active', NULL,        '2026-10-01 09:00:00'),
  (4, 'マウス',     '機器', 2500,  8, 'active', '旧仕様',    '2026-10-01 09:00:00'),
  (5, 'キーボード', '機器', 6000,  0, 'active', NULL,        '2026-10-01 09:00:00');
CREATE TABLE new_prices (
  item_id   INT PRIMARY KEY,
  new_price INT NOT NULL
);
INSERT INTO new_prices (item_id, new_price) VALUES (1, 220), (4, 2800), (9, 999);

price は NULL を許す列、status は NOT NULL の列です。この違いが、あとで出てくる「ELSE なしの CASE」の結果を分けます。new_prices の item_id = 9 は、items に相手がいない行です。

更新前の items はこの状態です(以降「初期状態」と呼びます。各例は初期状態に戻してから実行しています)。

 id |    name    | category | price | stock | status |   note    |     updated_at
----+------------+----------+-------+-------+--------+-----------+---------------------
  1 | ノート     | 文具     |   200 |    50 | active | 旧仕様    | 2026-10-01 09:00:00
  2 | ボールペン | 文具     |   120 |     0 | active | 旧仕様 黒 | 2026-10-01 09:00:00
  3 | 付箋       | 文具     |   300 |    15 | active |           | 2026-10-01 09:00:00
  4 | マウス     | 機器     |  2500 |     8 | active | 旧仕様    | 2026-10-01 09:00:00
  5 | キーボード | 機器     |  6000 |     0 | active |           | 2026-10-01 09:00:00
(5 rows)

UPDATE文の基本の書き方(SET・WHERE)

UPDATE 文は3つの部品でできています。

UPDATE テーブル名
SET    列名 = 新しい値
WHERE  更新する行の条件;
部品 役割 省略すると
UPDATE テーブル名 どのテーブルを書き換えるか 省略できない
SET 列 = 値 どの列を何にするか。右辺には式・関数・CASE・サブクエリも書ける 省略できない
WHERE 条件 どの行を書き換えるか。SELECT の WHERE と同じ書き方 全行が対象になる

1列・複数列・複数行を更新する

PostgreSQL 16 で初期状態から順に実行した結果です。

UPDATE items SET price = 150 WHERE id = 2;
UPDATE 1

UPDATE items SET price = 2300, stock = 20, status = 'sale' WHERE id = 4;
UPDATE 1

UPDATE items SET price = price + 10 WHERE category = '文具';
UPDATE 3

SELECT id, name, category, price, stock, status FROM items ORDER BY id;
 id |    name    | category | price | stock | status
----+------------+----------+-------+-------+--------
  1 | ノート     | 文具     |   210 |    50 | active
  2 | ボールペン | 文具     |   160 |     0 | active
  3 | 付箋       | 文具     |   310 |    15 | active
  4 | マウス     | 機器     |  2300 |    20 | sale
  5 | キーボード | 機器     |  6000 |     0 | active
(5 rows)
  • 複数列は SET を1回だけ書き、列ごとにカンマで区切ります。 SET price = 100 SET stock = 1 と書くと構文エラーです。
  • 複数行は WHERE の条件で対象を広げます。 price + 10 のように右辺に列自身を書くと、行ごとの更新前の値をもとに計算されます(id = 2 は 150 → 160)。

更新件数の見方(UPDATE 3・Rows matched・changes())

UPDATE を実行したら、まず件数の表示を見ます。表示のしかたは DB で違います。

DB 表示 意味
PostgreSQL(psql) UPDATE 3 更新した行数。同じ値への更新も数える
MySQL(mysql) Query OK, 1 row affected と Rows matched: 2 Changed: 1 matched は条件に合った行数、Changed は値が変わった行数
SQLite(sqlite3) 表示なし。SELECT changes(); で取る 直前の文で変更した行数
SQL Server (3 rows affected)。@@ROWCOUNT で取る 影響した行数
Oracle 3 rows updated.。PL/SQL では SQL%ROWCOUNT 影響した行数

MySQL 8.0 で、すでに 'sale' になっている id = 4 を含む「機器」2行を 'active' にした結果です。

UPDATE items SET status = 'active' WHERE category = '機器'
Query OK, 1 row affected
Rows matched: 2  Changed: 1  Warnings: 0

SELECT ROW_COUNT()
1

MySQL は同じ値への更新を「変更」と数えないため、ROW_COUNT() と rows affected は Changed の値になります。 条件が正しいかは Rows matched で確かめます。PostgreSQL の UPDATE n は、公式ドキュメントのとおり「値が変わらなかった行も含めた更新行数」です。

条件に合う行が1つもないときは、エラーではなく 0 件です。

UPDATE items SET stock = 99 WHERE id = 100;
UPDATE 0

UPDATE 0 はエラーになりません。 想定した件数と同じかを自分の目で確かめることが、UPDATE 文でいちばん大切な習慣です。

複数行を条件別に更新する(CASE式)

「在庫が 0 なら soldout、10 未満なら few、それ以外はそのまま」のように、行の状態によって入れる値を変えたいときは、SET の右辺に CASE 式を書きます。CASE 式そのものの書き方は、関連記事「SQL CASE式の使い方|用途別コピペ集とDB比較・実行結果つき」で扱っています。

条件で振り分ける(検索CASE)

UPDATE items
SET status = CASE
               WHEN stock = 0  THEN 'soldout'
               WHEN stock < 10 THEN 'few'
               ELSE status
             END;
UPDATE 5

SELECT id, name, stock, status FROM items ORDER BY id;
 id |    name    | stock | status
----+------------+-------+---------
  1 | ノート     |    50 | active
  2 | ボールペン |     0 | soldout
  3 | 付箋       |    15 | active
  4 | マウス     |     8 | few
  5 | キーボード |     0 | soldout
(5 rows)

CASE は上から順に判定し、最初に当てはまった WHEN の値を使います。stock = 0 の行は stock < 10 にも当てはまりますが、先に書いた stock = 0 が選ばれます。狭い条件を上に、広い条件を下に書きます。

ELSE status は「どれにも当てはまらない行には今の値を入れ直す」という意味です。WHERE が無いので5行すべてが対象になり、ELSE のおかげで id = 1 と 3 は active のまま保たれています。MySQL では同じ文が Rows matched: 5 Changed: 3 と表示され、値が変わった3行だけが Changed に数えられました。

行ごとに別の値を入れる(単純CASE)

「id = 1 は 210、id = 3 は 280、id = 5 は 5800」のように、行ごとに決まった値を入れるときは CASE 列 WHEN 値 THEN ... の形が読みやすくなります。

UPDATE items
SET price = CASE id
              WHEN 1 THEN 210
              WHEN 3 THEN 280
              WHEN 5 THEN 5800
              ELSE price
            END
WHERE id IN (1, 3, 5);
UPDATE 3

SELECT id, name, price FROM items ORDER BY id;
 id |    name    | price
----+------------+-------
  1 | ノート     |   210
  2 | ボールペン |   120
  3 | 付箋       |   280
  4 | マウス     |  2500
  5 | キーボード |  5800
(5 rows)

UPDATE 文を3回に分けても結果は同じですが、1文にまとめると「3行とも更新されるか、1行も更新されないか」のどちらかになり、途中まで進んだ状態が残りません。ELSE price と WHERE id IN (...) の両方を書くのが型です。 WHERE で対象を3行に絞り、WHERE を書きそびれても ELSE が他の行を守ります。

複数列をそれぞれ CASE で更新する

列ごとに CASE を書けば、1文で複数列を条件別に更新できます。

UPDATE items
SET price  = CASE WHEN category = '文具' THEN price + 10 ELSE price END,
    status = CASE WHEN stock = 0 THEN 'soldout' ELSE status END;
UPDATE 5

SELECT id, name, price, stock, status FROM items ORDER BY id;
 id |    name    | price | stock | status
----+------------+-------+-------+---------
  1 | ノート     |   210 |    50 | active
  2 | ボールペン |   130 |     0 | soldout
  3 | 付箋       |   310 |    15 | active
  4 | マウス     |  2500 |     8 | active
  5 | キーボード |  6000 |     0 | soldout
(5 rows)

ELSE なしの CASE は NULL で上書きする

CASE 式は、どの WHEN にも当てはまらず ELSE も無いときに NULL を返します。初期状態から、ELSE と WHERE を書かずに実行した結果です。

UPDATE items
SET price = CASE id
              WHEN 1 THEN 210
              WHEN 3 THEN 280
            END;
UPDATE 5

SELECT id, name, price FROM items ORDER BY id;
 id |    name    | price
----+------------+-------
  1 | ノート     |   210
  2 | ボールペン |
  3 | 付箋       |   280
  4 | マウス     |
  5 | キーボード |
(5 rows)

id = 2・4・5 の price が NULL になりました。MySQL 8.0(Rows matched: 5 Changed: 5)と SQLite 3.45(changes() が 5)でも同じ結果です。変えたかったのは2行なのに、件数は 5 です。ここで件数に気づけるかどうかが分かれ目になります。

同じことを NOT NULL の status 列で行うと、制約に守られて文の全体が取り消されます。

UPDATE items SET status = CASE WHEN stock = 0 THEN 'soldout' END;
DB エラー文(原文)
PostgreSQL 16.15 ERROR: null value in column "status" of relation "items" violates not-null constraint
MySQL 8.0.46 ERROR 1048 (23000): Column 'status' cannot be null
SQLite 3.45.1 Runtime error: NOT NULL constraint failed: items.status (19)
SQL Server(ドキュメント) Cannot insert the value NULL into column 'status', table '...'; column does not allow nulls. UPDATE fails.(エラー 515)
Oracle(ドキュメント) ORA-01407: cannot update ("...") to NULL
直し方 書き方 向いている場面
ELSE 列名 を足す ... WHEN 3 THEN 280 ELSE price END いつでも有効。まずこれを書く
WHERE で対象を絞る WHERE id IN (1, 3) 更新する行が決まっているとき。触れる行数も減る
両方書く ELSE と WHERE を併記 片方を書きそびれても値が保たれる

行数が多いなら VALUES の一覧と結合する(PostgreSQL)

CASE の WHEN が長くなってきたら、「id と新しい値の一覧」をその場で作って結合する書き方が読みやすくなります。一覧に無い行は結合されないので、ELSE を書かなくても値が保たれます。

UPDATE items AS i
SET price = v.price
FROM (VALUES (1, 210), (3, 280), (5, 5800)) AS v(id, price)
WHERE i.id = v.id
RETURNING i.id, i.name, i.price;
 id |    name    | price
----+------------+-------
  1 | ノート     |   210
  3 | 付箋       |   280
  5 | キーボード |  5800
(3 rows)

UPDATE 3

MySQL では一覧を一時テーブルに入れてから、次の節の UPDATE ... JOIN で結合します。

別テーブルの値で更新する(サブクエリ・JOIN・DB別)

new_prices にある新しい価格を items に反映します。期待する結果は「id = 1 が 220、id = 4 が 2800、ほかはそのまま」です。書き方は DB ごとに違います。

DB 最小構文
全DB共通 UPDATE items SET price = (SELECT ...) WHERE EXISTS (SELECT 1 ...)
MySQL 8.0 UPDATE items i JOIN new_prices n ON n.item_id = i.id SET i.price = n.new_price
PostgreSQL 16 UPDATE items i SET price = n.new_price FROM new_prices n WHERE n.item_id = i.id
SQLite 3.33 以降 UPDATE items SET price = n.new_price FROM new_prices n WHERE n.item_id = items.id
SQL Server UPDATE i SET i.price = n.new_price FROM items i JOIN new_prices n ON n.item_id = i.id
Oracle MERGE INTO items i USING new_prices n ON (i.id = n.item_id) WHEN MATCHED THEN UPDATE SET i.price = n.new_price

全DB共通:相関サブクエリ+WHERE EXISTS

UPDATE items
SET price = (SELECT n.new_price FROM new_prices n WHERE n.item_id = items.id)
WHERE EXISTS (SELECT 1 FROM new_prices n WHERE n.item_id = items.id);
UPDATE 2

SELECT id, name, price FROM items ORDER BY id;
 id |    name    | price
----+------------+-------
  1 | ノート     |   220
  2 | ボールペン |   120
  3 | 付箋       |   300
  4 | マウス     |  2800
  5 | キーボード |  6000
(5 rows)

WHERE EXISTS は必ずセットで書きます。 外すと、相手のいない行のサブクエリが NULL を返し、その行の price が NULL で上書きされます。

UPDATE items
SET price = (SELECT n.new_price FROM new_prices n WHERE n.item_id = items.id);
UPDATE 5

 id |    name    | price
----+------------+-------
  1 | ノート     |   220
  2 | ボールペン |
  3 | 付箋       |   300
  4 | マウス     |  2800
  5 | キーボード |
(5 rows)

Oracle のドキュメントにも「サブクエリが行を返さなければ、その列には NULL が代入される」と明記されています。ELSE なしの CASE と同じ形の事故です。

PostgreSQL・SQLite:UPDATE ... FROM

UPDATE items AS i
SET price = n.new_price
FROM new_prices AS n
WHERE n.item_id = i.id;
UPDATE 2

結合できた行だけが更新されるので、WHERE EXISTS を別に書かずに済みます。SQLite 3.45.1 でも同じ形(別名なしで WHERE n.item_id = items.id)で changes() が 2 でした。

PostgreSQL では SET の左辺にテーブルの別名を付けられません。 付けるとこのエラーになります。

UPDATE items AS i SET i.price = n.new_price FROM new_prices AS n WHERE n.item_id = i.id;
ERROR:  column "i" of relation "items" does not exist

FROM に書くのは参照する側のテーブルだけです。公式ドキュメントは「自己結合の意図が無い限り、更新対象のテーブルを from_item に繰り返し書かない」と注意しています。

MySQL:UPDATE ... JOIN

MySQL は UPDATE の直後に JOIN を書き、そのあとに SET を置きます。

UPDATE items i
JOIN new_prices n ON n.item_id = i.id
SET i.price = n.new_price;
Query OK, 2 rows affected
Rows matched: 2  Changed: 2  Warnings: 0

PostgreSQL 形式の FROM をそのまま貼ると、構文エラーになります。

UPDATE items i SET price = n.new_price FROM new_prices n WHERE n.item_id = i.id;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'FROM new_prices n WHERE n.item_id = i.id' at line 1

相手がいない行も含めて全行に「処理済み」の印を付けたいなら、LEFT JOIN と COALESCE で元の値を保ちます。

UPDATE items i
LEFT JOIN new_prices n ON n.item_id = i.id
SET i.price = COALESCE(n.new_price, i.price), i.note = '改定処理済み';
Query OK, 5 rows affected
Rows matched: 5  Changed: 5  Warnings: 0

MySQL の複数テーブル UPDATE は、SET に両方のテーブルの列を書けば 2つのテーブルを1文で更新できます(new_prices に applied 列を足して試すと Rows matched: 4 と、両テーブルの行数の合計が表示されました)。ただし、複数テーブル形式では ORDER BY と LIMIT が使えません。

SQL Server:UPDATE 別名 ... FROM ... JOIN

Microsoft Learn の例にならった書き方です。UPDATE の直後に別名を書き、FROM で結合します。

UPDATE i
SET i.price = n.new_price
FROM items AS i
INNER JOIN new_prices AS n ON n.item_id = i.id;

ドキュメントは「FROM 句で更新対象の各列に値が1つに決まらない書き方をすると、結果は未定義」と注意しています。

Oracle:相関サブクエリか MERGE、23ai からは UPDATE ... FROM

Oracle では、全DB共通の相関サブクエリか MERGE が基本の書き方です。

MERGE INTO items i
USING new_prices n
ON (i.id = n.item_id)
WHEN MATCHED THEN
  UPDATE SET i.price = n.new_price;

Oracle Database 23ai の SQL Language Reference には、UPDATE の from_using_clause(直接結合)も載っています。

UPDATE items i
SET    i.price = n.new_price
FROM   new_prices n
WHERE  n.item_id = i.id;

ドキュメントでは「各行を更新できるのは最大1回。結合条件で同じ行が2回以上更新されると ORA-30926」「SET の左辺は更新対象テーブルの列だけ」「RIGHT・FULL OUTER JOIN は使えない」と説明されています。

相手が複数行あるときの挙動(JOIN は1つを使い、サブクエリはエラー)

new_prices の主キーを外して item_id = 1 の行を2つ(220 と 999)にし、同じ UPDATE を実行しました。

SELECT * FROM new_prices ORDER BY item_id, new_price;
 item_id | new_price
---------+-----------
       1 |       220
       1 |       999
       4 |      2800
       9 |       999
(4 rows)

UPDATE items AS i SET price = n.new_price FROM new_prices AS n WHERE n.item_id = i.id;
UPDATE 2

SELECT id, name, price FROM items WHERE id = 1;
 id |  name  | price
----+--------+-------
  1 | ノート |   220
(1 row)

エラーにならず、どちらか一方の値(ここでは 220)で更新されました。 PostgreSQL のドキュメントは「結合が1つの対象行に対して複数の行を作るときは、そのうち1つだけが使われ、どれが使われるかは予測できない」と説明しています。SQLite と SQL Server のドキュメントも同じ趣旨です。

一方、相関サブクエリと MERGE はエラーで止まります。

書き方 PostgreSQL 16.15 の結果
相関サブクエリ ERROR: more than one row returned by a subquery used as an expression
MERGE ERROR: MERGE command cannot affect row a second time(HINT: 1つの対象行に合う元の行を1つにする)
MySQL のサブクエリ ERROR 1242 (21000): Subquery returns more than 1 row
SQL Server(ドキュメント) Subquery returned more than 1 value. ...(エラー 512)
Oracle(ドキュメント) ORA-01427: single-row subquery returns more than one row

更新の前に、参照するテーブルの重複を確かめておくと安心です。

SELECT item_id, COUNT(*) FROM new_prices GROUP BY item_id HAVING COUNT(*) > 1;
 item_id | count
---------+-------
       1 |     2
(1 row)

どの書き方を選ぶかの判断です。

flowchart TD
  A["別テーブルの値で更新したい"] --> B{"相手の行は必ず1つ?"}
  B -- "複数ありうる" --> C["先に重複を除く/集計して1行にする<br>(MERGE・サブクエリならエラーで止まる)"]
  B -- "必ず1つ" --> D{"相手がいない行は?"}
  C --> D
  D -- "そのまま残す" --> E["JOIN/FROM、または<br>サブクエリ+WHERE EXISTS"]
  D -- "別の値にする" --> F["LEFT JOIN+COALESCE<br>(MySQL)、CASE で分岐"]

複数一致の回避や MERGE・UPSERT との使い分けは、関連記事「SQLで別テーブルの値を使ってUPDATEする方法【DB別】」で扱っています。

上位N件だけ更新する(LIMIT・ORDER BY・サブクエリ)

「文具のうち価格が高い2件だけ sale にする」例です。UPDATE に LIMIT を書けるかどうかは DB で分かれます。

DB 書き方 結果
MySQL 8.0 UPDATE items SET status = 'sale' WHERE category = '文具' ORDER BY price DESC LIMIT 2; Rows matched: 2 Changed: 2。id = 1・3 が sale
PostgreSQL 16 UPDATE items SET stock = 0 LIMIT 1; ERROR: syntax error at or near "LIMIT"。ORDER BY も同じエラー
SQLite 3.45(Ubuntu 24.04) UPDATE items SET stock = 0 ORDER BY id LIMIT 1; 1行更新。ビルド時の SQLITE_ENABLE_UPDATE_DELETE_LIMIT で有効
SQL Server(ドキュメント) UPDATE TOP (2) items SET ... 選ばれる行の順序は決まらない。順序を付けるなら ORDER BY 付きのサブクエリ
Oracle(ドキュメント) サブクエリで主キーを選ぶ(FETCH FIRST 2 ROWS ONLY など) UPDATE に LIMIT 相当の句は無い

5つの DB で使えるのは、サブクエリで主キーを選ぶ書き方です(件数を絞る句だけが LIMIT・TOP・FETCH FIRST と DB で異なります)。PostgreSQL 16 で実行した結果です。

UPDATE items
SET status = 'sale'
WHERE id IN (SELECT id FROM items WHERE category = '文具' ORDER BY price DESC LIMIT 2)
RETURNING id, name, price, status;
 id |  name  | price | status
----+--------+-------+--------
  1 | ノート |   200 | sale
  3 | 付箋   |   300 | sale
(2 rows)

UPDATE 2

MySQL では、LIMIT は「WHERE に合った行を n 件見つけた時点で止まる」制限で、値が変わったかどうかは関係ありません。ORDER BY を付けないと、どの行が選ばれるかは決まりません。また JOIN を使う複数テーブル形式に LIMIT を付けると ERROR 1221 (HY000): Incorrect usage of UPDATE and LIMIT になります。

値の計算で更新する(加算・REPLACE・日付・NULL)

数値の加算・減算と小数の丸め

UPDATE items SET stock = stock - 1 WHERE id = 1 RETURNING id, stock;
 id | stock
----+-------
  1 |    49
(1 row)

INT の列に小数の計算結果を入れると、丸め方が DB で違います。price = 120 の行に price * 1.08(129.6)を入れた結果です。

DB 入った値 補足
PostgreSQL 16.15 130 四捨五入して整数に
MySQL 8.0.46 130 四捨五入して整数に
SQLite 3.45.1 129.6(typeof は real) INT 列でも小数のまま保存される。STRICT テーブルでは cannot store REAL value in INT column のエラー

切り捨て・切り上げを決めたいときは FLOOR()・CEIL()・ROUND() で明示します。

文字列の置換・連結(REPLACE・CONCAT)

REPLACE は5つの DB で同じ書き方です。

UPDATE items SET note = REPLACE(note, '旧仕様', '新仕様') WHERE note LIKE '%旧仕様%' RETURNING id, note;
 id |   note
----+-----------
  1 | 新仕様
  2 | 新仕様 黒
  4 | 新仕様
(3 rows)

UPDATE 3

連結は MySQL が CONCAT(name, '(在庫なし)')、PostgreSQL・SQLite・Oracle が name || '(在庫なし)'、SQL Server が name + N'(在庫なし)' です。結果が列の長さを超えると、MySQL は ERROR 1406 (22001): Data too long for column 'status' at row 1、PostgreSQL は ERROR: value too long for type character varying(10) で止まります(VARCHAR(10) の status に12文字を入れた結果)。

日付の加算と現在時刻

DB 7日後にする 現在時刻にする
PostgreSQL 16 updated_at + INTERVAL '7 days' CURRENT_TIMESTAMP(now())
MySQL 8.0 updated_at + INTERVAL 7 DAY(DATE_ADD(updated_at, INTERVAL 7 DAY)) NOW()
SQLite 3.45 datetime(updated_at, '+7 days') CURRENT_TIMESTAMP(UTC)
SQL Server(ドキュメント) DATEADD(day, 7, updated_at) SYSDATETIME()
Oracle(ドキュメント) updated_at + 7(数値は日数)、updated_at + INTERVAL '7' DAY SYSTIMESTAMP
UPDATE items SET updated_at = updated_at + INTERVAL '7 days' WHERE id = 1 RETURNING id, updated_at;
 id |     updated_at
----+---------------------
  1 | 2026-10-08 09:00:00
(1 row)

MySQL(+ INTERVAL 7 DAY → 2026-10-08 09:00:00)、SQLite(datetime(updated_at, '+7 days') → 2026-10-08 09:00:00)でも同じ値になりました。

NULL の扱い(= NULL は0件になる)

書き方 意味 結果
SET note = NULL note を NULL にする(代入) UPDATE 1
WHERE note = NULL どの行にも当てはまらない UPDATE 0
WHERE note IS NULL note が NULL の行を選ぶ UPDATE 3
SET note = COALESCE(note, '(未設定)') NULL のときだけ値を入れる UPDATE 5(値がある行はそのまま)
SET note = DEFAULT 列の既定値に戻す(PostgreSQL・MySQL・SQL Server) 既定値が無い列は NULL

SET 句の「=」は代入、WHERE 句の「=」は比較です。 NULL の行を探すときは IS NULL を使います。NULL の落とし穴は関連記事「SQLのNULLの扱い|COALESCE・NULLIFと落とし穴12選」で扱っています。

SET の評価順は MySQL だけ違う

同じ SET 句の中で、更新中の列を別の列の右辺に使ったときの結果です。

-- 初期状態の id = 1 は price = 200, stock = 50
UPDATE items SET price = price + 100, stock = price WHERE id = 1;
DB 更新後の price 更新後の stock 右辺の price が指す値
PostgreSQL 16.15 300 200 更新前の値
SQLite 3.45.1 300 200 更新前の値
MySQL 8.0.46 300 300 左から順に代入したあとの値

MySQL のドキュメントは「単一テーブルの UPDATE の代入は左から右へ評価され、この動作は標準 SQL と異なる」と明記しています。列の値を入れ替える SET price = stock, stock = price は、PostgreSQL では入れ替わり(15 / 300)、MySQL では両方が同じ値(15 / 15)になりました。

やってはいけない操作:WHEREなしUPDATEと、安全な手順

UPDATE でいちばん多い事故は、WHERE の書きそびれや条件の勘違いで、想定より多くの行を書き換えることです。COMMIT 前なら ROLLBACK で戻せるので、次の5ステップを型にします。

flowchart TD
  A["1. UPDATE と同じ WHERE で SELECT し、行と件数を確認"] --> B["2. BEGIN(MySQL は START TRANSACTION)"]
  B --> C["3. UPDATE を実行"]
  C --> D{"4. 件数は手順1と同じ?<br>SELECT で中身も確認"}
  D -- "同じ" --> E["5. COMMIT で確定"]
  D -- "違う" --> F["5. ROLLBACK で取り消し"]

手順を通して実行した結果(PostgreSQL 16)

「在庫 0 の備品を soldout にする」作業です。

-- 1. 同じ WHERE で対象を確認する
SELECT id, name, stock, status FROM items WHERE stock = 0;
 id |    name    | stock | status
----+------------+-------+--------
  2 | ボールペン |     0 | active
  5 | キーボード |     0 | active
(2 rows)

-- 2. トランザクションを開始する
BEGIN;
BEGIN

-- 3. 手順1と同じ WHERE で UPDATE する
UPDATE items SET status = 'soldout' WHERE stock = 0;
UPDATE 2

-- 4. 件数(UPDATE 2)が手順1と同じことを確かめ、中身も見る
SELECT id, name, stock, status FROM items WHERE stock = 0;
 id |    name    | stock | status
----+------------+-------+---------
  2 | ボールペン |     0 | soldout
  5 | キーボード |     0 | soldout
(2 rows)

-- 5. 確定する
COMMIT;
COMMIT

WHERE を書きそびれても、COMMIT 前なら ROLLBACK で戻せる

BEGIN;
BEGIN
UPDATE items SET status = 'soldout';
UPDATE 5
SELECT status, COUNT(*) FROM items GROUP BY status;
 status  | count
---------+-------
 soldout |     5
(1 row)

ROLLBACK;
ROLLBACK
SELECT status, COUNT(*) FROM items GROUP BY status;
 status | count
--------+-------
 active |     5
(1 row)

手順1で確かめた件数は 2 でしたが、表示は UPDATE 5 です。違いに気づいた時点で ROLLBACK すれば、データは更新前のままです。MySQL でも START TRANSACTION → UPDATE(Rows matched: 5)→ ROLLBACK のあと、5行とも active に戻りました。

自動コミットを確かめる(BEGIN なしの UPDATE は戻らない)

多くのクライアントは、初期設定では1文ごとに自動で確定します。BEGIN を書かずに実行した UPDATE は、その時点で確定しています。

-- psql(自動コミット on)
UPDATE items SET status = 'soldout' WHERE id = 1;
UPDATE 1
ROLLBACK;
WARNING:  there is no transaction in progress
ROLLBACK
SELECT id, status FROM items WHERE id = 1;
 id | status
----+---------
  1 | soldout
(1 row)
クライアント 自動コミットの確認 切り替え
psql \echo :AUTOCOMMIT → on \set AUTOCOMMIT off
MySQL SELECT @@autocommit; → 1 SET autocommit = 0;(セッション単位)
SQLite(sqlite3) 常に自動コミット BEGIN; で明示的に開始

MySQL で SET autocommit = 0; にすると、BEGIN を書かなくても UPDATE のあとの ROLLBACK で戻ることを確かめました。ただし COMMIT を忘れると、更新した行のロックが残り、他の処理が待たされます。仕組みは関連記事「SQLトランザクションとは|COMMIT・ROLLBACKの書き方と実行結果」で扱っています。

MySQL のセーフアップデートモードで書きそびれを止める

MySQL には、キー列を使わない UPDATE・DELETE をエラーにする設定があります。手作業で SQL を打つセッションで有効にしておくと、DB の側で止められます。

SET sql_safe_updates = 1;

UPDATE items SET status = 'soldout';
ERROR 1175 (HY000): You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column.

UPDATE items SET status = 'soldout' WHERE stock = 0;
ERROR 1175 (HY000): You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column.

UPDATE items SET status = 'soldout' WHERE id IN (2, 5);
Query OK, 2 rows affected
Rows matched: 2  Changed: 2  Warnings: 0

UPDATE items SET status = 'active' WHERE stock = 0 LIMIT 10;
Query OK, 2 rows affected
Rows matched: 2  Changed: 2  Warnings: 0

WHERE があっても、インデックスの無い列(stock)だけで絞ると止まります。主キーなどキー列で絞るか、LIMIT を付けると通ります。mysql クライアントを --safe-updates オプションで起動しても同じ設定になります。MySQL Workbench で見かける Error Code: 1175 も同じエラーで、Workbench は初期設定で Safe Updates が有効になっています。

やってはいけない操作の一覧

操作 起きること 代わりに
WHERE なしで UPDATE 全行が書き換わる。エラーにならない 同じ WHERE で先に SELECT。BEGIN の中で実行
ELSE なしの CASE で更新 対象外の行が NULL になる ELSE 列名 と WHERE を書く
EXISTS なしの相関サブクエリ 相手のいない行が NULL になる WHERE EXISTS か JOIN/FROM
WHERE 列 = NULL 0件更新になり、気づきにくい IS NULL
相手が複数ある JOIN で更新 どの値になるか決まらない 先に重複を確認し1行に絞る
主キーを別の値に更新 重複すると duplicate key value violates unique constraint、外部キーの参照も壊れる 新しい行を作って移す
確認の SELECT を省いて本番で実行 件数のずれに気づけない 件数を控えてから UPDATE
トランザクションを開いたまま離席 行ロックが残り、他の UPDATE が待つ 確認が済んだらすぐ COMMIT か ROLLBACK

COMMIT 済みの更新を戻すには、バックアップからの復元か、更新前の値に戻す UPDATE を書くことになります。更新前の値を残したいときは、対象の行を先に別テーブルへコピーしておく方法が手軽です(CREATE TABLE items_bak AS SELECT * FROM items WHERE ...)。書き方は関連記事「SQLテーブルコピー|CREATE TABLE AS SELECTと構造のみ」で扱っています。

UPDATE した行をその場で受け取る(RETURNING・OUTPUT)

PostgreSQL と SQLite(3.35.0 以降)では、UPDATE の末尾に RETURNING を付けると、更新後の行をそのまま結果として受け取れます。安全手順の4番目(件数と中身の確認)が1文で済みます。

UPDATE items
SET price = price - 20
WHERE category = '文具'
RETURNING id, name, price;
 id |    name    | price
----+------------+-------
  1 | ノート     |   180
  2 | ボールペン |   100
  3 | 付箋       |   280
(3 rows)

UPDATE 3

SQLite のドキュメントでは、RETURNING で返る行の順序は決まっていないと説明されています。MySQL 8.0 は UPDATE の RETURNING に対応しておらず、構文エラーになります。

UPDATE items SET price = price - 20 WHERE category = '文具' RETURNING id, name, price;
ERROR 1064 (42000): You have an error in your SQL syntax; ... near 'RETURNING id, name, price' at line 1

MySQL では、UPDATE のあとに同じ WHERE で SELECT するか、SELECT ROW_COUNT(); で件数を取ります。

SQL Server は OUTPUT 句で、更新前(deleted)と更新後(inserted)の両方を受け取れます。Oracle は RETURNING ... INTO で PL/SQL の変数に受け取ります(いずれもドキュメントの構文)。

-- SQL Server
UPDATE items
SET price = price - 20
OUTPUT deleted.id, deleted.price AS before_price, inserted.price AS after_price
WHERE category = N'文具';

-- Oracle(PL/SQL 内)
UPDATE items SET price = price - 20 WHERE id = 1
RETURNING price INTO v_price;

RETURNING の詳しい使い方は、関連記事「PostgreSQL RETURNINGの使い方|更新結果とIDを取得」で扱っています。

DB別の構文の違い(MySQL 8・PostgreSQL 16・SQL Server・Oracle・SQLite)

MySQL・PostgreSQL・SQLite の欄はこの記事の実行結果、SQL Server と Oracle の欄は公式ドキュメントにもとづきます。

DB 別テーブルの値で更新 上位N件(LIMIT・ORDER BY) 更新した行を受け取る 件数の取り方
MySQL 8.0 UPDATE a JOIN b ON ... SET ... ORDER BY ... LIMIT n(複数テーブル形式では不可) 非対応(構文エラー) ROW_COUNT()、Rows matched
PostgreSQL 16 UPDATE a SET ... FROM b WHERE ... 不可(構文エラー)。サブクエリで主キーを選ぶ RETURNING コマンドタグ UPDATE n
SQLite 3.45 UPDATE a SET ... FROM b WHERE ...(3.33.0 以降) ビルド時の設定しだい RETURNING(3.35.0 以降) changes()
SQL Server UPDATE a SET ... FROM a JOIN b ON ... UPDATE TOP (n)(順序は決まらない) OUTPUT inserted.*/deleted.* @@ROWCOUNT
Oracle 相関サブクエリ、MERGE、23ai の UPDATE ... FROM 不可。サブクエリで主キーを選ぶ RETURNING ... INTO(PL/SQL) SQL%ROWCOUNT(PL/SQL)
DB MERGE SET (a, b) = (1, 2) の形 トランザクション開始 型が合わない値(INT 列に 'abc')
MySQL 8.0 無し(INSERT ... ON DUPLICATE KEY UPDATE) 不可(構文エラー) START TRANSACTION / BEGIN ERROR 1366(strict モード)
PostgreSQL 16 15 以降で使える 使える(サブクエリも可) BEGIN ERROR: invalid input syntax for type integer: "abc"
SQLite 3.45 無し(INSERT ... ON CONFLICT DO UPDATE) 使える BEGIN エラーにならず文字列のまま入る(STRICT テーブルはエラー)
SQL Server 使える 不可 BEGIN TRANSACTION 変換エラー
Oracle 使える サブクエリとの組み合わせで使える 最初の DML で自動的に開始 ORA-01722: invalid number など

MERGE・UPSERT の構文は関連記事「SQL UPSERTの書き方|PostgreSQL・MySQL・MERGE構文比較表」で扱っています。

更新されない・エラーになる原因と直し方

エラーにならないのに更新されない

症状 原因 確かめ方・直し方
UPDATE 0/Rows matched: 0 条件に合う行が無い。= NULL で比較している。別の DB・スキーマに接続している 同じ WHERE で SELECT。NULL は IS NULL
MySQL で Rows matched: 1 Changed: 0 すでに同じ値 正常。matched で条件を確かめる
件数は出るのに他のセッションから見えない 自動コミットが off で COMMIT していない COMMIT; を実行。@@autocommit を確認
件数が WHERE の行数より少ない BEFORE UPDATE トリガーが更新を止めた(PostgreSQL は UPDATE 0 になる) トリガーの定義を確認
MySQL で ON UPDATE CURRENT_TIMESTAMP の列が変わらない 他の列の値が変わっていない 仕様。他の列が変わったときだけ更新される

エラーになる(原文・原因・直し方)

エラー文(原文) 原因 直し方
MySQL ERROR 1175 (HY000): You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column. セーフアップデートモードで、キー列を使わない WHERE 主キーなどキー列で絞る。SET sql_safe_updates = 0;
MySQL ERROR 1242 (21000): Subquery returns more than 1 row/PostgreSQL ERROR: more than one row returned by a subquery used as an expression SET の右辺のサブクエリが複数行を返した 相関条件で1行に絞る。MAX() などで集計
MySQL ERROR 1054 (42S22): Unknown column 'prise' in 'field list'/PostgreSQL ERROR: column "prise" of relation "items" does not exist 列名の誤り。文字列を "sale" のように二重引用符で書いた(PostgreSQL は列名と解釈) 列名を確認。文字列は単引用符 'sale'
MySQL ERROR 1146 (42S02): Table 'r24db.item' doesn't exist/PostgreSQL ERROR: relation "item" does not exist テーブル名の誤り、接続先 DB・スキーマの違い SHOW TABLES/\dt で確認
MySQL ERROR 1064 (42000): You have an error in your SQL syntax ... near 'FROM new_prices n ...' PostgreSQL 形式の FROM、RETURNING、SET a = 1 SET b = 2 など MySQL に無い構文 UPDATE a JOIN b ON ... SET ...。SET はカンマ区切り
MySQL ERROR 1093 (HY000): You can't specify target table 'items' for update in FROM clause 更新対象のテーブルを同じ文のサブクエリで直接参照 サブクエリを派生テーブルで包む(下記)
MySQL ERROR 1288 (HY000): The target table v_sum of the UPDATE is not updatable/PostgreSQL ERROR: cannot update view "v_items" 集計や JOIN を含むビューを更新した 元のテーブルを更新。PostgreSQL は INSTEAD OF トリガーで対応可
MySQL ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction/PostgreSQL ERROR: deadlock detected 2つのトランザクションが互いの行ロックを待った トランザクションをやり直す。更新する行の順序をそろえる
MySQL ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction/PostgreSQL ERROR: canceling statement due to lock timeout 他のトランザクションが同じ行をロックしたまま 相手の COMMIT を待つか、ロック元を特定して対処
MySQL ERROR 1366 (HY000): Incorrect integer value: 'abc' for column 'price' at row 1/PostgreSQL ERROR: invalid input syntax for type integer: "abc" 列の型に合わない値 値と型を合わせる。'300' のような数字の文字列は通る
MySQL ERROR 1292 (22007): Incorrect datetime value: '2026/13/01' .../PostgreSQL ERROR: date/time field value out of range: "2026/13/01" 日付の書式・値の誤り '2026-10-01 09:00:00' の形で書く
MySQL ERROR 1264 (22003): Out of range value for column 'price' at row 1/PostgreSQL ERROR: integer out of range INT の範囲(約21億)を超えた BIGINT に変更するか値を見直す
MySQL ERROR 1406 (22001): Data too long for column 'status' at row 1/PostgreSQL ERROR: value too long for type character varying(10) 列の長さを超えた 列を広げるか値を短くする
MySQL ERROR 1048 (23000): Column 'status' cannot be null/PostgreSQL ERROR: null value in column "status" ... violates not-null constraint NOT NULL 列に NULL(ELSE なし CASE、相手なしサブクエリ) ELSE 列名、WHERE EXISTS
MySQL ERROR 1062 (23000): Duplicate entry '2' for key 'items.PRIMARY'/PostgreSQL ERROR: duplicate key value violates unique constraint "items_pkey" 主キー・ユニーク列を既存の値に更新した 値を見直す
MySQL ERROR 1142 (42000): UPDATE command denied to user .../PostgreSQL ERROR: permission denied for table items UPDATE 権限が無い GRANT UPDATE ON items TO ...
MySQL ERROR 1221 (HY000): Incorrect usage of UPDATE and LIMIT JOIN を使う複数テーブル形式に LIMIT サブクエリで主キーを選ぶ

MySQL のエラー 1093 は、サブクエリをもう一段包んで派生テーブルにすると通ります。

UPDATE items SET price = (SELECT MAX(price) FROM items) WHERE id = 1;
ERROR 1093 (HY000): You can't specify target table 'items' for update in FROM clause

UPDATE items SET price = (SELECT m FROM (SELECT MAX(price) AS m FROM items) AS t) WHERE id = 1;
Query OK, 1 row affected
Rows matched: 1  Changed: 1  Warnings: 0

詳しくは関連記事「MySQL ERROR 1093の直し方|同じテーブルの更新」で扱っています。ビューの更新は、PostgreSQL では単一テーブルの単純なビュー(SELECT id, name, price FROM items)なら UPDATE 1 で通り、JOIN を含むビューは DETAIL: Views that do not select from a single table or view are not automatically updatable. で止まりました。MySQL は JOIN ビューでも片方のテーブルの列だけなら更新でき、集計ビューは 1288 になりました。

デッドロックとロック待ちを再現した結果

2つのセッションが id = 1 と id = 2 を逆の順序で更新すると、デッドロックになります。PostgreSQL 16 では片方の文がエラーになり(この実行ではセッションA)、そのトランザクションは ROLLBACK されました。

-- セッションA(id=1 → id=2 の順)
UPDATE items SET stock = stock - 1 WHERE id = 2;
ERROR:  deadlock detected
DETAIL:  Process 7503 waits for ShareLock on transaction 171404; blocked by process 7556.
Process 7556 waits for ShareLock on transaction 171403; blocked by process 7503.
HINT:  See server log for query details.
CONTEXT:  while updating tuple (0,2) in relation "items"
COMMIT;
ROLLBACK

MySQL 8.0 でも同じ手順で ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction になり、エラーになった側の最初の UPDATE も取り消されていました(id = 1 の stock が 49 で、片方の減算だけが残った)。

ロック待ちは、片方が COMMIT しないまま同じ行を更新しようとすると起きます。PostgreSQL は既定では待ち続けるので、SET lock_timeout = '2s'; を付けて実行しました。

UPDATE items SET price = 180 WHERE id = 1;
ERROR:  canceling statement due to lock timeout
CONTEXT:  while updating tuple (0,1) in relation "items"

待っている間に別のセッションから pg_stat_activity を見ると、wait_event_type が Lock で、pg_blocking_pids(pid) にロック元のプロセスが出ます。MySQL は innodb_lock_wait_timeout(既定 50 秒)を過ぎると ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction になります。デッドロックの原因と対策は関連記事「デッドロックの原因と対策|PostgreSQLで再現した実ログで解説」で扱っています。ロック元の特定と解除は関連記事「PostgreSQLロック確認SQL|ブロッキング特定から解除までコピペ集」で、MySQL のエラー 1213・1205 は関連記事「MySQL Deadlock foundの原因と対処|1213・1205」で扱っています。同じ行を複数の処理が更新する設計の選び方は、関連記事「楽観ロックと悲観ロックの違い|PostgreSQL実測でわかる使い分け」で扱っています。

大量の行を更新する(分割・インデックス・トリガー・updated_at)

一括 UPDATE と分割 UPDATE の実測(100万行)

100万行の orders テーブル(id・status・amount・created_at)で、status を 'pending' から 'done' にする時間を PostgreSQL 16 で測りました。

方法 1回あたり 合計 行ロックを持つ時間
1文で全行 UPDATE orders SET status = 'done' WHERE status = 'pending'; 4.28 秒 4.28 秒 4.28 秒(100万行すべて)
主キーの範囲で10万行ずつ WHERE id > 0 AND id <= 100000 AND status = 'pending' ×10 0.55〜1.04 秒 6.83 秒 各 1 秒前後(10万行ずつ)
WHERE id IN (SELECT id ... WHERE status = 'pending' ORDER BY id LIMIT 100000) ×10 1.08〜1.71 秒 13.68 秒 各 1〜2 秒
-- 主キーの範囲で分ける(各文を COMMIT してから次へ)
UPDATE orders SET status = 'done' WHERE id > 0      AND id <= 100000 AND status = 'pending';
UPDATE orders SET status = 'done' WHERE id > 100000 AND id <= 200000 AND status = 'pending';
...
  • 合計時間は1文のほうが短く、分割は「1回の文が持つロックの時間と範囲を小さくする」ための方法です。 他の処理が同じ表を更新する時間帯には、分割のほうが待たせる時間が短くなります。
  • LIMIT 付きのサブクエリで分ける方法は、status にインデックスが無いと「残りの pending を探す」走査が毎回入り、回を重ねるごとに遅くなりました(1.08 → 1.71 秒)。主キーの範囲で分けるほうが安定します。
  • PostgreSQL では、更新した行の数だけ不要になった行(dead tuple)が残ります。100万行の UPDATE 1回でテーブルは 71 MB から 142 MB になり、VACUUM で回収しました。大量更新のあとは VACUUM・ANALYZE を実行します。仕組みは関連記事「PostgreSQLのVACUUMとautovacuum|監視SQLと設定値の目安」で扱っています。

WHERE の列にインデックスがあるか(EXPLAIN)

EXPLAIN (ANALYZE, BUFFERS, COSTS OFF, TIMING OFF)
UPDATE orders SET amount = amount + 1 WHERE created_at = TIMESTAMP '2026-01-01 00:10:00';

-- インデックスなし
 Update on orders (actual rows=0 loops=1)
   ->  Seq Scan on orders (actual rows=1 loops=1)
         Filter: (created_at = '2026-01-01 00:10:00'::timestamp without time zone)
         Rows Removed by Filter: 999999
 Execution Time: 63.095 ms

-- CREATE INDEX idx_orders_created_at ON orders (created_at); のあと
 Update on orders (actual rows=0 loops=1)
   ->  Index Scan using idx_orders_created_at on orders (actual rows=1 loops=1)
         Index Cond: (created_at = '2026-01-01 00:10:00'::timestamp without time zone)
 Execution Time: 0.081 ms

1行を更新するだけでも、WHERE の列にインデックスが無いと100万行を読みます(63 ms → 0.08 ms)。逆に、更新する列にインデックスがあると、行の更新と一緒にインデックスも書き換わるため、更新そのものは遅くなります。インデックスが使われない書き方は関連記事「SQLのインデックスが効かない10のパターンと確認方法」で扱っています。

トリガーで更新が止まる・件数が減る

PostgreSQL の BEFORE UPDATE トリガーが NULL を返すと、その行の更新は行われず、件数にも数えられません。price を負の値にする更新を止めるトリガーを付けて実行した結果です。

UPDATE items SET price = -1 WHERE category = '文具';
UPDATE 0

WHERE に合う行は3行ありますが、表示は UPDATE 0 です。公式ドキュメントにも「BEFORE UPDATE トリガーが更新を抑止した場合、件数は条件に合った行数より少なくなることがある」とあります。UPDATE の件数が想定より少ないときは、トリガーの有無も確かめます。書き方は関連記事「PostgreSQLトリガー関数の書き方|PL/pgSQLテンプレ集と早見表」で扱っています。

updated_at を自動で更新する

DB 書き方 動き
MySQL 8.0 updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(DATETIME も可) 他の列の値が変わったときだけ更新される
PostgreSQL 16 BEFORE UPDATE トリガーで NEW.updated_at := CURRENT_TIMESTAMP 毎回更新される。WHEN (OLD.* IS DISTINCT FROM NEW.*) で変更時だけに
SQLite トリガーで UPDATE items SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id 毎回更新される

MySQL で ON UPDATE CURRENT_TIMESTAMP を付けたあと、同じ値(price = 230)で2回 UPDATE した結果です。

UPDATE items SET price = 230 WHERE id = 1;
Rows matched: 1  Changed: 1  Warnings: 0
-- updated_at: 2026-10-16 17:47:23

UPDATE items SET price = 230 WHERE id = 1;   -- 同じ値
Rows matched: 1  Changed: 0  Warnings: 0
-- updated_at: 2026-10-16 17:47:23(変わらない)

ドキュメントのとおり「他の列がすべて現在の値のままなら、自動更新の列も変わらない」動きでした。PostgreSQL のトリガーは、同じ値の UPDATE でも updated_at が進みます(08:45:23.451284 → 08:45:23.452625)。WHEN (OLD.* IS DISTINCT FROM NEW.*) を付けたトリガーでは、同じ値のときは進まず、値が変わったときだけ進みました。

CREATE OR REPLACE FUNCTION set_updated_at() RETURNS trigger AS $$
BEGIN
  NEW.updated_at := CURRENT_TIMESTAMP;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_items_updated_at
BEFORE UPDATE ON items
FOR EACH ROW WHEN (OLD.* IS DISTINCT FROM NEW.*)
EXECUTE FUNCTION set_updated_at();

ORM での UPDATE の書き方(JPA・MyBatis・SQLAlchemy・Prisma)

どの ORM でも、まとめて更新するときは「条件 → SET の内容 → 戻り値の件数」という UPDATE 文と同じ形で書きます。

ORM 書き方 件数の取り方
Spring Data JPA @Modifying @Query("update Item i set i.status = :s where i.stock = 0") int markSoldOut(@Param("s") String s); 戻り値の int
JPA(JPQL) em.createQuery("update Item i set i.price = i.price + 10 where i.category = :c").setParameter("c", "文具").executeUpdate() executeUpdate() の戻り値
MyBatis <update id="markSoldOut">UPDATE items SET status = #{status} WHERE stock = 0</update> マッパーのメソッドを int で宣言
SQLAlchemy 2.0 conn.execute(update(items).where(items.c.stock == 0).values(status="soldout")) result.rowcount(WHERE に合った行数)
Prisma ORM v7 prisma.item.updateMany({ where: { stock: 0 }, data: { status: "soldout" } }) 戻り値の { count }
  • Spring Data JPA の @Modifying は、@Query で書いた UPDATE・DELETE に必須です。Javadoc では flushAutomatically(実行前に永続化コンテキストをフラッシュ)と clearAutomatically(実行後にクリア)が既定で false と説明されています。一括 UPDATE は EntityManager を通さず DB を直接書き換えるため、読み込み済みのエンティティが更新前の値のままになります。クリアするか、同じトランザクションで再取得します。JPA の executeUpdate() はトランザクションが無いと TransactionRequiredException になります。
  • MyBatis の動的 SQL <set> は、<if> で列を出し分けたときに SET キーワードを付け、末尾の余分なカンマを取り除きます。
  • SQLAlchemy の rowcount は「WHERE に合った行数」で、RETURNING を使う UPDATE では取れないことがあります。UPDATE ... FROM は PostgreSQL・SQL Server・MySQL で使えます。
  • Prisma の update() は一意な where が必要で、行が無いと P2025 エラーになります。加算は data: { stock: { decrement: 1 } } のような atomic 操作を使います。updateManyAndReturn()(PostgreSQL・CockroachDB・SQLite)は更新後の行を返します。Prisma ORM 8 では API が変わり、.where({...}).updateAndCount({...}) の形になっています。

ORM が発行する SQL は、ログに出して確かめます。MyBatis の設定と動的 SQL は関連記事「MyBatis使い方|Spring Boot連携と動的SQLタグ全実例」で扱っています。SQLAlchemy の CRUD は関連記事「SQLAlchemy 2.0の使い方|CRUD・リレーションを動くコードで解説」で扱っています。

よくある質問

Q. sql update の基本の書き方は? A. UPDATE テーブル名 SET 列 = 値 WHERE 条件; です。WHERE を省くと全行が対象になります。実行後に表示される件数(UPDATE 3、Rows matched: 3)が想定どおりかを確かめます。

Q. update sql で複数の列を同時に更新するには? A. SET price = 2300, stock = 20, status = 'sale' のように、SET を1回書いて列をカンマで区切ります。SET a = 1 SET b = 2 は構文エラーです。

Q. sql update 複数行を、行ごとに別の値で更新するには? A. SET price = CASE id WHEN 1 THEN 210 WHEN 3 THEN 280 ELSE price END WHERE id IN (1, 3) のように CASE 式を使います。ELSE を省くと対象外の行が NULL になります。行数が多いときは、PostgreSQL の VALUES 一覧との結合や、一時テーブルとの JOIN が読みやすくなります。

Q. sql update 別テーブルの値で更新するには? A. どの DB でも使えるのは SET price = (SELECT ...) WHERE EXISTS (SELECT 1 ...) の相関サブクエリです。MySQL は UPDATE a JOIN b ON ... SET ...、PostgreSQL・SQL Server は UPDATE ... FROM、Oracle は MERGE を使います。相手がいない行は EXISTS や JOIN で除き、相手が複数ある行は先に1行に絞ります。

Q. oracle update 別テーブルの値で更新するには? A. 相関サブクエリ+WHERE EXISTS、または MERGE INTO items i USING new_prices n ON (i.id = n.item_id) WHEN MATCHED THEN UPDATE SET i.price = n.new_price です。Oracle Database 23ai では UPDATE items i SET i.price = n.new_price FROM new_prices n WHERE n.item_id = i.id の直接結合も書けます。サブクエリが行を返さない列は NULL になるので、EXISTS で絞ります。

Q. update set sql で WHERE を忘れたらどうなりますか? A. 全行が同じ値になり、エラーは出ません。BEGIN のあとで COMMIT 前なら ROLLBACK; で戻せます。自動コミットで確定した更新は ROLLBACK では戻らないため、バックアップか、更新前の値に戻す UPDATE が必要になります。MySQL は SET sql_safe_updates = 1; で、キー列を使わない UPDATE をエラーにできます。

Q. update文 sql の更新件数はどこで確かめますか? A. PostgreSQL は UPDATE n、MySQL は Rows matched: n Changed: m(ROW_COUNT() は m)、SQLite は SELECT changes();、SQL Server は @@ROWCOUNT、Oracle は SQL%ROWCOUNT です。0件はエラーになりません。

Q. UPDATE の上位N件だけを更新するには? A. MySQL は ORDER BY ... LIMIT n を UPDATE に直接書けます。PostgreSQL・Oracle は書けないので、WHERE id IN (SELECT id ... ORDER BY ... LIMIT n) のようにサブクエリで主キーを選びます。この形は5つの DB で共通です。

Q. UPDATE で更新した行を確認しながら実行できますか? A. PostgreSQL・SQLite は RETURNING id, price、SQL Server は OUTPUT inserted.price、Oracle は RETURNING ... INTO(PL/SQL)です。MySQL は対応していないため、同じ WHERE で SELECT し直します。

まとめ

  • 基本形は UPDATE テーブル SET 列 = 値 WHERE 条件;。複数列は SET の中をカンマで区切る。実行後の件数(UPDATE n・Rows matched)を必ず見る
  • 行ごと・条件ごとに別の値を入れるときは CASE 式。ELSE 列名 と WHERE をセットで書く。ELSE が無いと NULL で上書きされる
  • 別テーブルの値は、相関サブクエリ+WHERE EXISTS が全DB共通。JOIN 形式は MySQL(JOIN ... SET)・PostgreSQL/SQL Server(FROM)・Oracle(MERGE、23ai は FROM)で構文が違う。相手が複数ある行は JOIN だとどれが使われるか決まらない
  • WHERE なしの UPDATE は全行を書き換えてもエラーにならない。「同じ WHERE で SELECT → BEGIN → UPDATE → 件数確認 → COMMIT/ROLLBACK」を型にする。MySQL は sql_safe_updates で止められる
  • LIMIT・RETURNING・SET の評価順・小数の丸めは DB ごとに違う。DB を移すときは対応表を見て実行して確かめる
  • 大量更新は主キーの範囲で分割し、文ごとに COMMIT する。WHERE の列のインデックス、トリガー、updated_at の自動更新の仕組みも確かめる

参考・出典

確認日はいずれも 2026年10月16日です。

  • MySQL 8.0 Reference Manual「UPDATE Statement」:https://dev.mysql.com/doc/refman/8.0/en/update.html
  • MySQL 8.0 Reference Manual「Using Safe-Updates Mode (--safe-updates)」:https://dev.mysql.com/doc/refman/8.0/en/mysql-tips.html
  • MySQL 8.0 Reference Manual「Automatic Initialization and Updating for TIMESTAMP and DATETIME」:https://dev.mysql.com/doc/refman/8.0/en/timestamp-initialization.html
  • PostgreSQL 16 Documentation「UPDATE」:https://www.postgresql.org/docs/16/sql-update.html
  • SQLite「UPDATE」:https://www.sqlite.org/lang_update.html
  • SQLite「RETURNING」:https://www.sqlite.org/lang_returning.html
  • Microsoft Learn「UPDATE (Transact-SQL)」:https://learn.microsoft.com/en-us/sql/t-sql/queries/update-transact-sql
  • Microsoft Learn「DATEADD (Transact-SQL)」:https://learn.microsoft.com/en-us/sql/t-sql/functions/dateadd-transact-sql
  • Microsoft Learn「Database Engine events and errors (0 to 999)」:https://learn.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-events-and-errors-0-to-999
  • Microsoft Learn「MSSQLSERVER_1205」:https://learn.microsoft.com/en-us/sql/relational-databases/errors-events/mssqlserver-1205-database-engine-error
  • Oracle Database 23ai SQL Language Reference「UPDATE」:https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/UPDATE.html
  • Oracle Database 23ai SQL Language Reference「Data Types(Datetime/Interval Arithmetic)」:https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/Data-Types.html
  • Oracle Database Error Messages「ORA-01427」:https://docs.oracle.com/en/error-help/db/ora-01427/
  • Oracle Database Error Messages「ORA-01407」:https://docs.oracle.com/en/error-help/db/ora-01407/
  • Spring Data JPA Javadoc「Modifying」:https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/Modifying.html
  • Jakarta Persistence 3.2 Javadoc「Query#executeUpdate」:https://jakarta.ee/specifications/persistence/3.2/apidocs/jakarta.persistence/jakarta/persistence/query
  • MyBatis 3「Mapper XML Files」:https://mybatis.org/mybatis-3/sqlmap-xml.html
  • MyBatis 3「Dynamic SQL」:https://mybatis.org/mybatis-3/dynamic-sql.html
  • SQLAlchemy 2.0「Using UPDATE and DELETE Statements」:https://docs.sqlalchemy.org/en/20/tutorial/data_update.html
  • Prisma ORM v7「Prisma Client API reference」:https://www.prisma.io/docs/orm/reference/prisma-client-reference
  • Prisma ORM 8「Writing data」:https://www.prisma.io/docs/orm/fundamentals/writing-data

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

(記載例)出典:株式会社RJC「SQL UPDATE文の書き方|複数列・別テーブル・CASE・DB別構文の早見表」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

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で一緒に開発しながら、 成長を楽しみませんか?