1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Oracle Database 26ai) SQL で N 番目の行を取得する方法を試してみた (2026/05/05)

1
Posted at

はじめに

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 番目の行を取得するクエリで試してみました。

事前準備

手順

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 の最大値制限やインデックス設計が重要です。

参考情報

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?