はじめに
Oracle Database では FETCH FIRST 句を使って先頭 N 行を取得できますが、「ちょうど N 番目の行だけを取りたい」という場面があります。Oracle Databaseの SQL には N 番目の行取得に対応した複数のアプローチがあります。最もシンプルなのは OFFSET N-1 ROWS FETCH NEXT ROW ONLY を使う方法ですが、同じ値を持つ行(タイ)が存在する場合、どの行が返されるかは保証されません。タイの扱い方に分けて考えてみました。
| シナリオ | 要件 | 使用する構文 |
|---|---|---|
| N 番目の行のみ取得 | 単純に N 番目の行だけ返す | OFFSET N-1 ROWS FETCH NEXT ROW ONLY |
| N 番目以降のタイを含む | N 番目の行と、同じ値を持つ後続行も返す | OFFSET N-1 ROWS FETCH NEXT 1 ROW WITH TIES |
| N 番目と同じ値の全行 | N 番目と同じ値を持つ行をすべて返す(前後含む) |
NTH_VALUE + QUALIFY
|
| N 番目の一意値を持つ全行 | N 番目の一意な値(重複なしカウント)を持つ全行を返す |
DENSE_RANK + QUALIFY
|
Oracle Database の HR スキーマのサンプルテーブル hr.employees を使い、給与の低い順に N 番目の行を取得するクエリで試してみました。
事前準備
- Oracle Database 26ai (
QUALIFY句を使う場合必要) -
hrスキーマのサンプルデータにアクセスできること- 参考: HR スキーマのセットアップ
手順
Step 1: 準備データの確認
給与の低い順に並べた上位 5 件の社員を確認して、タイ(同一給与)がどのように分布しているかを確認します。
select employee_id, first_name, salary
from hr.employees
order by salary
fetch first 5 rows only;
実行結果:
EMPLOYEE_ID FIRST_NAME SALARY
----------- -------------------- ----------
132 TJ 2100
128 Steven 2200
136 Hazel 2200
127 James 2400
135 Ki 2400
4 番目と 5 番目の行(James と Ki)が同じ給与 2400 を持っています。また、3 番目の一意な給与も 2400 です。
Step 2: N 番目の行を 1 行だけ取得
OFFSET 句で先頭の N-1 行をスキップし、FETCH NEXT ROW ONLY で次の 1 行のみ返します。たとえば 4 番目に給与が低い社員を取得するには以下のようにします。
select employee_id, first_name, salary
from hr.employees
order by salary
offset 3 rows
fetch next row only;
実行結果:
EMPLOYEE_ID FIRST_NAME SALARY
----------- -------------------- ----------
127 James 2400
Step 3: N 番目の行と後続のタイ行を取得
FETCH NEXT ROW WITH TIES を使うと、N 番目の行に加えて、同じ並び順の値を持つ後続行も含めて返します。
select employee_id, first_name, salary
from hr.employees
order by salary
offset 3 rows
fetch next 1 row with ties;
実行結果(4 番目の行 + 同給与の後続行):
EMPLOYEE_ID FIRST_NAME SALARY
----------- -------------------- ----------
127 James 2400
135 Ki 2400
5 番目の行(offset 4)から始めた場合は Ki のみが返されます(James は前の行であるため含まれません)。
select employee_id, first_name, salary
from hr.employees
order by salary
offset 4 rows
fetch next 1 row with ties;
実行結果:
EMPLOYEE_ID FIRST_NAME SALARY
----------- -------------------- ----------
135 Ki 2400
Step 4: N 番目の行と同じ値を持つ全行を取得(NTH_VALUE を使用)
NTH_VALUE ウィンドウ関数を使うと、N 番目の行の値を取得できます。以下のクエリでは 5 番目の給与の値を各行に表示します。
select employee_id, first_name, salary,
nth_value ( salary, 5 ) over ( order by salary ) fifth_row_sal
from hr.employees
order by salary
fetch first 6 rows only;
実行結果:
EMPLOYEE_ID FIRST_NAME SALARY FIFTH_ROW_SAL
----------- -------------------- ---------- -------------
132 TJ 2100 <null>
128 Steven 2200 <null>
136 Hazel 2200 <null>
127 James 2400 2400
135 Ki 2400 2400
119 Karen 2500 2400
注意:
NTH_VALUEは 5 番目の行に到達するまでnullを返します。デフォルトのウィンドウフレームは「現在の行まで」であり、5 番目の給与に達するまで Nth の値が確定しないためです。
5 番目の行と同じ給与を持つ全員を返すには、NTH_VALUE の結果と salary を比較します。Oracle AI Database 23.26.0 以降では QUALIFY 句を使って直接フィルタリングできます。
select employee_id, first_name, salary
from hr.employees
qualify nth_value ( salary, 5 ) over ( order by salary ) = salary
order by salary;
実行結果:
EMPLOYEE_ID FIRST_NAME SALARY
----------- -------------------- ----------
127 James 2400
135 Ki 2400
Oracle AI Database 23.26.0 より前のバージョンでは、サブクエリを使います。
with nths as (
select employee_id, first_name, salary,
nth_value ( salary, 5 ) over ( order by salary ) nth
from hr.employees
)
select employee_id, first_name, salary
from nths
where nth = salary
order by salary;
実行結果:
EMPLOYEE_ID FIRST_NAME SALARY
----------- -------------------- ----------
127 James 2400
135 Ki 2400
Step 5: N 番目の一意な値を持つ全行を取得(DENSE_RANK を使用)
「3 番目に低い一意な給与を持つ全社員を取得したい」という場合(行の位置ではなく値のランクで絞り込む場合)には DENSE_RANK を使います。
DENSE_RANK は同じ値には同じランクを割り当て、連続した整数でランク付けします。RANK との違いに注意してください。RANK はタイの後の行に飛び番号を割り当てますが(例: 1, 1, 3)、DENSE_RANK は飛び番号を付けません(例: 1, 1, 2)。
Oracle AI Database 23.26.0 以降(QUALIFY 使用):
select employee_id, first_name, salary
from hr.employees
qualify dense_rank () over ( order by salary ) = 3
order by salary;
実行結果(第 3 位の給与 2400 を持つ全社員):
EMPLOYEE_ID FIRST_NAME SALARY
----------- -------------------- ----------
127 James 2400
135 Ki 2400
それより前のバージョンではサブクエリを使います。
with ranked as (
select employee_id, first_name, salary,
dense_rank () over ( order by salary ) rnk
from hr.employees
)
select employee_id, first_name, salary
from ranked
where rnk = 3
order by salary;
Step 6: 実行計画を確認
N 番目の行を取得する場合、データベースは N 行目までのすべての行を処理する必要があります。これを実行計画の A-rows 列で確認できます。
以下は 30 番目に給与が低い社員を取得して実行計画を確認する例です。
alter session set statistics_level = all;
set serveroutput off
select employee_id, first_name, salary
from hr.employees
order by salary
offset 29 rows
fetch next row only;
EMPLOYEE_ID FIRST_NAME SALARY
----------- -------------------- ----------
142 Curtis 3100
select * from dbms_xplan.display_cursor ( format => 'ROWSTATS LAST');
実行計画の出力例:
-----------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows |
-----------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |
|* 1 | VIEW | | 1 | 30 | 1 |
|* 2 | WINDOW SORT PUSHED RANK| | 1 | 107 | 30 |
| 3 | TABLE ACCESS FULL | EMPLOYEES | 1 | 107 | 107 |
-----------------------------------------------------------------------
Predicate Information (identified by operation id):
-------------------------------------------------
1 - filter("from$_subquery$_002"."rowlimit_$$_rownumber"<=30 AND
"from$_subquery$_002"."rowlimit_$$_rownumber">29)
2 - filter(ROW_NUMBER() OVER ( ORDER BY "SALARY")<=30)
WINDOW SORT PUSHED RANK の A-rows が 30 になっており、最終的に 1 行しか返さないにもかかわらず 30 行を処理していることがわかります。
注意: N の値が大きいほど処理コストが高くなります。
OFFSETでもウィンドウ関数でも同様です。パフォーマンス対策として以下を検討してください。
- ソート列にインデックスを作成する(テーブルソートの代わりにインデックススキャンが使われる場合があります)
- アプリケーション側で N の最大値を制限し、過大なクエリを拒否する
- 頻繁に使う場合はランク値を列として事前計算・保存する(書き込み時にコストを移動するトレードオフ)
おわりに
-
OFFSET N-1 ROWS FETCH NEXT ROW ONLYを使うと、N 番目の行を 1 行だけシンプルに取得できます。ただしタイが存在する場合、どの行が返されるかは不定です。 -
FETCH NEXT 1 ROW WITH TIESを使うと N 番目の行以降のタイ行も含められますが、それより前のタイ行は含まれません。 -
NTH_VALUE+QUALIFYを使うと、N 番目の行と同じ値を持つ全行(前後含む)を確実に取得できます。 -
DENSE_RANK+QUALIFYを使うと、N 番目の一意な値を持つ全行を取得できます。行の位置ではなく値のランクで絞り込む際に最適です。 - N が大きいほど処理コストが増加するため、アプリケーション側での N の最大値制限やインデックス設計が重要です。
参考情報
- How to get the Nth row with Oracle SQL (Oracle Blogs)
- Oracle Database SQL Language Reference - Row Limiting Clause
- Oracle Database SQL Language Reference - NTH_VALUE
- Oracle Database SQL Language Reference - DENSE_RANK
- Oracle Database SQL Language Reference - QUALIFY clause
- Oracle DBMS_XPLAN パッケージリファレンス