3.

SQL UPDATE 文の完全ガイド(複数カラム / JOIN / RETURNING / 楽観ロック)

編集
この記事の要点
  • UPDATE table SET col = value WHERE cond が基本構文
  • 複数カラム更新は SET col1 = v1, col2 = v2 とカンマ区切り
  • UPDATE ... FROM ... JOIN ... で他テーブルの値を参照して更新(PG / SQL Server)
  • PostgreSQL は RETURNING * で更新後行を取得可能。MySQL は非対応
  • Optimistic Locking: version 列を WHERESET 両方に入れて競合検知
  • WHERE 忘れは全件更新 → 必ず BEGIN; UPDATE...; SELECT...; COMMIT; でドライラン

基本構文

UPDATE は DML (Data Manipulation Language) の代表格で、テーブルの既存行を書き換えるためのコマンドです。

-- 1 カラム更新
UPDATE users
SET status = 'active'
WHERE id = 1;

-- 複数カラム同時更新
UPDATE users
SET status = 'active',
    updated_at = NOW(),
    login_count = login_count + 1
WHERE id = 1;

-- 計算式・関数も可
UPDATE products
SET price = price * 1.10,           -- 10% 値上げ
    updated_at = CURRENT_TIMESTAMP
WHERE category = 'food';

WHERE 忘れは全件更新の事故

UPDATE で最も致命的なミスは WHERE 句の付け忘れです。1 行のつもりがテーブル全行を書き換えます。

-- ❌ 全件 status='active' になる
UPDATE users SET status = 'active';

-- ✅ 必ず WHERE を付ける
UPDATE users SET status = 'active' WHERE id = 1;

-- ✅ さらに安全: 件数を SELECT で先に確認
SELECT COUNT(*) FROM users WHERE id = 1;  -- 1 のはず
BEGIN;
UPDATE users SET status = 'active' WHERE id = 1;
-- 影響行数を確認してから
COMMIT;  -- または ROLLBACK

MySQL Workbench には safe_updates モードがあり、WHERE 句が主キー / インデックスを含まない UPDATE をエラーで弾けます:

SET SQL_SAFE_UPDATES = 1;
UPDATE users SET status = 'active';  -- → Error Code 1175

JOIN を使った UPDATE

他のテーブルの値を参照して更新したい場合、DB ごとに構文が違います。

DB構文
MySQL / MariaDBUPDATE a JOIN b ON ... SET a.col = b.col
PostgreSQLUPDATE a SET col = b.col FROM b WHERE a.id = b.a_id
SQL ServerUPDATE a SET col = b.col FROM a JOIN b ON ...
Oracleサブクエリ or MERGE 文
-- MySQL: 注文テーブルの total を、明細から再計算
UPDATE orders o
JOIN (
    SELECT order_id, SUM(price * qty) AS total
    FROM order_items
    GROUP BY order_id
) t ON o.id = t.order_id
SET o.total = t.total;

-- PostgreSQL: 同じ内容
UPDATE orders o
SET total = t.total
FROM (
    SELECT order_id, SUM(price * qty) AS total
    FROM order_items
    GROUP BY order_id
) t
WHERE o.id = t.order_id;

-- Oracle: MERGE を使うのが定番
MERGE INTO orders o
USING (
    SELECT order_id, SUM(price * qty) AS total
    FROM order_items
    GROUP BY order_id
) t
ON (o.id = t.order_id)
WHEN MATCHED THEN UPDATE SET o.total = t.total;

RETURNING で更新後の行を取得

PostgreSQL / Oracle / SQL Server (OUTPUT) では、UPDATE と同時に更新後の値を取得できます。MySQL は非対応です。

-- PostgreSQL
UPDATE users
SET login_count = login_count + 1,
    last_login = NOW()
WHERE id = 1
RETURNING id, login_count, last_login;

-- Oracle (PL/SQL)
UPDATE users
SET login_count = login_count + 1
WHERE id = 1
RETURNING login_count INTO :new_count;

-- SQL Server
UPDATE users
SET login_count = login_count + 1
OUTPUT INSERTED.id, INSERTED.login_count
WHERE id = 1;

LIMIT 付き UPDATE

大量行を分割して更新したいときに使います。必ず ORDER BY と併用すべきです。

-- MySQL: 1000 行ずつ status を更新
UPDATE logs
SET archived = 1
WHERE archived = 0
ORDER BY id
LIMIT 1000;
-- これをループで実行(負荷分散)

-- PostgreSQL は UPDATE LIMIT 非対応。CTE で代用
WITH target AS (
    SELECT id FROM logs WHERE archived = 0 LIMIT 1000
)
UPDATE logs
SET archived = 1
WHERE id IN (SELECT id FROM target);

TRANSACTION で安全に

「失敗したら戻す」運用には必須。ドライランにも使えます。

BEGIN;  -- または START TRANSACTION

-- 1. 対象を SELECT で確認
SELECT id, status FROM users WHERE status = 'pending';

-- 2. UPDATE 実行
UPDATE users SET status = 'active' WHERE status = 'pending';
-- 影響行数を確認

-- 3a. 想定通り → コミット
COMMIT;

-- 3b. おかしい → ロールバック
ROLLBACK;

Optimistic Locking (楽観的ロック)

複数ユーザーが同じ行を編集する Web アプリでは、version 列を使った楽観ロックが定番です。

-- テーブル設計
CREATE TABLE articles (
    id BIGINT PRIMARY KEY,
    title VARCHAR(255),
    body TEXT,
    version INT NOT NULL DEFAULT 0,
    updated_at TIMESTAMP
);

-- 1. 取得時に version を保持
SELECT id, title, body, version FROM articles WHERE id = 100;
-- → version = 5

-- 2. 更新時に「version が 5 のまま」を条件に
UPDATE articles
SET title = '新しいタイトル',
    body  = '新しい本文',
    version = version + 1,
    updated_at = NOW()
WHERE id = 100
  AND version = 5;        -- ★ ここがポイント

-- 影響行数 0 件 = 他者に先を越された
-- → アプリ側で「他のユーザーが編集しました」を返す

Java JPA / Hibernate は @Version アノテーションで自動化:

@Entity
public class Article {
    @Id Long id;
    String title;
    @Version Integer version;   // ★ Hibernate が自動で WHERE version=? を付ける
}

// 他者更新済みなら OptimisticLockException が飛ぶ

TRIGGER による副作用

UPDATE 時に BEFORE/AFTER UPDATE トリガーが走り、別テーブルを更新したり履歴を残したりできます。

-- 例: ユーザー更新を audit_log に残す(MySQL)
CREATE TRIGGER trg_users_audit
AFTER UPDATE ON users
FOR EACH ROW
INSERT INTO audit_log (table_name, row_id, old_status, new_status, changed_at)
VALUES ('users', NEW.id, OLD.status, NEW.status, NOW());

-- UPDATE すると自動で audit_log に行が追加される
UPDATE users SET status = 'active' WHERE id = 1;

性能: インデックスと UPDATE

  • WHERE 句のカラムにインデックスが無いと、対象 1 行でも全件スキャンになる
  • 更新対象カラム自体にもインデックスがあるとインデックス更新のコストがかかる
  • 大量行の UPDATE は LIMIT + ループで分割し、binlog / WAL の肥大を防ぐ
  • 更新前後で行サイズが大きく増える場合 (TEXT 等) はページ分割が起きて遅い → 行末追記設計を検討
-- 実行計画で確認
EXPLAIN UPDATE users SET status = 'active' WHERE email = 'a@example.com';
-- type: ALL → インデックス無し(テーブル全スキャン)
-- type: ref / range → インデックス利用

-- email にインデックスを張る
CREATE INDEX idx_users_email ON users(email);

FAQ

Q: UPDATE で影響を受けた行数を知りたい
A: MySQL: SELECT ROW_COUNT()。PostgreSQL: GET DIAGNOSTICS rc = ROW_COUNT (PL/pgSQL)。ORM では UPDATE 戻り値が件数のことが多い。

Q: UPDATE が遅すぎる
A: WHERE 句に index があるか EXPLAIN で確認。大量行ならバッチ分割。トリガーや CHECK 制約も犯人候補。

Q: 同一行を同時に UPDATE すると?
A: 行ロックで直列化されます。長時間トランザクションはデッドロックの元 → トランザクションは短く。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. SELECT文
  2. INSERT
  3. UPDATE文
  4. DELETE文
  5. WHERE句
  6. JOIN句
  7. 集合演算子
  8. インラインビュー
  9. 副問い合わせ (サブクエリ)

最近更新/作成されたページ