【Oracle】指定した件数分のデータを取得する方法完全ガイド|ROWNUM内部動作・FETCH FIRST WITH TIES・SAMPLE・ページネーション設計まで解説

Oracle でテーブルから「上位 N 件だけ取得したい」「ページングで 10 件ずつ表示したい」「ランダムに 100 件サンプリングしたい」――こうした要求は日常的に発生します。

Oracle には MySQL の LIMIT や SQL Server の TOP に相当する直感的な構文がなく、バージョンや目的によって方法を使い分ける必要があります。本記事では、ROWNUM の内部動作メカニズム(なぜ > N が効かないのか)、FETCH FIRST の WITH TIES / PERCENT 構文、SAMPLE 句によるランダムサンプリング、そしてOFFSET ページネーションの性能問題とキーセットページネーションまで深く掘り下げて解説します。

この記事でわかること
・ROWNUM の評価順序と内部動作(なぜ ROWNUM > 5 は行を返さないのか)
・ROWNUM + ORDER BY の罠と正しい書き方
・FETCH FIRST N ROWS ONLY / WITH TIES / PERCENT の使い方(12c 以降)
・SAMPLE 句でテーブルからランダムにデータを取得する方法
・OFFSET ページネーションの性能劣化とキーセットページネーションの設計
・実行計画で見る各方式のパフォーマンス特性
・ROWNUM / FETCH FIRST / ROW_NUMBER() の使い分け早見表
スポンサーリンク

ROWNUM の基本と内部動作メカニズム

基本的な使い方

SQL(ROWNUM で上位 N 件を取得)
-- 先頭 10 件を取得
SELECT * FROM employees WHERE ROWNUM <= 10;

-- 件数に 0 を指定: 行は返らないがテーブル構造を確認できる
SELECT * FROM employees WHERE ROWNUM < 1;

ROWNUM の評価順序

ROWNUM を正しく使うには、Oracle がどの順番で処理を行うかを理解する必要があります。

処理順 段階 ROWNUM との関係
1 FROM(テーブルアクセス) 行を読み込む
2 WHERE(フィルタ) ROWNUM はここで評価される。条件に合う行に 1 から番号が付く
3 GROUP BY / HAVING グループ化(ROWNUM 後)
4 SELECT(列選択) 出力列を決定
5 ORDER BY(並べ替え) ROWNUM の後に実行される(重要)
ROWNUM > N が行を返さない理由
ROWNUM は「条件に合致した行に順番に付与される」仕組みです。最初の行が読まれたとき ROWNUM=1 ですが、WHERE ROWNUM > 5 は偽なのでこの行は棄却されます。棄却されたので次の行も ROWNUM=1 として評価され、また偽→棄却……を永久に繰り返し、結果は常に 0 件になります。
ROWNUM > NROWNUM = N(N > 1)は意味のない条件です。「N+1 件目以降を取得する」にはサブクエリまたは OFFSET を使います。

ROWNUM + ORDER BY の罠

SQL(NG: ORDER BY の前に ROWNUM が評価される)
-- NG: 給与上位 5 名を取りたいのに、ランダムな 5 件が返る
SELECT * FROM employees
WHERE ROWNUM <= 5
ORDER BY salary DESC;
-- ROWNUM がまず 5 件を確定 → その後に salary で並べ替え
-- → 全体の上位 5 名ではなく「先に取れた 5 件を並べ替えただけ」
SQL(OK: サブクエリで先に ORDER BY してから ROWNUM)
-- OK: サブクエリで並べ替え → 外側で ROWNUM で制限
SELECT * FROM (
    SELECT * FROM employees ORDER BY salary DESC
)
WHERE ROWNUM <= 5;
-- 全体を salary 降順に並べてから上位 5 件を取得
実行計画で確認: COUNT STOPKEY 最適化
Oracle は WHERE ROWNUM <= N を検出すると、実行計画に COUNT STOPKEY を挿入します。これは N 件に達した時点でスキャンを停止する最適化で、テーブル全体をスキャンしません。そのため ROWNUM による件数制限は非常に高速です。

FETCH FIRST(Oracle 12c 以降)

Oracle 12c から導入された FETCH FIRST 句は、SQL 標準に準拠した直感的な構文で件数を制限できます。

SQL(FETCH FIRST 基本)
-- 給与上位 5 名を取得(ORDER BY + FETCH)
SELECT employee_id, last_name, salary
FROM employees
ORDER BY salary DESC
FETCH FIRST 5 ROWS ONLY;

-- ROWNUM 方式より ORDER BY の罠がなくシンプル

WITH TIES:同値の行も含める

WITH TIES を指定すると、N 件目と同じ値を持つ行も結果に含まれます。ランキングで同率順位を切り捨てたくない場合に有用です。

SQL(WITH TIES の動作例)
-- 給与上位 3 名(同率は含める)
SELECT employee_id, last_name, salary
FROM employees
ORDER BY salary DESC
FETCH FIRST 3 ROWS WITH TIES;

-- 例: 3位が salary=7000 で、同じ 7000 の社員が他に 2 名いる場合
-- → 3 + 2 = 5 件が返る(同率 3 位を全員含む)

-- ROWS ONLY の場合: ちょうど 3 件(同率は切り捨て)
-- WITH TIES の場合: 3 件 + 同率分(3 件以上になる可能性がある)

PERCENT:割合で件数を指定する

SQL(PERCENT で全体の N% を取得)
-- 全体の上位 10% を取得
SELECT employee_id, last_name, salary
FROM employees
ORDER BY salary DESC
FETCH FIRST 10 PERCENT ROWS ONLY;

-- 107 件のテーブルなら CEIL(107 * 0.10) = 11 件が返る
-- 小数点以下は切り上げ

-- WITH TIES と組み合わせ可能
SELECT employee_id, last_name, salary
FROM employees
ORDER BY salary DESC
FETCH FIRST 10 PERCENT ROWS WITH TIES;

OFFSET + FETCH(ページネーション)

SQL(OFFSET で N 件スキップして取得)
-- 11 件目〜20 件目を取得(2 ページ目: 1 ページ 10 件)
SELECT employee_id, last_name, salary
FROM employees
ORDER BY employee_id
OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;

-- N ページ目の取得式
-- OFFSET (:page_num - 1) * :page_size ROWS
-- FETCH NEXT :page_size ROWS ONLY
構文 動作 備考
FETCH FIRST N ROWS ONLY 先頭 N 件を取得 最も基本的な形
FETCH FIRST N ROWS WITH TIES 先頭 N 件 + 同値の行 ORDER BY 必須
FETCH FIRST N PERCENT ROWS ONLY 全体の N% を取得 件数は切り上げ
OFFSET M ROWS FETCH NEXT N ROWS ONLY M 件スキップして N 件取得 ページネーション用

SAMPLE 句:ランダムにデータを取得する

テストデータの確認や統計的なサンプリングでは、「ランダムに N 件(または N%)取得したい」場合があります。Oracle の SAMPLE 句は、テーブルからランダムにデータを抽出する専用構文です。

SQL(SAMPLE 句の基本)
-- テーブルの約 10% をランダムに取得
SELECT * FROM employees SAMPLE (10);
-- 引数はパーセント(0.000001 〜 100)
-- 実行ごとに取得される行は変わる(ランダム)

-- SAMPLE BLOCK: ブロック単位でサンプリング(大量データ向け)
SELECT * FROM employees SAMPLE BLOCK (10);
-- 行単位ではなくブロック(データページ)単位で抽出するため高速
-- ただし偏りが出やすい

-- SEED: 再現性のあるサンプリング
SELECT * FROM employees SAMPLE (10) SEED (42);
-- 同じ SEED を指定すれば同じ結果が返る(テスト用途に便利)
SQL(ランダムに N 件を取得する方法)
-- 方法①: SAMPLE + ROWNUM(近似的)
SELECT * FROM employees SAMPLE (20)
WHERE ROWNUM <= 10;
-- 20% をサンプリングしてから 10 件に制限(高速だが正確に N 件にならないことがある)

-- 方法②: DBMS_RANDOM.VALUE で ORDER BY(正確に N 件)
SELECT * FROM (
    SELECT * FROM employees ORDER BY DBMS_RANDOM.VALUE
)
WHERE ROWNUM <= 10;
-- テーブル全体をスキャン+ソートするため大量データでは遅い

-- 方法③: FETCH FIRST + DBMS_RANDOM(12c 以降)
SELECT * FROM employees
ORDER BY DBMS_RANDOM.VALUE
FETCH FIRST 10 ROWS ONLY;
方法 正確に N 件? パフォーマンス 用途
SAMPLE (N%) いいえ(N% の近似値) ◎ 最速(ブロックレベル) 統計的サンプリング・大量データの概算調査
SAMPLE + ROWNUM ほぼ N 件 ◎ 速い 「だいたい N 件」で十分な場合
ORDER BY DBMS_RANDOM.VALUE + ROWNUM はい(正確に N 件) △ 遅い(全件ソート) 少量テーブルでの正確なランダム取得
SAMPLE は WHERE より先に評価される
SELECT * FROM employees SAMPLE(10) WHERE dept_id = 20 は、まずテーブルの 10% をサンプリングしてから dept_id=20 でフィルタします。「dept_id=20 の行の中からランダム 10%」ではないので注意してください。条件付きサンプリングには ORDER BY DBMS_RANDOM.VALUE 方式を使います。

ページネーション設計:OFFSET の性能問題と対策

OFFSET 方式の問題点

OFFSET M ROWS はページ番号が大きくなるほど性能が劣化します。OFFSET 10000 を指定すると、Oracle は内部的に 10000 件をスキャン・ソートしてから捨てる必要があるためです。

SQL(OFFSET が遅くなる例)
-- 1 ページ目: 高速(OFFSET 0)
SELECT * FROM orders ORDER BY order_id OFFSET 0 ROWS FETCH NEXT 20 ROWS ONLY;

-- 100 ページ目: 遅い(OFFSET 1980 = 99 × 20)
SELECT * FROM orders ORDER BY order_id OFFSET 1980 ROWS FETCH NEXT 20 ROWS ONLY;
-- → 内部的に 2000 件をソートしてから先頭 1980 件を捨てている

-- 10000 ページ目: 非常に遅い(OFFSET 199980)
SELECT * FROM orders ORDER BY order_id OFFSET 199980 ROWS FETCH NEXT 20 ROWS ONLY;
-- → 200000 件をソートして 199980 件を捨てる

キーセットページネーション(Seek Method)

OFFSET の代わりに「前のページの最後のキー値」を条件に使う方式です。OFFSET のようにページが深くなっても性能が劣化しません。

SQL(キーセットページネーション)
-- 1 ページ目(最初の 20 件)
SELECT * FROM orders
WHERE order_id > 0     -- 初回は最小値
ORDER BY order_id
FETCH FIRST 20 ROWS ONLY;
-- → 最後の order_id が 1020 だったとする

-- 2 ページ目(order_id > 1020 から 20 件)
SELECT * FROM orders
WHERE order_id > 1020  -- 前ページの最後のキー
ORDER BY order_id
FETCH FIRST 20 ROWS ONLY;
-- → インデックスレンジスキャンで高速(ページ番号に依存しない)

-- 3 ページ目(order_id > 最後のキー から 20 件)
SELECT * FROM orders
WHERE order_id > :last_order_id
ORDER BY order_id
FETCH FIRST 20 ROWS ONLY;
項目 OFFSET 方式 キーセット方式
パフォーマンス ページ番号に比例して劣化 ページ番号に依存しない(常に高速)
ページジャンプ 任意のページに直接移動可能 不可(順方向にのみ移動)
データ挿入・削除の影響 ページがずれて同じ行が重複表示される可能性 キーベースなので行の重複・欠落なし
実装の複雑さ シンプル やや複雑(前ページのキーを保持する必要がある)
適するケース 管理画面のページネーション(深いページ不要) API のカーソルページネーション・無限スクロール
API 設計では キーセット方式を推奨
REST API で大量データのページネーションを提供する場合、OFFSET 方式は深いページで性能劣化するだけでなく、データの挿入・削除でページがずれる問題もあります。キーセット方式(?after=last_id パラメータ)を採用すれば両方の問題が解消されます。

ROWNUM / FETCH FIRST / ROW_NUMBER() の使い分け

項目 ROWNUM FETCH FIRST (12c+) ROW_NUMBER()
対応バージョン 全バージョン 12c 以降 全バージョン(8i+)
ORDER BY との連携 ×(サブクエリ必須) ○(自然に連携) ○(OVER 句で指定)
WITH TIES(同値含む) × × → RANK() で代替
PERCENT(割合指定) × ×
OFFSET(ページネーション) ×(ROWNUM BETWEEN で代替可) ○(WHERE rn BETWEEN)
グループ内 Top N × × ○(PARTITION BY)
実行計画の最適化 ◎ COUNT STOPKEY ◎ WINDOW SORT PUSHED RANK ○ WINDOW SORT
可読性 △(サブクエリのネストが必要) ◎(直感的)
使い分けの判断基準
・Oracle 12c 以降 → FETCH FIRST を第一選択(シンプル・高速・同値対応)
・Oracle 11g 以前 → ROWNUM(ただし ORDER BY 時はサブクエリ必須)
・グループ内 Top N → ROW_NUMBER() OVER (PARTITION BY …)
・ランダムサンプリング → SAMPLE 句 または ORDER BY DBMS_RANDOM.VALUE
基本的な件数取得や順位付けの詳細は「データを取得する件数を絞る方法」「RANK / ROW_NUMBER 完全ガイド」も参照してください。

実行計画で見る各方式のパフォーマンス

SQL(各方式の実行計画を確認する方法)
-- ROWNUM 方式の実行計画
EXPLAIN PLAN FOR
SELECT * FROM (
    SELECT * FROM employees ORDER BY salary DESC
) WHERE ROWNUM <= 5;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
-- → SORT ORDER BY STOPKEY(N 件で停止する最適化)

-- FETCH FIRST 方式の実行計画
EXPLAIN PLAN FOR
SELECT * FROM employees ORDER BY salary DESC
FETCH FIRST 5 ROWS ONLY;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
-- → WINDOW SORT PUSHED RANK(ROW_NUMBER を使ったランク最適化)
実行計画の操作 出現する方式 意味
COUNT STOPKEY ROWNUM N 件に達した時点でスキャンを停止(最速パターン)
SORT ORDER BY STOPKEY ROWNUM + ORDER BY ソートしつつ N 件で停止
WINDOW SORT PUSHED RANK FETCH FIRST / ROW_NUMBER ウィンドウソートで上位 N 件を効率取得
VIEW(インラインビュー) ROWNUM サブクエリ サブクエリが展開されている

よくある質問

QROWNUM = 5 で 5 行目だけ取得できますか?
Aできません。WHERE ROWNUM = 5 は常に 0 件を返します。ROWNUM は条件を満たした行に順番に 1 から付与されるため、最初の行が ROWNUM=1 となり = 5 は偽 → 棄却 → 次の行もまた ROWNUM=1 になり永遠に条件を満たしません。特定の行番号で取得するには ROW_NUMBER() OVER (...) をサブクエリで使います。
QFETCH FIRST と ROWNUM はどちらが速いですか?
A実行計画を比較すると、両者はほぼ同等のパフォーマンスです。ROWNUM は COUNT STOPKEY、FETCH FIRST は WINDOW SORT PUSHED RANK で最適化されます。ただし FETCH FIRST は ORDER BY との連携が自然で、サブクエリのネストが不要な分コードがシンプルです。12c 以降であれば FETCH FIRST を推奨します。
QSAMPLE (10) で正確に 10% の行が返りますか?
A正確に 10% とは限りません。SAMPLE はブロックレベルのランダム抽出を行うため、実際の件数はばらつきます(テーブルの物理配置に依存)。正確に N 件取得したい場合は SAMPLE の後に ROWNUM <= N を付けるか、ORDER BY DBMS_RANDOM.VALUE 方式を使います。
QOFFSET が大きいとなぜ遅くなるのですか?
AOracle は OFFSET M ROWS を処理するために、内部的に先頭から M + N 件をソート・取得してから最初の M 件を捨てます。つまり OFFSET 10000 FETCH NEXT 20 なら 10020 件分の作業が発生します。深いページほどスキャン量が増えるため、性能が O(M) で劣化します。キーセットページネーション(WHERE key > :last_key)ならインデックスレンジスキャンで O(1) です。
QWITH TIES を使うと件数が N 件を超えることがありますか?
Aはい。N 件目と同じ ORDER BY の値を持つ行がすべて含まれるため、N 件を超える結果が返ることがあります。「最大 N 件を厳密に守りたい」場合は ROWS ONLY を使ってください。「同値を切り捨てたくない」場合は WITH TIES を使います。
QDELETE や UPDATE でも FETCH FIRST は使えますか?
AOracle では DELETE / UPDATE 文で FETCH FIRST を直接使うことはできません。WHERE ROWID IN (SELECT ROWID FROM ... FETCH FIRST N ROWS ONLY) のようにサブクエリ経由で行を特定します。件数を指定した UPDATE の詳細は「更新するデータの件数を指定する方法」を参照してください。

まとめ

Oracle で指定件数のデータを取得する方法の要点をまとめます。

やりたいこと 推奨方式
上位 N 件を取得(12c 以降) FETCH FIRST N ROWS ONLY
上位 N 件を取得(11g 以前) SELECT * FROM (SELECT … ORDER BY …) WHERE ROWNUM <= N
上位 N 件 + 同率の行も含める FETCH FIRST N ROWS WITH TIES
全体の N% を取得 FETCH FIRST N PERCENT ROWS ONLY
M+1 件目〜M+N 件目を取得 OFFSET M ROWS FETCH NEXT N ROWS ONLY
ランダムに N% サンプリング SELECT * FROM table SAMPLE (N)
ランダムに正確に N 件取得 ORDER BY DBMS_RANDOM.VALUE FETCH FIRST N ROWS ONLY
グループ内 Top N ROW_NUMBER() OVER (PARTITION BY … ORDER BY …) <= N
大量データのページネーション(API) キーセットページネーション(WHERE key > :last_key)

ROWNUM・FETCH FIRST・ROW_NUMBER の基本比較は「データを取得する件数を絞る方法」、OFFSET/FETCH の基本は「OFFSETとFETCHで行数を限定し取得する方法」、SQL 全般の LIMIT 構文は「SQL LIMIT句の使い方」も併せて参照してください。