OracleのUPDATE文で「先頭の100件だけ更新したい」「特定の条件で並べ替えた上位N件だけ更新したい」と思ったことはありませんか?
MySQLなら UPDATE ... LIMIT 100 で簡単にできますが、OracleにはLIMIT句がありません。代わりに ROWNUM、ROW_NUMBER()、FETCH FIRST N ROWS ONLY(12c以降)などを使って件数を制限します。
この記事では、件数を指定してUPDATEする全パターンを、基本から実務レベルまで網羅的に解説します。
この記事でわかること
ROWNUM を使った基本的な件数制限UPDATE
- ソート後のN件を更新する方法(サブクエリ)
ROW_NUMBER() OVER() による柔軟な件数指定
FETCH FIRST N ROWS ONLY(Oracle 12c+)の活用
UPDATE ... WHERE IN (サブクエリ) パターン
- 大量データのバッチ更新(分割更新)テクニック
- MySQL / SQL Server / PostgreSQL との比較
- トランザクションでの安全な更新手順
OracleにはLIMIT句がない
MySQLやPostgreSQLでは、UPDATE文に LIMIT を付けるだけで更新件数を制限できます。
MySQL(参考:OracleではNG)
-- MySQLでは可能だが、Oracleではエラーになる
UPDATE employees
SET status = 'inactive'
LIMIT 100; -- ORA-00933: SQL command not properly ended
注意:Oracleでは UPDATE文に LIMIT 句を直接使用できません。代わりに ROWNUM や分析関数を利用します。
Oracleで件数を制限してUPDATEするには、以下のいずれかの方法を使います。
| 方法 |
バージョン |
特徴 |
ROWNUM |
全バージョン |
最もシンプル、ソート不可 |
ROWNUM + サブクエリ |
全バージョン |
ソート後のN件を更新可能 |
ROW_NUMBER() OVER() |
9i以降 |
柔軟なソート・条件指定 |
FETCH FIRST N ROWS ONLY |
12c以降 |
直感的なSQL標準構文 |
WHERE IN (サブクエリ) |
全バージョン |
主キー指定で確実な更新 |
ROWNUM を使った件数制限UPDATE(基本)
ROWNUM はOracleが行を返す際に自動的に付与する疑似列です。WHERE句に指定することで、更新対象の行数を制限できます。
SQL
-- 最初の100件だけを更新する
UPDATE employees
SET status = 'inactive'
WHERE department_id = 10
AND ROWNUM <= 100;
ROWNUMの条件の書き方
ROWNUM は行が返される順番に1から振られるため、条件の書き方にルールがあります。
| 条件 |
動作 |
結果 |
ROWNUM <= 100 |
先頭100件 |
正常動作 |
ROWNUM < 101 |
先頭100件 |
正常動作(同じ意味) |
ROWNUM = 1 |
先頭1件 |
正常動作 |
ROWNUM = 5 |
5番目の行のみ |
0件になる(注意) |
ROWNUM > 5 |
6件目以降 |
0件になる(注意) |
注意:ROWNUMは行が返されるときに順番に振られるため、ROWNUM = 5 や ROWNUM > 5 のように「先頭を飛ばす」条件では結果が0件になります。ROWNUMは必ず1から始まる連番としてしか使えません。
ROWNUMの重要な制限:ソート順が保証されない
ROWNUM は WHERE句の評価時に振られるため、ORDER BY より先に番号が付きます。つまり、ROWNUM だけでは「特定の並び順で上位N件」を正確に更新することはできません。
NG例:意図通りに動かない
-- 給与の低い順に100件更新したいが…
UPDATE employees
SET bonus = 5000
WHERE ROWNUM <= 100
ORDER BY salary ASC; -- ORDER BYはROWNUMの後に評価される!
このSQL自体は構文エラーになります。UPDATE文ではORDER BY句は使えません。ソート後のN件を更新するには、次のセクションで紹介するサブクエリを使う方法が必要です。
ROWNUM + サブクエリでソート後のN件を更新
「給与が低い順に100件だけ更新したい」のように、特定の順序で上位N件を更新するには、サブクエリで先にソートしてからROWNUMで件数を制限します。
SQL
-- 給与が低い順に上位100件を更新
UPDATE employees
SET bonus = 5000
WHERE employee_id IN (
SELECT employee_id
FROM (
SELECT employee_id
FROM employees
ORDER BY salary ASC
)
WHERE ROWNUM <= 100
);
ポイント:サブクエリの中で ORDER BY を実行し、外側のクエリで ROWNUM を使う「二重サブクエリ」構造がポイントです。内側でソートしてから外側で件数を絞ることで、正しい順序での上位N件を取得できます。
WHERE条件付きでソート後のN件を更新
SQL
-- 部署10の社員のうち、入社日が古い順に50件を更新
UPDATE employees
SET status = 'senior'
WHERE employee_id IN (
SELECT employee_id
FROM (
SELECT employee_id
FROM employees
WHERE department_id = 10
ORDER BY hire_date ASC
)
WHERE ROWNUM <= 50
);
ROW_NUMBER() OVER() を使った件数指定UPDATE
ROW_NUMBER() はOracle 9i以降で使える分析関数(ウィンドウ関数)で、ソート順を明示的に指定した行番号を振ることができます。ROWNUMより柔軟で、実務ではこの方法が最もよく使われます。
SQL
-- 給与が高い順に上位50件を更新
UPDATE employees
SET bonus = 10000
WHERE employee_id IN (
SELECT employee_id
FROM (
SELECT employee_id,
ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn
FROM employees
)
WHERE rn <= 50
);
ROW_NUMBER() のメリット
| 比較項目 |
ROWNUM |
ROW_NUMBER() |
| ソート順の指定 |
不可(サブクエリ必須) |
OVER句で直接指定 |
| 範囲指定(N件目〜M件目) |
困難 |
rn BETWEEN 51 AND 100 で可能 |
| PARTITION BY |
不可 |
グループごとのN件に対応 |
| 可読性 |
シンプル |
意図が明確 |
範囲指定:N件目〜M件目を更新する
ROW_NUMBER() なら、「51件目から100件目まで」のような範囲指定も簡単にできます。
SQL
-- 51件目〜100件目を更新(ページング的な更新)
UPDATE employees
SET processed_flag = 'Y'
WHERE employee_id IN (
SELECT employee_id
FROM (
SELECT employee_id,
ROW_NUMBER() OVER (ORDER BY employee_id) AS rn
FROM employees
WHERE processed_flag = 'N'
)
WHERE rn BETWEEN 51 AND 100
);
PARTITION BY でグループごとにN件更新
部署ごとに上位3名のボーナスを更新するなど、グループ単位での件数制限ができるのは ROW_NUMBER() ならではの機能です。
SQL
-- 部署ごとに給与上位3名のボーナスを更新
UPDATE employees
SET bonus = 20000
WHERE employee_id IN (
SELECT employee_id
FROM (
SELECT employee_id,
ROW_NUMBER() OVER (
PARTITION BY department_id
ORDER BY salary DESC
) AS rn
FROM employees
)
WHERE rn <= 3
);
実行結果
部署10: 3名更新
部署20: 3名更新
部署30: 3名更新
合計: 9行が更新されました。
FETCH FIRST N ROWS ONLY(Oracle 12c以降)
Oracle 12c で導入された FETCH FIRST N ROWS ONLY は、SQL標準に準拠した行制限構文です。SELECT文では直接使えますが、UPDATE文には直接使えないため、サブクエリとして組み合わせます。
SELECT での基本構文
SQL(SELECT文での使用例)
-- 給与が高い順に上位10件を取得
SELECT employee_id, last_name, salary
FROM employees
ORDER BY salary DESC
FETCH FIRST 10 ROWS ONLY;
UPDATE と組み合わせる
SQL
-- FETCH FIRST を使って上位10件を更新(12c以降)
UPDATE employees
SET bonus = 15000
WHERE employee_id IN (
SELECT employee_id
FROM employees
ORDER BY salary DESC
FETCH FIRST 10 ROWS ONLY
);
OFFSETと組み合わせる
OFFSET を使うと、先頭の何件かをスキップしてから取得できます。
SQL
-- 11番目〜20番目の社員を更新
UPDATE employees
SET review_status = 'pending'
WHERE employee_id IN (
SELECT employee_id
FROM employees
ORDER BY hire_date ASC
OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY
);
ポイント:FETCH FIRST N ROWS ONLY は内部的に ROW_NUMBER() に変換されるため、パフォーマンスは同等です。可読性に優れているため、12c以降の環境ではこの構文がおすすめです。
UPDATE + WHERE IN (サブクエリ) パターン
主キーを使って更新対象を明確に指定するパターンです。件数制限だけでなく、複雑な条件による更新にも対応できます。
基本パターン
SQL
-- 未処理データのうち最新200件を処理済みに更新
UPDATE orders
SET processed = 'Y',
processed_date = SYSDATE
WHERE order_id IN (
SELECT order_id
FROM (
SELECT order_id
FROM orders
WHERE processed = 'N'
ORDER BY order_date DESC
)
WHERE ROWNUM <= 200
);
EXISTS を使ったパターン
大量データの場合、IN より EXISTS の方がパフォーマンスが良いことがあります。
SQL
-- EXISTS を使った件数制限UPDATE
UPDATE employees e
SET e.bonus = 5000
WHERE EXISTS (
SELECT 1
FROM (
SELECT employee_id
FROM employees
WHERE department_id = 20
AND ROWNUM <= 50
) sub
WHERE sub.employee_id = e.employee_id
);
バッチ更新(大量データの分割更新)
数百万件のデータを一度にUPDATEすると、UNDO領域(ロールバックセグメント)の枯渇やロック長期化の問題が起きます。バッチ更新は大量データを小さな単位に分割して更新するテクニックです。
PL/SQL によるバッチ更新
PL/SQL
DECLARE
v_batch_size NUMBER := 1000; -- 1回あたりの更新件数
v_total NUMBER := 0;
BEGIN
LOOP
UPDATE large_table
SET status = 'processed'
WHERE status = 'pending'
AND ROWNUM <= v_batch_size;
-- 更新件数を取得
v_total := v_total + SQL%ROWCOUNT;
-- 更新対象がなくなったら終了
EXIT WHEN SQL%ROWCOUNT = 0;
-- バッチごとにコミット
COMMIT;
DBMS_OUTPUT.PUT_LINE('累計更新件数: ' || v_total);
END LOOP;
COMMIT;
DBMS_OUTPUT.PUT_LINE('完了: 合計 ' || v_total || ' 件更新');
END;
/
実行結果
累計更新件数: 1000
累計更新件数: 2000
累計更新件数: 3000
...
完了: 合計 50000 件更新
バッチサイズの目安
| データ規模 |
推奨バッチサイズ |
理由 |
| 〜10万件 |
1,000〜5,000 |
小規模なら一度に更新可能 |
| 10万〜100万件 |
5,000〜10,000 |
UNDO領域とのバランス |
| 100万件以上 |
10,000〜50,000 |
ロック時間とスループットの最適化 |
注意:バッチ更新でCOMMITを挟むと、途中でエラーが発生した場合に一部のみ更新された状態になります。データの整合性を重視する場合は、1トランザクションで更新するか、処理済みフラグなどで再実行可能な設計にしてください。
BULK COLLECT + FORALL による高速バッチ更新
さらにパフォーマンスを求める場合は、BULK COLLECT と FORALL を使います。
PL/SQL(高速バッチ)
DECLARE
TYPE id_table IS TABLE OF employees.employee_id%TYPE;
v_ids id_table;
CURSOR c_emp IS
SELECT employee_id
FROM employees
WHERE status = 'pending';
BEGIN
OPEN c_emp;
LOOP
FETCH c_emp BULK COLLECT INTO v_ids LIMIT 1000;
EXIT WHEN v_ids.COUNT = 0;
FORALL i IN 1 .. v_ids.COUNT
UPDATE employees
SET status = 'active'
WHERE employee_id = v_ids(i);
COMMIT;
END LOOP;
CLOSE c_emp;
END;
/
ポイント:FORALL は通常のFORループより数倍〜数十倍高速です。PL/SQLとSQLエンジン間のコンテキストスイッチを削減できるため、大量データの更新に適しています。
他のRDBMSとの比較
Oracle以外のRDBMSでは、UPDATE文に直接件数制限を指定できるものがあります。
| RDBMS |
構文 |
備考 |
| Oracle |
WHERE ROWNUM <= N |
LIMIT句なし。サブクエリが必要 |
| MySQL |
UPDATE ... LIMIT N |
直接指定可能 |
| SQL Server |
UPDATE TOP(N) ... |
直接指定可能 |
| PostgreSQL |
WHERE ctid IN (SELECT ... LIMIT N) |
LIMITはSELECTのみ。サブクエリ必要 |
MySQL の書き方
MySQL
-- MySQL: UPDATE + LIMIT
UPDATE employees
SET status = 'inactive'
WHERE department_id = 10
ORDER BY hire_date ASC
LIMIT 100;
SQL Server の書き方
SQL Server
-- SQL Server: UPDATE TOP
UPDATE TOP(100) employees
SET status = 'inactive'
WHERE department_id = 10;
PostgreSQL の書き方
PostgreSQL
-- PostgreSQL: サブクエリ + LIMIT
UPDATE employees
SET status = 'inactive'
WHERE ctid IN (
SELECT ctid
FROM employees
WHERE department_id = 10
ORDER BY hire_date ASC
LIMIT 100
);
トランザクションでの安全な更新
件数を指定してUPDATEする際は、トランザクションを活用して安全に更新しましょう。
更新前に確認 → 更新 → 検証 → COMMIT の流れ
SQL(安全な更新手順)
-- ステップ1: 更新対象を事前に確認
SELECT COUNT(*) AS target_count
FROM employees
WHERE status = 'pending'
AND ROWNUM <= 500;
-- ステップ2: 更新を実行
UPDATE employees
SET status = 'active',
updated_at = SYSDATE
WHERE status = 'pending'
AND ROWNUM <= 500;
-- ステップ3: 更新件数を確認
-- SQL%ROWCOUNT または SQL*Plusの出力で確認
-- ステップ4: 問題なければコミット、問題があればロールバック
COMMIT; -- または ROLLBACK;
SAVEPOINT を使った段階的な更新
SQL
-- SAVEPOINTで段階的に更新
SAVEPOINT before_update;
UPDATE employees
SET salary = salary * 1.05
WHERE employee_id IN (
SELECT employee_id
FROM (
SELECT employee_id
FROM employees
WHERE department_id = 10
ORDER BY performance_score DESC
)
WHERE ROWNUM <= 20
);
-- 結果を確認して問題があれば部分ロールバック
-- ROLLBACK TO before_update;
-- 問題なければコミット
COMMIT;
よくあるエラーと対処法
ORA-00933: SQL command not properly ended
UPDATE文に LIMIT や ORDER BY を直接使おうとした場合に発生します。
エラー例と対処
-- NG: UPDATE文にLIMITは使えない
UPDATE employees SET status = 'X' LIMIT 100;
-- ORA-00933: SQL command not properly ended
-- OK: ROWNUMで件数を制限
UPDATE employees SET status = 'X' WHERE ROWNUM <= 100;
ORA-01427: single-row subquery returns more than one row
サブクエリが複数行を返している場合に発生します。= ではなく IN を使ってください。
エラー例と対処
-- NG: = は単一行しか受け取れない
UPDATE employees
SET bonus = 5000
WHERE employee_id = (
SELECT employee_id FROM employees WHERE ROWNUM <= 10
);
-- ORA-01427: single-row subquery returns more than one row
-- OK: IN を使う
UPDATE employees
SET bonus = 5000
WHERE employee_id IN (
SELECT employee_id FROM employees WHERE ROWNUM <= 10
);
ROWNUM = N(N > 1)で結果が0件になる
エラー例と対処
-- NG: ROWNUM = 5 は結果が0件になる
UPDATE employees SET bonus = 5000 WHERE ROWNUM = 5;
-- 0行が更新されました(意図と異なる)
-- OK: ROW_NUMBER() を使って特定の行を指定
UPDATE employees
SET bonus = 5000
WHERE employee_id IN (
SELECT employee_id
FROM (
SELECT employee_id,
ROW_NUMBER() OVER (ORDER BY employee_id) AS rn
FROM employees
)
WHERE rn = 5
);
実務パターン集
パターン1: テストデータの一部だけ更新
SQL
-- テスト用に先頭10件だけステータスを変更
UPDATE test_orders
SET order_status = 'SHIPPED',
ship_date = SYSDATE
WHERE order_status = 'PENDING'
AND ROWNUM <= 10;
パターン2: 古いデータをアーカイブ用にマーク
SQL
-- 作成日が古い順に1000件をアーカイブ対象に
UPDATE log_table
SET archive_flag = 'Y'
WHERE log_id IN (
SELECT log_id
FROM (
SELECT log_id
FROM log_table
WHERE archive_flag = 'N'
ORDER BY created_at ASC
)
WHERE ROWNUM <= 1000
);
パターン3: 優先度の高い順にN件処理
SQL
-- 優先度が高い順に50件を処理中に変更
UPDATE task_queue
SET status = 'PROCESSING',
started_at = SYSDATE
WHERE task_id IN (
SELECT task_id
FROM (
SELECT task_id,
ROW_NUMBER() OVER (
ORDER BY priority DESC, created_at ASC
) AS rn
FROM task_queue
WHERE status = 'WAITING'
)
WHERE rn <= 50
);
パターン4: データ移行の段階的実行
PL/SQL
-- データ移行: 新カラムに値を設定(段階的実行)
DECLARE
v_count NUMBER;
BEGIN
LOOP
UPDATE customers
SET full_name = last_name || ' ' || first_name
WHERE full_name IS NULL
AND ROWNUM <= 5000;
v_count := SQL%ROWCOUNT;
COMMIT;
EXIT WHEN v_count = 0;
END LOOP;
END;
/
方法の選び方チャート
用途に応じて最適な方法を選びましょう。
| やりたいこと |
おすすめの方法 |
| 単純に先頭N件を更新 |
WHERE ROWNUM <= N |
| ソート後の上位N件を更新 |
ROWNUM + サブクエリ or ROW_NUMBER() |
| N件目〜M件目の範囲指定 |
ROW_NUMBER() + BETWEEN |
| グループごとに上位N件 |
ROW_NUMBER() + PARTITION BY |
| 12c以降でシンプルに書きたい |
FETCH FIRST N ROWS ONLY |
| 大量データの分割更新 |
PL/SQL バッチ更新 |
まとめ
この記事のまとめ
- OracleにはMySQLのような
UPDATE ... LIMIT 構文がない
- 最もシンプルな方法は
WHERE ROWNUM <= N
- ソート後のN件を更新するにはサブクエリでORDER BYを先に評価する
ROW_NUMBER() OVER() は範囲指定やPARTITION BYに対応する最も柔軟な方法
- Oracle 12c以降なら
FETCH FIRST N ROWS ONLY が直感的
- 大量データはバッチ更新(PL/SQLのLOOP + COMMIT)で分割処理する
- 本番更新ではSAVEPOINTを活用し、確認してからCOMMITする