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 の基本と内部動作メカニズム
基本的な使い方
-- 先頭 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 は「条件に合致した行に順番に付与される」仕組みです。最初の行が読まれたとき ROWNUM=1 ですが、
WHERE ROWNUM > 5 は偽なのでこの行は棄却されます。棄却されたので次の行も ROWNUM=1 として評価され、また偽→棄却……を永久に繰り返し、結果は常に 0 件になります。ROWNUM > N や ROWNUM = N(N > 1)は意味のない条件です。「N+1 件目以降を取得する」にはサブクエリまたは OFFSET を使います。ROWNUM + ORDER BY の罠
-- NG: 給与上位 5 名を取りたいのに、ランダムな 5 件が返る SELECT * FROM employees WHERE ROWNUM <= 5 ORDER BY salary DESC; -- ROWNUM がまず 5 件を確定 → その後に salary で並べ替え -- → 全体の上位 5 名ではなく「先に取れた 5 件を並べ替えただけ」
-- OK: サブクエリで並べ替え → 外側で ROWNUM で制限
SELECT * FROM (
SELECT * FROM employees ORDER BY salary DESC
)
WHERE ROWNUM <= 5;
-- 全体を salary 降順に並べてから上位 5 件を取得
Oracle は
WHERE ROWNUM <= N を検出すると、実行計画に COUNT STOPKEY を挿入します。これは N 件に達した時点でスキャンを停止する最適化で、テーブル全体をスキャンしません。そのため ROWNUM による件数制限は非常に高速です。FETCH FIRST(Oracle 12c 以降)
Oracle 12c から導入された FETCH FIRST 句は、SQL 標準に準拠した直感的な構文で件数を制限できます。
-- 給与上位 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 件目と同じ値を持つ行も結果に含まれます。ランキングで同率順位を切り捨てたくない場合に有用です。
-- 給与上位 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:割合で件数を指定する
-- 全体の上位 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(ページネーション)
-- 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 句は、テーブルからランダムにデータを抽出する専用構文です。
-- テーブルの約 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 を指定すれば同じ結果が返る(テスト用途に便利)
-- 方法①: 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 件) | △ 遅い(全件ソート) | 少量テーブルでの正確なランダム取得 |
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 件をスキャン・ソートしてから捨てる必要があるためです。
-- 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 のようにページが深くなっても性能が劣化しません。
-- 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 のカーソルページネーション・無限スクロール |
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 完全ガイド」も参照してください。
実行計画で見る各方式のパフォーマンス
-- 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 サブクエリ | サブクエリが展開されている |
よくある質問
WHERE ROWNUM = 5 は常に 0 件を返します。ROWNUM は条件を満たした行に順番に 1 から付与されるため、最初の行が ROWNUM=1 となり = 5 は偽 → 棄却 → 次の行もまた ROWNUM=1 になり永遠に条件を満たしません。特定の行番号で取得するには ROW_NUMBER() OVER (...) をサブクエリで使います。ROWNUM <= N を付けるか、ORDER BY DBMS_RANDOM.VALUE 方式を使います。ROWS ONLY を使ってください。「同値を切り捨てたくない」場合は WITH TIES を使います。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句の使い方」も併せて参照してください。