◀ 3.

Oracle 19cのUPDATE文|更新件数・NULL・相関サブクエリ・MERGE

▶

Oracle Database 19c の UPDATE文は、既存行の指定した列を更新します。WHEREで対象行を限定し、更新件数・値・トランザクションの確定を別々に確認します。条件に一致する行が0件でも、通常のUPDATEはそれだけでエラーにはなりません。

この記事の要点
  • WHEREを省略すると、更新対象の表や更新可能な対象に含まれる全行が対象です。SETに書かない列まで直接上書きする意味ではありません。
  • 事前SELECTは対象確認に役立ちますが、その後の並行更新を防ぐものではありません。
  • 更新件数は「値が異なった件数」や「COMMIT済み件数」と同じではありません。
  • 19cでは相関サブクエリやMERGEを使った参照更新ができます。直接FROM結合を持つ後続版の構文とは分けます。
  • COMMITする例はドライランではありません。保存するか、ROLLBACKするかを選びます。

Oracle UPDATE:対象と更新件数を確認し、未確定の変更をCOMMITまたはROLLBACKする

基本構文

DMLの更新操作です。対象表の所有者であるか必要な権限があり、WHERE等での参照に必要な権限も満たすことが前提です。ビュー、ポリシー、制約、トリガーのある対象では、単純な表とは対象範囲や成否が異なります。Oracle 19c UPDATE

教材表と接続の準備

以下は未実行のOracle19c用教材です。専用の使い捨てスキーマ、未使用の表名、表作成とDMLの権限、他の未確定作業がない同一接続を前提にします。準備のDDLとデータ確定を先に済ませます。例を試す間はクライアントの自動コミットを無効にし、DDLを挟みません。SQL*PlusならSET AUTOCOMMIT OFFはクライアントコマンドです。

CREATE TABLE w38_u_users (
    id NUMBER PRIMARY KEY,
    status VARCHAR2(20),
    login_count NUMBER,
    updated_at TIMESTAMP WITH TIME ZONE
);
CREATE TABLE w38_u_orders (
    id NUMBER PRIMARY KEY,
    total NUMBER
);
CREATE TABLE w38_u_items (
    order_id NUMBER REFERENCES w38_u_orders(id),
    price NUMBER,
    qty NUMBER
);
CREATE TABLE w38_u_articles (
    id NUMBER PRIMARY KEY,
    title VARCHAR2(255),
    body CLOB,
    version NUMBER DEFAULT 0 NOT NULL,
    updated_at TIMESTAMP WITH TIME ZONE
);
INSERT INTO w38_u_users (id, status, login_count) VALUES (1, 'pending', 0);
INSERT INTO w38_u_users (id, status, login_count) VALUES (2, NULL, NULL);
INSERT INTO w38_u_orders (id, total) VALUES (10, 999);
INSERT INTO w38_u_orders (id, total) VALUES (20, 888);
INSERT INTO w38_u_items (order_id, price, qty) VALUES (10, 100, 2);
INSERT INTO w38_u_items (order_id, price, qty) VALUES (10, 50, 1);
INSERT INTO w38_u_articles (id, title, body, version)
VALUES (100, 'Old title', 'Old body', 5);
COMMIT;

このCOMMITは教材初期値を保存する準備です。実習のドライランではありません。以下の各例はこの初期状態から始め、末尾のROLLBACKで戻す独立した実習です。SQL*PlusでPL/SQLを実行する場合、末尾の単独スラッシュはブロック送信のためのクライアント表記です。

UPDATE w38_u_users
SET status = 'active',
    updated_at = CURRENT_TIMESTAMP,
    login_count = login_count + 1
WHERE id = 1;
SELECT id, status, login_count FROM w38_u_users WHERE id = 1;
ROLLBACK;

複数列の代入や計算式を使えます。元記事のusersの1列更新、NOW()を使う複数列更新、productsの10%値上げは補足U1に残しています。NOW()はOracle19cの日時関数として使いません。

NULLの代入と条件

NULL + 1はNULLです。NULLを0と見なして加算する業務仕様ならNVLを明示します。NULLはIS NULLで判定し、= NULLや<> NULLを使いません。Oracle19cでは長さ0の文字列もNULLとして扱われます。Oracle 19c Nulls

UPDATE w38_u_users
SET login_count = NVL(login_count, 0) + 1
WHERE id = 2 AND status IS NULL;
SELECT id, status, login_count FROM w38_u_users WHERE id = 2;
ROLLBACK;

WHERE 忘れは全件更新の事故

通常の表でUPDATE w38_u_users SET status='active'と書くと、全行が対象になります。主キー条件なら対象を絞れますが、WHEREが書いてあることだけで1件になるわけではありません。制約違反等で失敗する場合もあり、「全行が必ず書き換わりCOMMITも済む」という保証ではありません。

次は対象確認と件数判定の教材です。DBMS_OUTPUT表示を使うツールでは出力を有効にします。SQL*PlusのSET SERVEROUTPUT ONはSQLではありません。

SELECT id, status FROM w38_u_users WHERE id = 1;
SAVEPOINT before_update;
DECLARE
    affected_rows PLS_INTEGER;
BEGIN
    UPDATE w38_u_users SET status = 'active' WHERE id = 1;
    affected_rows := SQL%ROWCOUNT;
    DBMS_OUTPUT.PUT_LINE('affected_rows=' || affected_rows);
    IF affected_rows <> 1 THEN
        ROLLBACK TO before_update;
        RAISE_APPLICATION_ERROR(-20001, 'Unexpected row count');
    END IF;
END;
/
SELECT id, status FROM w38_u_users WHERE id = 1;
ROLLBACK;

SQL%ROWCOUNTは別のSQLを実行する前に取得します。これはUPDATE対象行の件数で、変更前後の値が異なる行だけを別途数えたものでも、COMMIT成功の証拠でもありません。MySQLの変更行/一致行の返却設定とも混同しません。Oracle implicit cursor attributes、MySQL UPDATE

補足U2はWHEREなしの危険例を含むため実行手順にしません。U3のSQL_SAFE_UPDATESはMySQL8.4のサーバー変数を使う比較です。Workbench専用のSQLではなく、キー利用やLIMIT等に関係する保護で、Oracleの設定でも全件事故を完全に防ぐ保証でもありません。MySQL sql_safe_updates

JOIN を使った UPDATE

他表の値で更新する方法は製品・版によって異なります。既存の比較用SQL3本は補足U4に残しています。

DB参照更新の例
MySQL / MariaDBUPDATE a JOIN b ON ... SET a.col=b.col。細部の対応はそれぞれの版で確認
PostgreSQL18UPDATE 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 ...
Oracle19c相関サブクエリ、MERGE等。すべての更新可能ビューの方式を列挙したものではない

Oracleの後続版UPDATE資料にはFROMを使う直接結合があります。「Oracleは全版で直接JOIN不可」とはしません。ただし本記事の19c例へ持ち込まず、正確な初出パッチや対応環境は対象版の資料で確認します。

19cの相関サブクエリ: 0行・1行・複数行

単一値を代入するサブクエリは対象行ごとに1行1列を返す条件が必要です。0行ならNULLが代入され、複数行ならエラーになります。NOT NULL等の制約があればNULL代入も失敗し得ます。複数列の代入では返す列数も合わせます。

次は1注文ごとに集約するため、GROUP BYでorder_idが1行にまとまります。WHERE EXISTSにより明細がない注文を対象外にします。明細のprice/qtyがNULLなら積もNULLとなり、SUMの結果や業務上の扱いは別途決める必要があります。

UPDATE w38_u_orders o
SET total = (
    SELECT SUM(i.price * i.qty)
    FROM w38_u_items i
    WHERE i.order_id = o.id
    GROUP BY i.order_id
)
WHERE EXISTS (
    SELECT 1 FROM w38_u_items i WHERE i.order_id = o.id
);
SELECT id, total FROM w38_u_orders ORDER BY id;
ROLLBACK;

教材ではid=10が250、id=20は888のままという期待です。EXISTSを外したこのGROUP BY付き例では、id=20のサブクエリは0行なのでtotalがNULLになります。GROUP BYなしのSUMなら明細0件でもNULLの1行を返し、COUNTなら0の1行になる点も区別します。任意の1行をFETCHして複数行問題を隠すのではなく、対応キー・一意制約・集計の業務仕様を確認します。

19cのMERGE: 明細から合計を更新

元記事と同じ「明細を集約して注文合計へ反映する」例です。USINGの結果は1つのorder_idにつき1行、更新先idは主キーです。WHEN MATCHEDだけなので、注文の新規挿入はしません。明細のない注文も変更しません。

MERGE INTO w38_u_orders o
USING (
    SELECT order_id, SUM(price * qty) AS total
    FROM w38_u_items
    GROUP BY order_id
) t
ON (o.id = t.order_id)
WHEN MATCHED THEN UPDATE SET o.total = t.total;
SELECT id, total FROM w38_u_orders ORDER BY id;
ROLLBACK;

同じ対象行に複数のsource行が対応する形を許容する一般例ではありません。MERGEは同じ対象行を複数回更新できません。任意のMAXで1行へ潰す修正は、正しい値の決定規則がある場合だけ検討します。Oracle19c MERGE

RETURNING で更新後の行を取得

Oracle19cではRETURNING ... INTOで変数へ受けます。PostgreSQLの結果セットを返すRETURNINGや、SQL ServerのOUTPUTと同じ受取り方ではありません。元記事の:new_countは出力バインドを用意するホスト側の例で、宣言不要のPL/SQLローカル変数ではありません。

次は主キー条件で最大1行となるPL/SQL例です。0行なら受取変数の値は未定義なので使いません。複数行用のコレクションを扱う完成例ではありません。Oracle19c RETURNING INTO

DECLARE
    new_count NUMBER;
    affected_rows PLS_INTEGER;
BEGIN
    UPDATE w38_u_users
    SET login_count = NVL(login_count, 0) + 1
    WHERE id = 1
    RETURNING login_count INTO new_count;
    affected_rows := SQL%ROWCOUNT;
    IF affected_rows = 1 THEN
        DBMS_OUTPUT.PUT_LINE('new_count=' || new_count);
    ELSE
        DBMS_OUTPUT.PUT_LINE('No matching row');
    END IF;
END;
/
ROLLBACK;

補足U5にはPostgreSQL・Oracleバインド・SQL Serverの3例を残しています。MySQL8.4のUPDATE構文にはこのRETURNINGはありません。これをMariaDBや将来版まで一括した結論にしません。PostgreSQL18 UPDATE、SQL Server UPDATE

LIMIT 付き UPDATE

補足U6のMySQL例は一意なidをORDER BYしてLIMITする単一表更新です。PostgreSQLの旧CTE例にはORDER BYがないため、どの1000行かが固定されません。同じ数値idが一意なlogs(id, archived)教材表を前提とした修正版は次です。Oracle19c向けの構文ではありません。

-- PostgreSQL 18 comparison; dedicated teaching table
WITH target AS (
    SELECT id
    FROM logs
    WHERE archived = 0
    ORDER BY id
    LIMIT 1000
)
UPDATE logs
SET archived = 1
WHERE id IN (SELECT id FROM target);

分割COMMITは一括取消できる単位も分割します。並行worker、再実行、行の追加や状態変更、確定間隔を別途設計します。ORDER BYだけで同時実行時の重複処理を防げるわけではありません。19cのUPDATEへ直接ORDER BY/LIMITを足す手順にはしません。

TRANSACTION で安全に

事前SELECT、UPDATE直後の件数、更新後の値、最後のCOMMIT成功は別々の確認です。実習はROLLBACKで終了します。本当に保存する作業ならCOMMITを選びます。COMMITしてから続けてROLLBACKしても、その確定済み更新は戻りません。

Oracle19cではSQLのBEGIN/START TRANSACTIONを付けません。SAVEPOINTによる部分取消も、同じ接続に保存点と未確定変更が残っている間だけです。通常のDDLは暗黙COMMIT境界になるため、表や索引作成を更新確認の途中へ混ぜません。Oracle19c COMMIT

Optimistic Locking (楽観的ロック)

読み取ったversionをWHEREへ入れ、更新時に増やします。教材のw38_u_articlesは初期version=5です。複数の書込み経路が同じルールへ従うことが前提です。

SELECT id, title, body, version FROM w38_u_articles WHERE id = 100;
DECLARE
    affected_rows PLS_INTEGER;
BEGIN
    UPDATE w38_u_articles
    SET title = 'New title',
        body = 'New body',
        version = version + 1,
        updated_at = CURRENT_TIMESTAMP
    WHERE id = 100 AND version = 5;
    affected_rows := SQL%ROWCOUNT;
    DBMS_OUTPUT.PUT_LINE('affected_rows=' || affected_rows);
END;
/
SELECT id, title, version FROM w38_u_articles WHERE id = 100;
ROLLBACK;

0行は「このidとversionの組合せを更新できなかった」ことを示します。他者の先行更新だけでなく、対象不存在なども原因になり得るため、0行だけから原因を断定しません。初期値・削除・ポリシー・他writerも確認します。制約違反等のSQLエラーは、正常終了の0行と別に扱います。

補足U8のBIGINT/TEXT/NOW()のDDL/DMLは非Oracleの比較として残します。Oracle版ではNUMBER/VARCHAR2/CLOBとDEFAULT 0 NOT NULLを使います。U9のJPA @Versionはマッピングの抜粋です。import、コンストラクタ、アクセス方法や永続化設定を省略しており、完成したエンティティではありません。providerの競合検出はflush/commit等で表面化し得ます。任意の生SQL更新にも自動でversion条件が付くという保証ではありません。Jakarta Persistence3.2

TRIGGER による副作用

UPDATEによりトリガーが動くことがあります。指定表の件数だけで、監査表等への副作用まで把握したことにはなりません。補足U10はMySQLのAFTER UPDATE / NEW / OLDの例です。Oracle用CREATE TRIGGERとして貼り付けません。外部送信や別の独立トランザクションに副作用があるなら、主処理のROLLBACKと同じ取消範囲とも限りません。

性能: インデックスと UPDATE

索引の有無だけで全件スキャンや速度を決めつけません。選択率・統計・条件式・更新列の索引保守・制約・トリガー・競合・データ量等を分けて確認します。binlog/WALやページ分割の説明も製品・格納方式依存です。LIMITループを全製品共通の改善策とはしません。

次はOracleの計画取得の書き方です。対象表の権限に加え、使用するPLAN_TABLEへの書込み等の前提があります。対象UPDATEを実行するものではありませんが、計画表には書込みます。ここでは実行していません。Oracle19c EXPLAIN PLAN

EXPLAIN PLAN FOR
UPDATE w38_u_users SET status = 'active' WHERE id = 1;

補足U11のEXPLAIN UPDATEとtype: ALL/ref/rangeはMySQLの比較です。ALLは全件走査の表示で、索引が存在しない証拠ではありません。CREATE INDEXを診断のため自動実行せず、同じ結果を保つか、計画と負荷がどう変わるかを別途確認します。

FAQ

影響行数を知るには? Oracle PL/SQLでは更新直後のSQL%ROWCOUNTを保存します。MySQLのROW_COUNT()やPGのGET DIAGNOSTICS ... ROW_COUNTはそれぞれの処理系用です。API/ORMの戻り値の意味も確認します。

UPDATEが0行なら例外? 正常終了の0行もあります。存在確認、条件、NULL、versionなどを調べ、必要な業務上の期待件数を別途判定します。

UPDATEが遅いなら? 対象SQLの計画、対象数、待機、トリガーや制約を分けます。索引追加やバッチ分割だけを一律の解決策にしません。

同じ行へ同時更新すると? 行ロックによる待機は起こり得ますが、先に読み取った値を後から上書きする問題まで自動的に防ぐわけではありません。トランザクションを短くし、必要ならversion条件等で競合を検出します。

既存コードの製品別補足

現行11個のpreを、製品・前提・訂正点付きで残します。Oracleの教材表w38_u_*とは別の表名です。旧例を一括実行せず、特にDDL・全件UPDATE・COMMITを含むものは実行用の確認手順にしないでください。

U1

U1: users(id,status,updated_at,login_count)、products(price,updated_at,category)を想定した既存比較です。NOW()はMySQL/PG型でOracle19cには使いません。productsのCURRENT_TIMESTAMPは対応する列型が前提です。NULL+1はNULLです。本文のOracle例と区別し、値上げ処理を確認用に実行しません。

-- 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';

U2

U2: 実行しない旧事故例です。WHEREなしは全行対象で、SELECTが1件でも後の並行変更を防ぎません。BEGINはOracleのトランザクション開始SQLではありません。COMMITは確定でドライランではありません。users(id,status)と専用接続があっても一括コピー実行しないでください。

-- ❌ 全件 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

U3

U3: MySQL8.4の専用教材接続・users(status)を想定した比較です。SQL_SAFE_UPDATESはOracleにはなく、Workbenchに限る機能でもありません。設定や実行計画等の条件なしに必ず同じ1175を観測したと主張しません。保護解除や本番設定変更の案内ではありません。

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

U4

U4: 3製品を区切った参照更新例です。同じ論理列orders(id,total)、order_items(order_id,price,qty)、一意なorders.idと各表への必要権限が前提です。sourceはGROUP BYでorder_idごとに1行、明細なしの注文は未更新です。MySQL/PGの構文をOracleへ貼り付けず、Oracle19cではここに残したMERGEを使います。NULL値と重複sourceの意味は本文を参照してください。

-- 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;

U5

U5: PostgreSQL/Oracle/SQL Serverの比較です。users(id,login_count,last_login)、一意なid=1、適切な列型・権限・接続設定が必要です。Oracleの:new_countはホスト側の出力bind宣言が別途必要です。最大1行を前提とし、0行時の受取変数を読みません。複数行を単一変数へ受ける完成例ではありません。

-- 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;

U6

U6: MySQL/PGの比較です。logs(id,archived)、一意なid、専用教材接続が前提です。旧PG側にはORDER BYがないため、対象1000行が固定されません。修正版は本文にあります。ループ、同時worker、COMMIT単位や再試行を実装した例ではなく、Oracle19cにUPDATE LIMITを勧めるものでもありません。

-- 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);

U7

U7: 実行しない旧トランザクション断片です。BEGIN/START TRANSACTIONはOracle19cの開始SQLではありません。3aと3bは二者択一であり、COMMITした後のROLLBACKで3aを取消できません。users(id,status)と専用接続があっても連続実行せず、本文のROLLBACKで終える実習と区別します。

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;

U8

U8: MySQL/PG型の旧DDL/DMLです。BIGINT/TEXT/NOW()とDEFAULTの配置をOracle19c用と扱いません。新規未使用articles表を作る例で既存表へ実行せず、DDLの確定境界を確認します。0件を必ず他者の先行更新とする旧コメントは採用しません。本文にOracleの完全な準備DDLと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 件 = 他者に先を越された
-- → アプリ側で「他のユーザーが編集しました」を返す

U9

U9: JPA/Hibernateのマッピング抜粋です。import、適切なコンストラクタ・アクセス方式・provider/DB設定を省略した完成前の例です。@Versionの競合はflush/commit等で表面化する場合があり、任意の生SQLまで自動保護されるわけではありません。

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

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

U10

U10: MySQLのDDL/トリガー比較です。users(id,status)、audit_log(table_name,row_id,old_status,new_status,changed_at)と適切な型・権限・未使用trigger名が必要です。Oracle構文ではなく、DDLの暗黙COMMITやaudit行の追加を伴います。観察するだけのコマンドではありません。

-- 例: ユーザー更新を 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;

U11

U11: MySQLの計画/索引比較です。users(status,email)と必要権限を想定します。ALLは全走査で、索引がない証拠ではありません。ref/range等は固定の実測結果ではありません。CREATE INDEXは変更操作なので診断として自動実行しません。Oracle版のEXPLAIN PLANも計画表書込みを伴う点は本文に記載しました。

-- 実行計画で確認
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);
子ページ

子ページはありません

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

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