Oracle 12c 以降で導入された OFFSET / FETCH 句は、SQL 標準に準拠したページネーション構文です。従来の ROWNUM サブクエリ方式に比べて直感的で簡潔に書けるため、現在の Oracle 開発では標準的な方法として定着しています。
本記事では、OFFSET / FETCH の構文バリエーション(ROW/ROWS、FIRST/NEXT の違い)、バインド変数を使った動的ページネーション、ROWNUM 方式からの書き換え、ORDER BY の不定順による落とし穴、そして管理画面・API・バッチ分割の実務パターンまで解説します。
・OFFSET / FETCH の基本構文と各キーワードの意味
・ROW と ROWS / FIRST と NEXT の違い(省略ルール)
・OFFSET なしの FETCH FIRST(先頭 N 件取得)
・WITH TIES / PERCENT との組み合わせ
・バインド変数を使った動的ページネーション
・ROWNUM 方式からの書き換え対応表
・ORDER BY が不定の場合の落とし穴
・管理画面ページネーション・API カーソル・バッチ分割の実務パターン
OFFSET / FETCH の基本構文
SELECT columns
FROM table_name
[WHERE conditions]
ORDER BY sort_column
OFFSET m {ROW | ROWS}
FETCH {FIRST | NEXT} n {ROW | ROWS} {ONLY | WITH TIES};
-- 給与の高い順で 11〜20 件目を取得(2 ページ目) SELECT employee_id, last_name, salary FROM employees ORDER BY salary DESC OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;
| キーワード | 意味 | 例 |
|---|---|---|
| OFFSET m ROWS | 先頭から m 行をスキップ | OFFSET 10 ROWS(先頭 10 行を飛ばす) |
| FETCH NEXT n ROWS ONLY | OFFSET の後ろから n 行を取得 | FETCH NEXT 10 ROWS ONLY(10 行取得) |
| FETCH FIRST n ROWS ONLY | 先頭から n 行を取得(OFFSET なし時) | FETCH FIRST 5 ROWS ONLY |
| WITH TIES | n 件目と同じ ORDER BY 値の行も含める | 同率順位を切り捨てない |
| n PERCENT | 全体の n% を取得 | FETCH FIRST 10 PERCENT ROWS ONLY |
ROW / ROWS・FIRST / NEXT の違い
Oracle は構文の柔軟性のために、いくつかのキーワードを同義語として扱います。機能的な違いはありません。
| 書き方 A | 書き方 B | 違い |
|---|---|---|
| OFFSET 1 ROW | OFFSET 1 ROWS | 同じ(単数/複数の違いだけ) |
| FETCH FIRST 10 ROWS | FETCH NEXT 10 ROWS | 同じ(FIRST も NEXT も同義) |
| FETCH NEXT 1 ROW ONLY | FETCH NEXT 1 ROWS ONLY | 同じ |
・OFFSET のみ:
OFFSET 10 ROWS(複数形)・先頭 N 件:
FETCH FIRST N ROWS ONLY(FIRST が自然)・OFFSET + FETCH:
OFFSET M ROWS FETCH NEXT N ROWS ONLY(NEXT が自然)チーム内で統一すれば十分です。
構文パターン一覧
-- (1) 先頭 5 件だけ取得(OFFSET なし) SELECT * FROM employees ORDER BY salary DESC FETCH FIRST 5 ROWS ONLY; -- (2) 先頭 5 件 + 同率を含める SELECT * FROM employees ORDER BY salary DESC FETCH FIRST 5 ROWS WITH TIES; -- (3) 全体の 10% を取得 SELECT * FROM employees ORDER BY salary DESC FETCH FIRST 10 PERCENT ROWS ONLY; -- (4) 11〜20 件目を取得(ページネーション) SELECT * FROM employees ORDER BY employee_id OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY; -- (5) OFFSET のみ(FETCH なし: OFFSET 以降のすべての行) SELECT * FROM employees ORDER BY employee_id OFFSET 10 ROWS; -- 11 件目以降をすべて取得
ROWNUM 方式からの書き換え
Oracle 11g 以前で使われていた ROWNUM サブクエリ方式を、OFFSET / FETCH に書き換える対応表です。
| やりたいこと | ROWNUM 方式(11g 以前) | OFFSET / FETCH 方式(12c 以降) |
|---|---|---|
| 先頭 10 件を取得 | SELECT * FROM ( SELECT * FROM emp ORDER BY sal DESC ) WHERE ROWNUM <= 10 |
SELECT * FROM emp ORDER BY sal DESC FETCH FIRST 10 ROWS ONLY |
| 11〜20 件目を取得 | SELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT * FROM emp ORDER BY sal DESC ) t WHERE ROWNUM <= 20 ) WHERE rn >= 11 |
SELECT * FROM emp ORDER BY sal DESC OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY |
| 全体の上位 10% | (計算が複雑) | FETCH FIRST 10 PERCENT ROWS ONLY |
| 同率を含めた上位 5 件 | RANK() OVER(…) をサブクエリで使用 | FETCH FIRST 5 ROWS WITH TIES |
・サブクエリのネストが不要でコードがシンプル
・ORDER BY との連携が自然(ROWNUM の評価順序の罠がない)
・WITH TIES / PERCENT が 1 行で書ける
・SQL 標準準拠で他の RDBMS への移植性が高い
バインド変数を使った動的ページネーション
-- :page_num = ページ番号(1 始まり) -- :page_size = 1 ページあたりの件数 SELECT employee_id, last_name, salary FROM employees ORDER BY employee_id OFFSET (:page_num - 1) * :page_size ROWS FETCH NEXT :page_size ROWS ONLY; -- 例: page_num=3, page_size=20 → OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY -- → 41〜60 件目を取得
DECLARE
v_page_num NUMBER := 3;
v_page_size NUMBER := 20;
v_offset NUMBER;
TYPE t_emp IS TABLE OF employees%ROWTYPE;
v_emps t_emp;
BEGIN
v_offset := (v_page_num - 1) * v_page_size;
SELECT * BULK COLLECT INTO v_emps
FROM employees
ORDER BY employee_id
OFFSET v_offset ROWS FETCH NEXT v_page_size ROWS ONLY;
FOR i IN 1..v_emps.COUNT LOOP
DBMS_OUTPUT.PUT_LINE(v_emps(i).employee_id || ': ' || v_emps(i).last_name);
END LOOP;
END;
/
総件数と総ページ数の取得
ページネーション UI では「全 157 件中 41〜60 件を表示」のように総件数が必要です。OFFSET / FETCH と COUNT を組み合わせるパターンを紹介します。
-- 方法(1): 別クエリで COUNT(シンプル)
SELECT COUNT(*) AS total_count FROM employees WHERE status = 'ACTIVE';
-- 総ページ数: CEIL(total_count / page_size)
SELECT * FROM employees WHERE status = 'ACTIVE'
ORDER BY employee_id
OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY;
-- 方法(2): ウィンドウ関数で 1 クエリに統合
SELECT employee_id, last_name, salary,
COUNT(*) OVER () AS total_count
FROM employees
WHERE status = 'ACTIVE'
ORDER BY employee_id
OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY;
-- 各行に total_count が付与される(全行をスキャンするためやや重い)
ORDER BY の不定順による落とし穴
ORDER BY salary DESC で salary が同じ行が複数ある場合、その中での順序は実行ごとに変わる可能性があります。ページを切り替えたときに同じ行が重複したり、行が抜けたりすることがあります。-- NG: salary が同じ行の順序が不定 → ページ切り替えで行が重複/欠落する SELECT * FROM employees ORDER BY salary DESC OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY; -- OK: 一意な列(employee_id)を ORDER BY の末尾に追加 SELECT * FROM employees ORDER BY salary DESC, employee_id ASC OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY; -- salary が同じ行は employee_id 順で安定する → ページが安定
ページネーションの ORDER BY には、ソート列に加えて主キーなどの一意列を末尾に追加してください。これにより行の順序が確定し、ページ切り替え時の重複・欠落を防げます。
実行計画の確認
EXPLAIN PLAN FOR SELECT * FROM employees ORDER BY salary DESC, employee_id OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY); -- 典型的な実行計画: -- | Id | Operation | Name | -- |----|--------------------------|-----------|| -- | 0 | SELECT STATEMENT | | -- | 1 | VIEW | | -- | 2 | WINDOW SORT PUSHED RANK| | -- | 3 | TABLE ACCESS FULL | EMPLOYEES | -- WINDOW SORT PUSHED RANK: 上位 N 件を効率的に取得する最適化
Oracle はOFFSET / FETCH を内部的に ROW_NUMBER() ウィンドウ関数に変換して実行します。そのためROWNUM 方式と同等のパフォーマンスが得られます。
パフォーマンスの注意点
| 注意点 | 説明 | 対処法 |
|---|---|---|
| OFFSET が大きいと遅くなる | OFFSET 10000 は内部で 10020 件をソートしてから 10000 件を捨てる | キーセットページネーション(WHERE key > :last_key)を検討 |
| ORDER BY 列にインデックスがない | テーブルフルスキャン + ソートが発生する | ORDER BY に使う列にインデックスを作成 |
| COUNT(*) OVER() との併用 | 総件数取得のために全行スキャンが発生する | 総件数は別クエリで取得するか、キャッシュする |
OFFSET の性能劣化問題とキーセットページネーションの詳細は「指定した件数分のデータを取得する方法」を参照してください。
実務パターン集
パターン(1): 管理画面のページネーション
-- 画面入力: page=3, size=20, sort=salary DESC -- 検索条件: department_id = 10 SELECT employee_id, last_name, salary, hire_date FROM employees WHERE department_id = 10 ORDER BY salary DESC, employee_id OFFSET (3 - 1) * 20 ROWS FETCH NEXT 20 ROWS ONLY; -- → OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY -- → 41〜60 件目を取得
パターン(2): REST API のページネーションレスポンス
-- データ取得
SELECT employee_id, last_name, salary,
COUNT(*) OVER () AS total_count
FROM employees
WHERE department_id = :dept_id
ORDER BY employee_id
OFFSET :offset ROWS FETCH NEXT :limit ROWS ONLY;
-- API レスポンスのイメージ:
-- { "data": [...], "total": 157, "page": 3, "size": 20, "pages": 8 }
パターン(3): バッチ処理の分割実行
-- PL/SQL で 1000 件ずつバッチ処理
DECLARE
v_offset NUMBER := 0;
v_batch NUMBER := 1000;
v_count NUMBER;
BEGIN
SELECT COUNT(*) INTO v_count FROM target_table WHERE status = 'PENDING';
WHILE v_offset < v_count LOOP
UPDATE target_table
SET status = 'PROCESSED', updated_at = SYSDATE
WHERE ROWID IN (
SELECT ROWID FROM target_table
WHERE status = 'PENDING'
ORDER BY created_at
OFFSET v_offset ROWS FETCH NEXT v_batch ROWS ONLY
);
COMMIT;
v_offset := v_offset + v_batch;
END LOOP;
END;
/
パターン(4): N 件目だけを取得
-- 給与ランキング 5 位の社員だけを取得 SELECT * FROM employees ORDER BY salary DESC, employee_id OFFSET 4 ROWS FETCH NEXT 1 ROW ONLY; -- OFFSET 4 = 先頭 4 件スキップ → 5 件目の 1 行を取得
他 RDBMS との構文比較
| RDBMS | 先頭 N 件 | M+1 〜 M+N 件目 |
|---|---|---|
| Oracle 12c+ | FETCH FIRST N ROWS ONLY | OFFSET M ROWS FETCH NEXT N ROWS ONLY |
| MySQL | LIMIT N | LIMIT N OFFSET M |
| PostgreSQL | LIMIT N / FETCH FIRST N ROWS ONLY | LIMIT N OFFSET M / OFFSET M FETCH NEXT N ROWS ONLY |
| SQL Server | TOP N | OFFSET M ROWS FETCH NEXT N ROWS ONLY(2012+) |
| Oracle 11g 以前 | WHERE ROWNUM <= N | ROWNUM サブクエリ(ネスト必要) |
よくある質問
FETCH FIRST N ROWS ONLY だけで先頭 N 件を取得できます。OFFSET のデフォルトは 0(スキップなし)です。「上位 N 件だけ取得」したい場合は OFFSET を省略してください。ROWS ONLY を使い、「同値を切り捨てたくない」場合は WITH TIES を使います。UPDATE table SET ... WHERE ROWID IN (
SELECT ROWID FROM table ORDER BY col OFFSET M ROWS FETCH NEXT N ROWS ONLY
)まとめ
OFFSET / FETCH 句の要点をまとめます。
| やりたいこと | 構文 |
|---|---|
| 先頭 N 件を取得 | ORDER BY col FETCH FIRST N ROWS ONLY |
| 先頭 N 件 + 同率を含める | ORDER BY col FETCH FIRST N ROWS WITH TIES |
| 全体の N% を取得 | ORDER BY col FETCH FIRST N PERCENT ROWS ONLY |
| M+1 〜 M+N 件目を取得 | ORDER BY col OFFSET M ROWS FETCH NEXT N ROWS ONLY |
| 動的ページネーション | OFFSET (:page-1)*:size ROWS FETCH NEXT :size ROWS ONLY |
| N 件目だけ取得 | OFFSET N-1 ROWS FETCH NEXT 1 ROW ONLY |
| ORDER BY 不定順を防ぐ | ORDER BY sort_col, primary_key(一意列を末尾に追加) |
ROWNUM の内部動作や SAMPLE 句、キーセットページネーションの詳細は「指定した件数分のデータを取得する方法」、ROWNUM / FETCH FIRST / ROW_NUMBER の基本比較は「データを取得する件数を絞る方法」も併せて参照してください。
