- SQL
SQL UPDATE文の書き方|複数列・別テーブル・CASE・DB別構文の早見表
結論
SQL の UPDATE 文は UPDATE テーブル名 SET 列 = 値 WHERE 条件; と書きます。複数列は SET の中をカンマで区切り、WHERE を省くと全行が対象です。
この記事でわかること
- 状況別13パターンの早見表(複数列・CASE・別テーブル)
- 5つの DB の構文対応表(JOIN・LIMIT・件数)
- WHERE なしの事故を ROLLBACK で戻す5ステップ
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期連続の黒字決算。