Oracle ROWNUMでデータ取得!基本と注意点の詳細解説
Oracle Databaseを利用している方であれば、「ROWNUM
」というキーワードを耳にしたことがある、あるいは実際にクエリの中で見たことがあるでしょう。ROWNUM
は、クエリの結果セットの各行に連番を一時的に割り当てる擬似列です。特に、取得する行数を限定したい場合や、結果セットの一部を抜き出したい場合(ページングなど)によく利用されます。
しかし、ROWNUM
はその見た目とは裏腹に、直感とは異なる振る舞いをすることがあります。特に、特定の範囲の行を取得しようとしたり、並べ替えと組み合わせて使おうとしたりする際に、意図しない結果になることがあるため、その正確な挙動を理解しておくことが非常に重要です。
本記事では、Oracle ROWNUM
の基本的な使い方から、陥りやすい落とし穴、そして現代的なOracle Databaseで推奨される代替手段に至るまで、詳細かつ網羅的に解説します。約5000語を目標に、各項目を深く掘り下げていきますので、ROWNUM
に関する疑問や不安を解消し、より安全で効率的なデータ取得方法を習得するための一助となれば幸いです。
1. はじめに:ROWNUMとは何か
Oracle DatabaseにおけるROWNUM
は、クエリによって返される結果セットの各行に、動的に割り当てられる擬似列(Pseudo-column)です。擬似列とは、テーブルに実際に格納されている列ではないけれども、列のように扱うことができる特殊な属性のことです。
ROWNUM
の値は、クエリが処理され、条件を満たす行がフェッチされていく過程で、行に対して順番に割り当てられます。最初の行に「1」、次の行に「2」、その次に「3」…というように、1から始まる連番が付与されます。
ROWNUM
が最も一般的に利用される目的は、クエリの結果から最初のN件の行だけを取得するというケースです。これは、例えば検索結果のプレビュー表示や、ランキングの上位N件を取得する際などに役立ちます。
しかし、前述の通り、ROWNUM
の挙動は、特にWHERE
句でのフィルタリングやORDER BY
句との組み合わせにおいて、注意が必要です。その仕組みを理解せずに利用すると、期待通りの結果が得られないだけでなく、誤ったデータを取得してしまうリスクも存在します。
この記事を通して、ROWNUM
の正しい理解を深め、より適切なクエリの書き方を習得しましょう。
2. Oracle ROWNUMの基本的な使い方
ROWNUM
は、SELECT
リストの中で他の列と一緒に指定することも、WHERE
句の中で条件として利用することも可能です。
最も基本的な使い方は、結果セットの最初のN件を取得することです。これは、WHERE ROWNUM <= N
という条件を使って実現します。
例として、以下のようなemployees
テーブルがあると仮定します。
“`sql
— employees テーブルの簡易定義 (例)
CREATE TABLE employees (
employee_id NUMBER PRIMARY KEY,
first_name VARCHAR2(50),
last_name VARCHAR2(50),
hire_date DATE,
salary NUMBER
);
— サンプルデータの挿入
INSERT INTO employees VALUES (100, ‘Steven’, ‘King’, DATE ‘2003-06-17’, 24000);
INSERT INTO employees VALUES (101, ‘Neena’, ‘Kochhar’, DATE ‘2003-09-21’, 17000);
INSERT INTO employees VALUES (102, ‘Lex’, ‘De Haan’, DATE ‘2003-01-13’, 17000);
INSERT INTO employees VALUES (103, ‘Alexander’, ‘Hunold’, DATE ‘2006-01-03’, 9000);
INSERT INTO employees VALUES (104, ‘Bruce’, ‘Ernst’, DATE ‘2007-05-21’, 6000);
INSERT INTO employees VALUES (105, ‘David’, ‘Austin’, DATE ‘2005-06-25’, 4800);
INSERT INTO employees VALUES (106, ‘Valli’, ‘Pataballa’, DATE ‘2006-02-05’, 4800);
INSERT INTO employees VALUES (107, ‘Diana’, ‘Lorentz’, DATE ‘2007-02-07’, 4200);
INSERT INTO employees VALUES (108, ‘Nancy’, ‘Greenberg’, DATE ‘2002-08-17’, 12000);
INSERT INTO employees VALUES (109, ‘Daniel’, ‘Faviet’, DATE ‘2002-08-16’, 9000);
— さらに多数のデータがあることを想定
“`
例1:最初の10件の従業員を取得する
sql
SELECT ROWNUM, employee_id, first_name, last_name, salary
FROM employees
WHERE ROWNUM <= 10;
このクエリを実行すると、データベースはemployees
テーブルから行を取得し始めます。取得された最初の行にROWNUM=1
、次の行にROWNUM=2
…と割り当てていきます。同時に、WHERE ROWNUM <= 10
という条件を評価します。ROWNUM
の値が10以下である限り、その行は結果セットに含まれます。ROWNUM
が11になった時点で条件を満たさなくなるため、それ以降の行は処理されません。
この処理の重要な点は、ROWNUM
の値が条件を満たしているかどうかを確認しながら順次割り当てられるということです。これが、後述する注意点に繋がります。
例2:最初の1件の従業員を取得する
sql
SELECT ROWNUM, employee_id, first_name, last_name, salary
FROM employees
WHERE ROWNUM <= 1;
または、より簡潔に
sql
SELECT ROWNUM, employee_id, first_name, last_name, salary
FROM employees
WHERE ROWNUM = 1;
WHERE ROWNUM = 1
は特別で、最初の1行だけを取得したい場合に有効です。最初の行にROWNUM=1
が割り当てられ、条件を満たすため結果セットに含まれます。次の行が処理される前に条件を満たした行数が1件に達するため、処理が停止(または次の行のROWNUM
が2になるが条件=1
を満たさないため対象外)し、最初の1件のみが取得されます。
3. ROWNUMを使う上での重要な注意点
ROWNUM
はその性質上、いくつかの「落とし穴」があります。これらの注意点を理解せずに利用すると、予期しない結果に悩まされることになります。
注意点1: WHERE ROWNUM > N
は機能しない
これは、ROWNUM
に関する最も一般的な誤解の一つです。「10件目以降のデータを取得したいから、WHERE ROWNUM > 10
とすれば良いのではないか?」と考えてしまいがちですが、これはうまくいきません。この条件では、通常何も行が返されません。
理由を理解するために、ROWNUM
がどのように割り当てられるかを思い出してください。行は一つずつフェッチされ、条件を満たすかどうかチェックされます。
- 最初の行がフェッチされます。これに
ROWNUM=1
が割り当てられます。 WHERE ROWNUM > 10
という条件が評価されます。1 > 10
は偽(False)です。- 最初の行は条件を満たさないため、結果セットに含まれません。
- 次の行を処理しようとしますが、まだ条件を満たした行が一つもありません。したがって、次の行に
ROWNUM=2
が割り当てられることはありません(実際には、最初の行が条件を満たさなかった時点で、それ以降の行も同様に条件を満たさないことが確定するため、処理が進まないと考えられます)。
結果として、ROWNUM
が1
として割り当てられた最初の行が条件 > N
(N>=1) を満たさないため、その行はスキップされます。そして、条件を満たした行がまだ0件であるため、次の行に連番を割り当てるプロセスが進まないのです。したがって、どの行もROWNUM
が1
より大きい値になる機会を得られず、結果セットは空になります。
例3:WHERE ROWNUM > 10 (何も返されない)
sql
-- これは何も行を返しません!
SELECT ROWNUM, employee_id, first_name, last_name, salary
FROM employees
WHERE ROWNUM > 10;
注意点2: WHERE ROWNUM >= N
も同様に機能しない(N > 1 の場合)
注意点1と同様の理由で、WHERE ROWNUM >= N
という条件も、N
が2以上の場合には機能しません。
- 最初の行がフェッチされ、
ROWNUM=1
が割り当てられます。 WHERE ROWNUM >= N
(N >= 2
) という条件が評価されます。1 >= N
は偽(False)です。- 最初の行は条件を満たさないため、結果セットに含まれません。
- 次の行に
ROWNUM=2
が割り当てられることがなく、処理が進みません。
結果として、N > 1
の場合は何も行が返されません。
例4:WHERE ROWNUM >= 2 (何も返されない)
sql
-- これは何も行を返しません!
SELECT ROWNUM, employee_id, first_name, last_name, salary
FROM employees
WHERE ROWNUM >= 2;
例外:WHERE ROWNUM >= 1
は機能する
ただし、WHERE ROWNUM >= 1
という条件は機能します。これは、最初の行にROWNUM=1
が割り当てられた際に、1 >= 1
という条件を満たすためです。最初の行が結果セットに含まれることで、次の行にはROWNUM=2
が割り当てられる機会が生まれ、それも2 >= 1
を満たし…というように処理が進みます。結局、この条件はすべての行に対して真となり、テーブルの全行が取得されます。これは、WHERE 1=1
のような無条件と同じ効果を持ちます。
注意点3: ORDER BY
と ROWNUM の評価順序
ROWNUM
が厄介なもう一つの点は、ORDER BY
句との関係です。SQLの論理的な処理順序において、WHERE
句によるフィルタリングは通常、ORDER BY
句による並べ替えより前に実行されます。ROWNUM
はWHERE
句が評価される過程で割り当てられます。
これは何を意味するかというと、ROWNUM
が割り当てられ、WHERE ROWNUM <= N
の条件でフィルタリングが行われた後に、ORDER BY
による並べ替えが行われる可能性がある、ということです。つまり、並べ替えが適用される前の「最初のN件」が取得されてしまうことになります。
例5:間違ったORDER BYとROWNUMの組み合わせ(意図しない結果になる可能性)
「給与が高い順に並べたときの、最初の10件の従業員を取得したい」と考え、以下のようなクエリを書いたとします。
sql
-- これは意図した結果にならない可能性が高いです!
SELECT ROWNUM, employee_id, first_name, last_name, salary
FROM employees
WHERE ROWNUM <= 10
ORDER BY salary DESC;
このクエリは、ROWNUM
が割り当てられ、WHERE ROWNUM <= 10
でランダムな(またはデータが物理的に格納されている順序に近い)最初の10件がフィルタリングされた後に、その10件が給与の高い順に並べ替えられて表示されます。
例えば、テーブルに合計100件のデータがあり、その中で最も給与が高い従業員がたまたま11件目以降に位置していた場合、このクエリではその従業員は結果に含まれません。あなたが本当に欲しかったのは、「全従業員の中で給与が高い方から数えて最初の10件」のはずです。
正しい方法:副問い合わせを使って先に並べ替える
並べ替えを行った後の結果に対してROWNUM
を適用するには、副問い合わせ(Subquery)を利用する必要があります。まず、内部の副問い合わせで目的の順序にデータを並べ替え、その並べ替えられた結果全体に対して、外側のクエリでROWNUM
を適用してフィルタリングします。
sql
SELECT ROWNUM, employee_id, first_name, last_name, salary
FROM (
-- まず、給与が高い順に全従業員を並べ替える
SELECT employee_id, first_name, last_name, salary
FROM employees
ORDER BY salary DESC
) sorted_employees
WHERE ROWNUM <= 10;
このクエリでは、以下の手順で処理が進みます。
- 内側の副問い合わせ
(SELECT ... FROM employees ORDER BY salary DESC)
が実行されます。これにより、全従業員が給与の高い順に並べ替えられた一時的な結果セットが生成されます。この一時的な結果セットは、sorted_employees
という別名で参照されます。 - 外側のクエリ
SELECT ROWNUM, ... FROM sorted_employees WHERE ROWNUM <= 10
が、このsorted_employees
という結果セットに対して実行されます。 sorted_employees
の最初の行から順に、ROWNUM=1
,ROWNUM=2
…と割り当てられ、WHERE ROWNUM <= 10
の条件でフィルタリングされます。
この方法であれば、「給与が高い順に並べたときの、最初の10件」という意図通りの結果を取得できます。
4. ROWNUMを使ったページングの実装方法
ウェブサイトなどでよく見られる「ページング」機能(例: 1ページに10件表示し、次のページ、前のページに移動できる機能)をROWNUM
を使って実現するには、注意点1, 2で説明したWHERE ROWNUM > N
が直接使えないという問題を回避する必要があります。
伝統的に、ROWNUM
を使ったページングは、前述の「正しいORDER BY
の使い方」で見た副問い合わせのテクニックを応用して実現されます。
具体的な方法としては、以下の手順を踏みます。
- 内側の副問い合わせで、目的の順序で全データを並べ替えます。
- さらにその外側の副問い合わせで、並べ替えられた結果に対して
ROWNUM
を付与します。 - 最も外側のクエリで、付与された
ROWNUM
に対して範囲指定を行います。
例6:ROWNUMを使ったページング(2ページ目、11件目から20件目を給与が高い順に取得)
sql
-- ROWNUMを使った伝統的なページング手法
SELECT employee_id, first_name, last_name, salary
FROM (
-- Step 2: 並べ替えられた結果にROWNUMを付与する副問い合わせ
SELECT ROWNUM as rn, employee_id, first_name, last_name, salary
FROM (
-- Step 1: 目的の順序で全データを並べ替える副問い合わせ
SELECT employee_id, first_name, last_name, salary
FROM employees
ORDER BY salary DESC -- 例: 給与が高い順
) sorted_employees
) ranked_employees
WHERE rn BETWEEN 11 AND 20; -- Step 3: 付与されたROWNUM(rn)で範囲指定
このクエリの実行手順を詳しく見てみましょう。
- 一番内側の副問い合わせ
SELECT employee_id, first_name, last_name, salary FROM employees ORDER BY salary DESC
が実行され、全従業員データが給与の高い順に並べ替えられます。 - 次に、その結果セットに対して二番目の副問い合わせ
SELECT ROWNUM as rn, ... FROM (...) sorted_employees
が実行されます。ここで、並べ替えられた結果の各行に、ROWNUM
が1
,2
,3
, …という順で割り当てられ、rn
という別名が付けられます。この時点では、まだ全行にrn
が付与されています。 - 最後に、一番外側のクエリ
SELECT employee_id, ... FROM (...) ranked_employees WHERE rn BETWEEN 11 AND 20
が実行されます。二番目の副問い合わせの結果セット(ranked_employees
)から、rn
の値が11から20の間の行だけがフィルタリングされ、最終的な結果として返されます。
このように、ROWNUM
を使って中間的な連番を割り当て、その連番に対して範囲指定を行うことで、特定の範囲の行(例: ページ)を取得することが可能です。
ただし、この方法にはいくつかの欠点があります。
- 複雑さ: クエリがネストされた副問い合わせになり、可読性が低くなります。
- メンテナンス性: ページ番号や1ページあたりの件数を変更する際に、
BETWEEN
句の値を計算する必要があり、少し面倒です。 - パフォーマンス: 大量のデータに対してまず並べ替えを行い、その結果全体に対して
ROWNUM
を割り当てる必要があるため、特にデータの総数が多い場合や、取得したいページが後の方にある場合にパフォーマンスが低下する可能性があります。
これらの欠点を解消するために、モダンなOracle Databaseのバージョンでは、よりシンプルで効率的な代替手段が提供されています。
5. ROWNUM以外の代替手段(モダンなOracle Databaseでの推奨)
Oracle Database 12c以降では、標準SQLに準拠したページングのための句が導入されました。これらの句を利用することで、ROWNUM
を使った複雑な副問い合わせの必要がなくなり、クエリの可読性とメンテナンス性が大幅に向上します。また、オプティマイザによる適切な実行計画の選択が期待でき、パフォーマンス面でも有利になることが多いです。
主要な代替手段は以下の2つです。
OFFSET
およびFETCH FIRST / NEXT
句ROW_NUMBER()
分析関数
それぞれについて詳しく見ていきましょう。
5.1. OFFSET
および FETCH FIRST / NEXT
句 (Oracle 12c以降)
これは、まさにページングのために導入された句であり、最も推奨される方法です。ORDER BY
句と組み合わせて使用し、「指定した数の行をスキップし、その後の指定した数の行を取得する」という処理をシンプルに記述できます。
構文は以下のようになります。
sql
SELECT ...
FROM table_name
[WHERE condition]
ORDER BY columns -- 並べ替えは必須または推奨される
OFFSET offset_rows ROWS -- スキップする行数
FETCH {FIRST | NEXT} fetch_rows ROWS {ONLY | WITH TIES}; -- 取得する行数
OFFSET offset_rows ROWS
: 結果セットの先頭から、offset_rows
で指定した数の行をスキップします。FETCH FIRST | NEXT fetch_rows ROWS
: スキップした行の次に、fetch_rows
で指定した数の行を取得します。FIRST
とNEXT
は同義です。ONLY | WITH TIES
:ONLY
:fetch_rows
で指定した数だけ正確に取得します。WITH TIES
:fetch_rows
件目の行と同じソートキーを持つ行が他にも存在する場合、それらの行もすべて含めて取得します。このオプションを使用する場合、必ずORDER BY
句を指定する必要があります。通常、ページングではONLY
が使われます。
この句を使うと、前述の「11件目から20件目を給与が高い順に取得する」というページングの例が以下のように非常にシンプルになります。
例7:OFFSET / FETCH FIRST を使ったページング(2ページ目、11件目から20件目を給与が高い順に取得)
sql
-- OFFSET / FETCH FIRST を使ったモダンなページング
SELECT employee_id, first_name, last_name, salary
FROM employees
ORDER BY salary DESC -- 並べ替え
OFFSET 10 ROWS -- 最初の10件をスキップ
FETCH FIRST 10 ROWS ONLY; -- 次の10件を取得
このクエリは、ROWNUM
を使った副問い合わせの例と全く同じ結果を返しますが、その記述は格段に分かりやすくなっています。
この句を使う際のポイント:
- 必ず
ORDER BY
句と組み合わせるのが一般的です。OFFSET
やFETCH
は、並べ替えられた結果セットに対して意味を持ちます。ORDER BY
を指定しない場合、どの行が「最初のN件」や「次のM件」になるかは保証されません。 offset_rows
やfetch_rows
には、バインド変数や式を使用することも可能です。これにより、アプリケーション側でページ番号や1ページあたりの件数に応じて動的に値を設定できます。
OFFSET
/FETCH
句は、標準SQLの機能であり、他の多くのデータベースシステムでも同様の構文がサポートされています。Oracle Database 12c以降でページングを実装する際は、この方法を第一に検討すべきです。
5.2. ROW_NUMBER()
分析関数
ROW_NUMBER()
は、結果セット内の行に連番を割り当てる分析関数です。ROWNUM
と似ていますが、重要な違いがあります。ROW_NUMBER()
は、指定されたパーティション内で、指定された順序に基づいて行に連番を割り当てます。
ROWNUM
が「フェッチされた順序に基づき、条件を満たすかチェックしながら動的に割り当てられる」のに対し、ROW_NUMBER()
は「結果セット全体に対して、指定されたORDER BY
に基づき連番が一度に割り当てられる」という違いがあります。この性質により、ROW_NUMBER()
はROWNUM
の持つ問題点(> N
や>= N
が使えない、ORDER BY
との組み合わせが難しい)を解決できます。
構文は以下のようになります。
sql
ROW_NUMBER() OVER ([PARTITION BY partition_columns] ORDER BY order_columns)
OVER
: 分析関数であることを示します。PARTITION BY partition_columns
: 結果セットを指定した列でグループ化(パーティション化)し、パーティションごとに連番をリセットします(ページング目的では通常不要です)。ORDER BY order_columns
: 連番を割り当てるための順序を指定します。ページング目的の場合、このORDER BY
句が必須となります。
ROW_NUMBER()
を使ってページングを実現する際は、以下の手順を踏みます。
- 内側の副問い合わせで、
ROW_NUMBER()
関数を使って各行に連番を付与します。この際、OVER
句のORDER BY
で並べ替え順序を指定します。 - 外側のクエリで、付与された連番に対して範囲指定を行います。
例8:ROW_NUMBER() 分析関数を使ったページング(2ページ目、11件目から20件目を給与が高い順に取得)
sql
-- ROW_NUMBER() 分析関数を使ったページング
SELECT employee_id, first_name, last_name, salary
FROM (
-- Step 1: ROW_NUMBER() で並べ替え順に基づいた連番を付与
SELECT
employee_id,
first_name,
last_name,
salary,
ROW_NUMBER() OVER (ORDER BY salary DESC) as rn -- 給与が高い順に連番
FROM employees
) ranked_employees
WHERE rn BETWEEN 11 AND 20; -- Step 2: 付与された連番(rn)で範囲指定
このクエリの実行手順:
- 内側の副問い合わせ
SELECT ..., ROW_NUMBER() OVER (ORDER BY salary DESC) as rn FROM employees
が実行されます。employees
テーブルの全データに対して、給与の高い順にROW_NUMBER()
による連番(1から始まる)が付与され、rn
という列として結果セットに含まれます。この時点では、全データにrn
が付与されています。 - 外側のクエリ
SELECT employee_id, ... FROM (...) ranked_employees WHERE rn BETWEEN 11 AND 20
が実行されます。内側の副問い合わせの結果セット(ranked_employees
)から、rn
の値が11から20の間の行だけがフィルタリングされ、最終的な結果として返されます。
ROW_NUMBER()
を使う方法も、ROWNUM
の副問い合わせを使う方法と同様に副問い合わせが必要になりますが、ROW_NUMBER()
関数の OVER
句の中で並べ替え順序を指定できるため、ROWNUM
で必要だった二重の副問い合わせは不要になります。また、ROW_NUMBER()
は結果セット全体に対する論理的な順序付けに基づいているため、BETWEEN
句を使った範囲指定が自然に行えます。
ROW_NUMBER()
は分析関数であるため、より複雑なランキング(例: 各部門内で給与の高い順にランキングを付けるなど、PARTITION BY
を使用)にも対応可能です。ページング目的では、OFFSET
/FETCH
句の方がよりシンプルに記述できるため推奨されますが、ROW_NUMBER()
も強力な選択肢の一つです。
6. パフォーマンスに関する考慮事項
データ取得のパフォーマンスは、アプリケーションのスケーラビリティにおいて非常に重要です。ROWNUM
や代替手段を使ったクエリのパフォーマンスについても理解しておきましょう。
WHERE ROWNUM <= N
のパフォーマンス:ROWNUM <= N
という条件は、Oracleのオプティマイザによって効率的に処理されることが多いです。Oracleは、この条件を満たすN件の行が見つかった時点でテーブルスキャンやインデックススキャンを停止することができます。これは、「Fast Full Scan」や「Fast Top-N Query」最適化として知られています。そのため、単純に最初の数件を取得する場合には、ROWNUM <= N
は非常に高速に動作します。ROWNUM
を使ったページング(副問い合わせ)のパフォーマンス: ページングのためにROWNUM
を副問い合わせと組み合わせる方法は、特にデータ総数が多い場合や、取得したいページが後半にある場合にパフォーマンス上の課題が生じやすいです。- ソートのコスト: まず、内側の副問い合わせで全データを並べ替える必要があります。データ量が多ければ多いほど、このソート処理にかかるコストは増加します。ソート処理はメモリまたは一時ディスク領域を消費します。
- 中間結果セットの生成: 並べ替えられた結果セット全体に対して、
ROWNUM
を付与した中間結果セットが生成されます。これもデータ量に応じてオーバーヘッドが発生します。 - 後半ページの取得: 後半のページを取得しようとすると、
WHERE rn BETWEEN M AND N
という条件になります。この場合、中間結果セットの先頭からM件をスキップし、N件目までを評価する必要があります。OracleがM件を効率的にスキップするメカニズムを持っていたとしても、前半のM件を処理するコストは発生します。
OFFSET
/FETCH
句のパフォーマンス: Oracle 12c以降のOFFSET
/FETCH
句は、ページング処理に最適化されています。オプティマイザは、この構文を認識し、可能な限り効率的な実行計画を選択します。特に、ソート処理やスキップ処理を効率的に行うための内部的な最適化が行われることが期待できます。多くのケースで、ROWNUM
を使った伝統的なページングよりも高速かつ効率的になります。ROW_NUMBER()
を使ったページングのパフォーマンス:ROW_NUMBER()
も、分析関数として結果セット全体に対して処理を行うため、ソートのコストは発生します。ただし、OFFSET
/FETCH
と同様に、現代のオプティマイザはこのパターンを認識し、効率的な実行計画を選択しようとします。OFFSET
/FETCH
がより直接的にページングの意図を表しているため、単純なページング目的であればOFFSET
/FETCH
の方がわずかに優位な実行計画になりやすい傾向がありますが、大きな差がない場合も多いです。- インデックスの活用: ページング処理では、通常
ORDER BY
句で指定した列や、WHERE
句の条件に使用される列に適切なインデックスが存在するかどうかが、パフォーマンスに大きく影響します。特に、並べ替えに使用される列にインデックスがある場合、全件ソートのコストを削減できる可能性があります。
パフォーマンスを最適化するためには、常にEXPLAIN PLAN
を使ってクエリの実行計画を確認することが重要です。これにより、データベースがどのようにクエリを処理しようとしているのか、ボトルネックはどこにあるのかを把握できます。例えば、大量のデータに対するFull Table Scanや、一時表領域への大規模なソート操作(DISK SORT)が発生していないかなどをチェックします。
7. バージョンによる違い
本記事で説明した内容のうち、ROWNUM
の基本的な挙動(特に注意点1, 2, 3)は、Oracle Databaseの非常に古いバージョンから最新バージョンまで共通です。ROWNUM
はOracle独自の機能として長らく存在しています。
一方、OFFSET
/FETCH FIRST
句はOracle Database 12cで導入されました。それより前のバージョン(11gR2以前)では利用できません。これらの古いバージョンでページングを実現する唯一の現実的な方法は、本記事で解説したROWNUM
を副問い合わせと組み合わせる方法でした。
ROW_NUMBER()
分析関数は、Oracle Database 8iで導入された分析関数群の一部です。したがって、比較的新しいバージョン(10g, 11gなど)でも利用可能ですが、OFFSET
/FETCH
句が導入されるまでは、ROWNUM
による副問い合わせがより一般的(ただし複雑)なページング手法でした。
結論として:
- Oracle 12c以降: ページングには
OFFSET
/FETCH
句を最優先で検討すべきです。ROW_NUMBER()
も有効な代替手段です。単純な最初のN件取得にはROWNUM <= N
も依然として高速で利用できますが、混乱を避けるためにFETCH FIRST N ROWS ONLY
を使うのも良いでしょう。 - Oracle 11gR2以前: ページングには
ROWNUM
を副問い合わせと組み合わせる方法、またはROW_NUMBER()
分析関数を使う方法しか選択肢がありません。ROWNUM
の落とし穴を避けるためには、これらの副問い合わせを使った方法が必須となります。
8. まとめ
Oracle DatabaseのROWNUM
擬似列は、クエリ結果の最初のN件を取得する際にはシンプルで高速な手段として有効です(特にWHERE ROWNUM <= N
)。
しかし、その動的な割り当ての性質から、WHERE ROWNUM > N
や WHERE ROWNUM >= N
(N > 1)という条件が機能しないという重要な落とし穴があります。また、ORDER BY
句と直接組み合わせた場合に意図しない結果になる可能性があることも、必ず理解しておくべき注意点です。
特定の範囲の行を取得する「ページング」処理をROWNUM
で実現するためには、データを先に並べ替えた上で、ROWNUM
を副問い合わせ内で利用するという、やや複雑な手法が必要になります。
幸いなことに、Oracle Database 12c以降では、標準SQLに準拠したOFFSET
/FETCH FIRST
句が導入され、ページング処理が格段にシンプルかつ直感的に記述できるようになりました。また、ROW_NUMBER()
分析関数も、ROWNUM
の持つ問題を回避しつつ柔軟な連番割り当てとフィルタリングを可能にする有力な手段です。
現代的なOracle環境で開発を行う場合、以下の推奨事項を参考にしてください。
- 単純な「最初のN件」取得:
WHERE ROWNUM <= N
は依然として効率的ですが、Oracle 12c以降であればFETCH FIRST N ROWS ONLY
も可読性の観点から良い選択肢です。 - 特定の範囲の行取得(ページング): Oracle 12c以降では、
OFFSET offset_rows ROWS FETCH FIRST fetch_rows ROWS ONLY
句の使用を強く推奨します。 これが最もシンプルで効率的な方法です。 - 複雑なランキングや柔軟な連番付け:
ROW_NUMBER()
分析関数を検討してください。特に、PARTITION BY
句が必要なケースなどに適しています。 - Oracle 11gR2以前: ページングや並べ替えを伴う先頭N件以外の取得には、本記事で解説した
ROWNUM
を副問い合わせと組み合わせる方法、またはROW_NUMBER()
分析関数を使う方法が必須となります。
ROWNUM
は歴史的に重要な機能であり、既存の多くのシステムで利用されています。その特性を正しく理解することは、既存コードの解読やメンテナンスにおいても不可欠です。しかし、新規開発においては、OFFSET
/FETCH
句のようなモダンな機能を利用することで、より保守しやすくパフォーマンスの高いクエリを記述できるでしょう。
本記事が、Oracle ROWNUM
に関する理解を深め、より効果的なデータ取得クエリを作成する一助となれば幸いです。