【Oracle】OFFSET / FETCH でページネーションを実装する方法|構文・バインド変数・ROWNUM 書き換え・ORDER BY の注意点まで解説

【Oracle】OFFSET / FETCH でページネーションを実装する方法|構文・バインド変数・ROWNUM 書き換え・ORDER BY の注意点まで解説 Oracle

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 の基本構文

SQL(基本構文)
SELECT columns
FROM table_name
[WHERE conditions]
ORDER BY sort_column
OFFSET m {ROW | ROWS}
FETCH {FIRST | NEXT} n {ROW | ROWS} {ONLY | WITH TIES};
SQL(基本例: 11〜20 件目を取得)
-- 給与の高い順で 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 が自然)
チーム内で統一すれば十分です。

構文パターン一覧

SQL(全パターン)
-- (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
OFFSET / FETCH のメリット
・サブクエリのネストが不要でコードがシンプル
・ORDER BY との連携が自然(ROWNUM の評価順序の罠がない)
・WITH TIES / PERCENT が 1 行で書ける
・SQL 標準準拠で他の RDBMS への移植性が高い

バインド変数を使った動的ページネーション

SQL(バインド変数でページ番号を動的に指定)
-- :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 件目を取得
SQL(PL/SQL での動的ページネーション)
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 を組み合わせるパターンを紹介します。

SQL(総件数 + ページデータを同時取得)
-- 方法(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 の値が重複する行は順序が不定
ORDER BY salary DESC で salary が同じ行が複数ある場合、その中での順序は実行ごとに変わる可能性があります。ページを切り替えたときに同じ行が重複したり、行が抜けたりすることがあります。
SQL(ORDER BY の不定順を防ぐ)
-- 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 には一意な列を必ず含める
ページネーションの ORDER BY には、ソート列に加えて主キーなどの一意列を末尾に追加してください。これにより行の順序が確定し、ページ切り替え時の重複・欠落を防げます。

実行計画の確認

SQL(OFFSET / FETCH の実行計画)
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): 管理画面のページネーション

SQL(管理画面でのページネーション実装)
-- 画面入力: 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 のページネーションレスポンス

SQL(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): バッチ処理の分割実行

SQL(大量データを N 件ずつ処理)
-- 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 件目だけを取得

SQL(特定の 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 サブクエリ(ネスト必要)

よくある質問

QOFFSET / FETCH は Oracle のどのバージョンから使えますか?
AOracle 12c(12.1)以降で使用可能です。11g 以前では ROWNUM サブクエリを使う必要があります。12c 以降であれば、新規開発では OFFSET / FETCH を標準として使うことを推奨します。
QORDER BY は必須ですか?
AORDER BY を省略しても構文エラーにはなりませんが、結果の順序が不定になり、実行ごとに異なる行が返る可能性があります。ページネーションでは結果の一貫性が必須なので、ORDER BY は事実上必須です。
QOFFSET を省略して FETCH FIRST だけ使えますか?
Aはい。FETCH FIRST N ROWS ONLY だけで先頭 N 件を取得できます。OFFSET のデフォルトは 0(スキップなし)です。「上位 N 件だけ取得」したい場合は OFFSET を省略してください。
QOFFSET が大きいとなぜ遅くなりますか?
AOFFSET M は内部的に先頭から M + N 件をソートして取得し、最初の M 件を捨てます。M が大きいほどソート対象が増えるため、パフォーマンスが O(M) で劣化します。深いページが必要な場合はキーセットページネーション(WHERE key > :last_key)を検討してください。詳細は「件数指定データ取得完全ガイド」を参照。
QWITH TIES を使うと N 件を超える結果が返りますか?
Aはい。N 件目と同じ ORDER BY 値の行がすべて含まれるため、N 件を超えることがあります。「最大 N 件を厳密に守りたい」場合は ROWS ONLY を使い、「同値を切り捨てたくない」場合は WITH TIES を使います。
QOFFSET / FETCH を UPDATE や DELETE で使えますか?
AOracle では UPDATE / DELETE 文に OFFSET / FETCH を直接使うことはできません。サブクエリで ROWID を特定してから更新・削除します。
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 の基本比較は「データを取得する件数を絞る方法」も併せて参照してください。