Oracle RANK・DENSE_RANK・ROW_NUMBER完全比較|違い・使い分け・実用SQL
結論:TIE 処理と用途で選ぶ
3つの関数は似た構文でも、同順位をどう扱うかと必要な結果件数によって選択が変わります。
最初に「同点を同順位にするか」「順位を飛ばすか」「1行ずつ一意に選ぶか」を決めると迷いません。
要件定義では、同点が境界にある具体例を1つ書き、期待する順位番号と返却行数を並べておくと、実装者とレビュー担当者の解釈をそろえられます。
関数の違いを 1 表で
RANKは同点を同順位にして次の順位を飛ばし、DENSE_RANKは同順位のまま次の順位を詰め、ROW_NUMBERは各行へ必ず異なる番号を振ります。
たとえば給与が5000、3000、3000、2500なら、順位列はRANKが1・2・2・4、DENSE_RANKが1・2・2・3、ROW_NUMBERが1・2・3・4になります。
| 関数 | 同点 | 次順位 | 主な用途 |
|---|---|---|---|
| RANK | 同順位 | 飛ぶ | 競技順位・同率順位 |
| DENSE_RANK | 同順位 | 飛ばない | 値の段階・上位N種類 |
| ROW_NUMBER | 別番号 | 常に連番 | 代表1行・ページング |
比較表を見るときは順位の数字だけでなく、上位N位を条件にした場合に何行返るかまで確認するのが重要です。
同点を含む業務要件ではRANK系、必ず1行ずつ切り出す処理ではROW_NUMBERという大枠を先に押さえましょう。
具体的な使い分け
競技順位のように欠番を意味として残すならRANK、金額帯やランク帯を連続番号で扱うならDENSE_RANKが自然です。
ページング、重複排除、各グループから最新1件を取る処理では、一意な連番を作れるROW_NUMBERが扱いやすくなります。
ROW_NUMBERで同じORDER BY値が並ぶ場合、処理順に依存して結果が変わり得るため、主キーなどを追加して全順序を作る必要があります。
関数名から選ぶのではなく、同点の扱いと返したい行数を仕様として文章化してからSQLへ落とし込むと安全です。
典型的な用途
RANKはスコア順位や表彰順位、DENSE_RANKは上位N種類の値や連続した評価帯、ROW_NUMBERは1行選択やページ単位の切り出しでよく使われます。
部門別Top3のような要件でも、3行だけ欲しいのか、3位タイを全員含めたいのかで選ぶ関数が変わります。
重複排除ではPARTITION BYで重複キーをまとめ、残したい順序をORDER BYへ書いてROW_NUMBER=1の行だけを残すのが定番です。
ランキング表示とデータ抽出を同じSQLで行うと要件が混ざりやすいため、表示順位と取得条件を別々に確認すると設計しやすくなります。
用途別チャート
「同点は同順位で次の順位を飛ばす」ならRANK、「同点は同順位で番号を詰める」ならDENSE_RANK、「同点でも各行を分ける」ならROW_NUMBERです。
Top NのNが順位を意味するのか、行数を意味するのかを質問すると、関数選択の多くはその場で決まります。
上位3行という要件をRANKで書くとタイの位置次第で3行を超えるため、固定行数が必要ならROW_NUMBERかFETCH FIRSTを検討します。
一方で表彰対象を同率3位まで含める要件なら、固定3行へ切るよりRANKで業務順位を表現する方が意図に合います。
関数の結果比較(1 行で理解)
RANKは「同順位あり・欠番あり」、DENSE_RANKは「同順位あり・欠番なし」、ROW_NUMBERは「同順位なし・連番のみ」と覚えると整理できます。
この1行を基準にすると、同じOVER句を使っていても返す番号の意味がまったく違うことを見落としにくくなります。
ROW_NUMBERは同じ値を同順位として扱う関数ではないため、順位表の意味を持たせたい場面で安易に置き換えないようにします。
逆にRANKやDENSE_RANKは同順位の行へ同じ番号を返すので、ユニークな行識別子としては利用できません。
基本構文
基本形はRANK() OVER (PARTITION BY グループ列 ORDER BY 並べ替え列)のように書き、DENSE_RANKとROW_NUMBERも同じ形で利用できます。
PARTITION BYを省略すると結果全体が1つのグループになり、指定すると部門やカテゴリごとに順位が1から振り直されます。
SQL例: SELECT ename, deptno, sal, RANK() OVER (PARTITION BY deptno ORDER BY sal DESC) AS rnk FROM emp。
分析関数のORDER BYは順位計算の順序を決めるもので、SELECT結果そのものの表示順を保証しない点に注意してください。
画面表示の順序まで固定したい場合は、分析関数内とは別にSELECT文末のORDER BYを明示します。
6つの必須知識
実務で押さえるべき要点はTIE、PARTITION BY、決定的なORDER BY、NULLの位置、Top Nの意味、最終表示順の6つです。
特にROW_NUMBERはORDER BYが一意にならないと同点行の番号が不定になり得るため、再現性が必要な処理ほどタイブレーク列が重要になります。
昇順ではNULLS LAST、降順ではNULLS FIRSTが既定なので、業務上の期待と異なる場合はNULLS FIRSTまたはNULLS LASTを明記します。
関数の結果をWHEREで直接絞る設計では副問合せやCTEを使い、分析関数を計算した後の外側で条件を適用する形にすると理解しやすくなります。
関数の動作詳解
動作差は小さく見えますが、同値行が現れた瞬間に結果番号と抽出件数へ直接影響します。
同じデータへ3関数を同時に適用すると、仕様上の違いを最短で確認できます。
3関数を理解するときは、同値がない正常系だけでなく、同値が先頭・中央・末尾にあるケースを比べると、順位の欠番と連番の差がはっきり見えます。
サンプルテーブル(emp)
説明にはemp表のename、deptno、salを使い、同じ部門内に同額給与がある状態を想定すると違いが分かりやすくなります。
同値が一度も現れないデータだけで試すと3関数が同じ番号を返すため、比較用データには意図的にTIEを含めます。
検証時はNULL給与も1行加えると、NULLS FIRSTとNULLS LASTが順位へ与える影響まで同時に確認できます。
本番データを直接更新せず、CTEや検証用テーブルで小さな再現ケースを作ると結果の読み違いを防げます。
関数同時実行
同じSELECTでRANK、DENSE_RANK、ROW_NUMBERを並べれば、同点前後の番号がどのように変わるかを1回の実行で比較できます。
ORDER BY sal DESCを共通にすると、同額給与の行だけが3関数の差を生むため、結果表から意味を追いやすくなります。
SQL例: SELECT ename, sal, RANK() OVER (ORDER BY sal DESC) AS rnk, DENSE_RANK() OVER (ORDER BY sal DESC) AS drnk, ROW_NUMBER() OVER (ORDER BY sal DESC, empno) AS rn FROM emp。
ROW_NUMBERへemployee_idなどを第2ソートキーとして加えると、一意な並びになり番号の再現性を確保できます。
3列を別々の問合せで確認するより、同一入力を同時評価した方がデータ差による誤解を避けられます。
関数の本質的違い
RANKは自分より前にある行数を意識した順位、DENSE_RANKは自分より上位にある異なる値の数を意識した順位として捉えられます。
ROW_NUMBERは順位というより並べ替え後の行へ一意な通し番号を与える仕組みなので、同点という概念を保持しません。
この違いから、RANKは順位の欠番が意味を持つ表現、DENSE_RANKは値の段階、ROW_NUMBERは行の選択に向いています。
業務仕様の「3位」と「3行目」は同じ言葉に見えても別物なので、SQL設計前にどちらを求めているか確認してください。
OVER 句の完全解説
ランキング関数を正しく使うには関数名だけでなく、OVER句の区切り方と並べ方を設計する必要があります。
PARTITION BYとORDER BYを分けて考えると、グループ範囲と順位基準を混同しにくくなります。
OVER句を変更したときは、行数が変わらなくても順位の意味が変化するため、SQL差分だけでなく期待結果表も更新してレビューするのが有効です。
PARTITION BY
PARTITION BYは分析対象を論理的なグループへ分け、各グループの中で順位計算を最初からやり直します。
deptnoを指定すれば部門ごと、category_idを指定すればカテゴリごとに1位から番号が始まります。
GROUP BYと違って明細行をまとめて1行へ縮約しないため、元の行を残したままグループ内順位を付けられます。
区切り列を増やすほどパーティションは細かくなるので、業務上どの単位で競わせるのかを先に決めます。
ORDER BY
分析関数内のORDER BYは、順位や行番号を決める評価順序を定義します。
降順なら大きい値を先頭へ、昇順なら小さい値を先頭へ置けるため、TopとBottomを同じ関数で表現できます。
ROW_NUMBERで安定した結果が必要なら、主条件に続けて一意キーを指定し、全行の前後関係が必ず決まるようにします。
分析用ORDER BYだけでは最終結果の表示順は保証されないため、必要なら問合せ末尾にもORDER BYを置きます。
NULLS FIRST / NULLS LAST
Oracleの分析ORDER BYでは昇順の既定がNULLS LAST、降順の既定がNULLS FIRSTです。
欠損値を最下位へ送りたいランキングでは、降順でもNULLS LASTを明示して意図をコードへ残すと安全です。
既定値へ依存するとASCとDESCの切替時にNULLの位置まで変わるため、レビュー時に見逃しやすい不具合になります。
NULLを順位対象外にする要件なら、並べ方で調整するだけでなくWHERE条件で除外するかどうかも検討します。
PARTITION BY + ORDER BY
PARTITION BYで競争範囲を決め、ORDER BYでその範囲内の評価基準を決める組合せが分析関数の基本です。
たとえば部門別給与ランキングならPARTITION BY deptnoとORDER BY sal DESCを組み合わせます。
SQL例: ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY published_at DESC, product_id DESC) のように、区切りと順序を同時に定義します。
同額給与の表示順まで固定したい場合はORDER BY sal DESC, employee_idのようにタイブレーク列を追加します。
ただしRANKやDENSE_RANKで同額を同順位にしたいなら、順位判定へ含めるORDER BY列を増やすと同順位が解消される点に注意が必要です。
実用例10選
関数の選択は抽象的な説明より、取得したい行の形へ当てはめると判断しやすくなります。
ここではランキング、ページング、重複排除から周辺分析まで、目的ごとに適した書き方を整理します。
実用SQLを流用するときは、サンプルの列名だけを置き換えるのではなく、同点を許すか、代表1行へ絞るか、NULLを含めるかを再確認してください。
例1: 部門別給与 Top 3
部門ごとに上位3位を取り、3位タイも含めたいならRANK() OVER (PARTITION BY deptno ORDER BY sal DESC)を使います。
外側の問合せでrnk <= 3と絞ると、同順位の人数によって返却行数が3行を超える可能性があります。
SQL例: SELECT * FROM (SELECT e.*, RANK() OVER (PARTITION BY deptno ORDER BY sal DESC) AS rnk FROM emp e) WHERE rnk <= 3。
必ず各部門3行に固定したい場合はROW_NUMBERへ切り替え、同額時のタイブレーク列も指定します。
DENSE_RANK <= 3は上位3種類の給与額を持つ全員という意味になるため、RANKとの違いを要件書に残すと誤解を防げます。
例2: ページング(OFFSET-FETCH 代替)
ROW_NUMBERで全行へ順序番号を付け、外側で11から20のような範囲を指定すればページ単位の抽出を実装できます。
古いOracle互換や複雑な順序制御では有効ですが、現在の環境ではOFFSETとFETCHによる行制限も比較対象になります。
ページ間で行が重複したり抜けたりしないよう、ORDER BYは更新で揺れにくい列と一意キーを組み合わせることが重要です。
大量オフセットは後方ページほどコストが増えることがあるため、実行計画とデータ件数を見てキーセット方式も検討します。
例3: 重複排除(超典型パターン)
重複キーごとにPARTITION BYし、残したい優先順でROW_NUMBERを振ってrn=1だけ残すと、DISTINCTでは表現できない選択ができます。
たとえばcustomer_idごとにupdated_at DESC, id DESCと並べれば、最新かつ同時刻なら最大IDの1行を選べます。
SQL例: SELECT * FROM (SELECT t.*, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY updated_at DESC, id DESC) AS rn FROM t) WHERE rn = 1。
並べ替え条件が不足すると、どの重複行を残すかが不定になり、実行のたびに結果が変わる可能性があります。
削除処理へ応用するときは、まずSELECTでrn>1の対象を確認してからトランザクション内で削除する方が安全です。
例4: 各部門の最高給与者
最高給与者を同額者全員含めて取得するならRANKまたはDENSE_RANKで1位を抽出すると意図が明確です。
各部門から代表者を必ず1人だけ選ぶ要件ならROW_NUMBER=1を使い、employee_idなどの選定基準を追加します。
MAX(sal)と結合する方法でも実現できますが、分析関数なら明細列を保持したまま順位条件を追加しやすい利点があります。
同率1位を許すか1人に決めるかは業務判断なので、SQLだけで暗黙に決めないことが重要です。
例5: Median(中央値)計算
中央値そのものを求めるならOracleのMEDIANやPERCENTILE_CONTが直接的で、DENSE_RANKを第一選択にする必要はありません。
順位関数で中央値候補を作る方法は、重複値の扱いや偶数件の定義を自前で調整する必要があり、保守性が下がりやすくなります。
既存SQLがDENSE_RANKを使っている場合は、求めたい統計量が値の中央値なのか順位帯の中央なのかを確認してください。
分析目的のSQLでは結果の定義をコメントやテストデータで固定し、関数名だけから統計的意味を推測しないようにします。
例6: N 番目の値
N番目の行が欲しいならROW_NUMBER=N、N番目の異なる値を持つ全行が欲しいならDENSE_RANK=Nが分かりやすい使い分けです。
RANK=Nは前の同点数によってNという順位自体が欠番になる場合があるため、常に結果が存在するとは限りません。
SQL例: SELECT * FROM (SELECT e.*, DENSE_RANK() OVER (ORDER BY sal DESC) AS drnk FROM emp e) WHERE drnk = 3。
たとえば給与の3番目に高い異なる金額を探すならDENSE_RANKを使うと重複社員をまとめて取得できます。
要件の「N番目」が行位置か業務順位か異なる値の段階かを明文化すると、関数選択をレビューしやすくなります。
例7: パーセンタイル分位分割
データをほぼ同じ件数のグループへ分けたいときはNTILE、順位比率が欲しいときはPERCENT_RANKやCUME_DISTが候補になります。
RANKやDENSE_RANKを手作業で割合へ変換するより、目的に合う専用分析関数を使う方が定義を伝えやすくなります。
同点値が境界付近に集中する場合、NTILEは同じ値を別バケットへ分ける可能性があるため、値の一体性が必要か確認します。
顧客ランク付けなど業務区分へ使う場合は、統計的分位とビジネスルールの境界を混同しないようにしてください。
例8: 累積・変化(LAG/LEAD)
前行や次行との差分を取りたい場合は順位関数ではなくLAGとLEADを使うと、自己結合なしで隣接行を参照できます。
日次売上の前日差やステータス遷移の前後比較では、ORDER BYに時系列列を指定して基準順序を固定します。
同一時刻データが複数ある場合は、LAGやLEADでもどの行が隣になるか不定になり得るため、一意なタイブレークが必要です。
ランキング列と併用すると、上位商品の順位変化や前回値との差を1つの結果セットへまとめられます。
例9: 最初/最後の値(FIRST_VALUE/LAST_VALUE)
並び順の先頭値や末尾値を各行へ参照したい場合はFIRST_VALUEとLAST_VALUEが便利です。
LAST_VALUEはウィンドウ句を省略すると既定のフレームが現在行までになるため、期待する全体末尾値にならないことがあります。
パーティション全体の最後を取りたい場合はUNBOUNDED FOLLOWINGを含むフレームを明示して意図を固定します。
ランキングと同じORDER BYを使う場合でも、順位関数と値参照関数ではウィンドウフレームの意味が異なる点を意識します。
例10: 集計関数版 RANK
OracleのRANKとDENSE_RANKにはWITHIN GROUPを使う集計形式があり、仮想的な値が集合の中で何位になるかを求められます。
分析関数版が各行へ順位を付けるのに対し、集計形式は指定した1つの仮想行の相対位置を返す用途です。
引数とWITHIN GROUP内のORDER BY式は位置と型を対応させる必要があるため、列数や並び順を変更するときはセットで確認します。
通常の一覧ランキングと目的が異なるので、コードレビューではOVER版とWITHIN GROUP版を同じ機能として扱わないようにします。
姉妹分析関数
順位だけでは答えにくい分析には、分位、累積分布、前後行参照など専用の分析関数があります。
RANK系へ無理に寄せず、欲しい統計量や参照関係に合わせて関数を選ぶとSQLを短くできます。
専用関数を選ぶ基準は、順位そのものが欲しいのか、相対位置、分位、前後差、端点の値が欲しいのかを言葉で区別することです。
NTILE(分位分割)
NTILE(n)はORDER BYされたパーティションをn個のバケットへ分け、各行へ1からnの番号を割り当てます。
四分位ならNTILE(4)、十分位ならNTILE(10)のように、件数ベースのグループ分けを簡潔に書けます。
同じ値を必ず同じバケットへ入れる順位関数ではないため、境界の同点値が分割される可能性を要件と照合します。
評価ランクとして使う場合は、データ件数の変化で境界が動くことを利用者へ説明できるようにします。
PERCENT_RANK(0-1 の比率)
PERCENT_RANKはパーティション内の順位を0から1の範囲へ変換し、先頭行は0になります。
概念的には順位から相対位置を求めるため、単なる行番号の割合とは異なります。
件数が少ない集合では値の刻みが大きくなるため、細かなパーセンタイル表現が必要かどうかを確認します。
スコアの相対的位置を表示するときは、百分率表示へ変換する前にNULLや対象母集団の範囲を固定します。
CUME_DIST(累積分布)
CUME_DISTは現在値以下に位置する行の割合を返し、結果は0より大きく1以下になります。
同順位の行は同じ累積分布値になるため、同点を含む分布の把握に利用できます。
PERCENT_RANKとは分子の考え方が違うので、同じデータでも値が一致するとは限りません。
ダッシュボードへ表示する場合は、どちらの定義を採用したかを列名や仕様書で明示します。
FIRST_VALUE / LAST_VALUE
FIRST_VALUEとLAST_VALUEは並べ替えた集合の先頭または末尾の値を、各行から参照するための関数です。
最大値や最小値だけならMAXやMINでも表現できますが、並び順と別の列値を取得したい場面で役立ちます。
LAST_VALUEは既定ウィンドウの影響を受けやすいため、全パーティションを対象にしたいときはフレームを明示します。
IGNORE NULLSを使う設計では、NULLを欠損として飛ばすのか意味ある値として残すのかを業務定義と合わせます。
LAG / LEAD
LAGは前の行、LEADは後ろの行を参照でき、差分や変化率の計算を自己結合なしで書けます。
offsetを省略した場合は1行前後を参照し、範囲外では既定値を指定しなければNULLになります。
前後関係はORDER BYで決まるため、時系列に同時刻があるなら一意な順序列を追加します。
ROW_NUMBERと同時に出力すると、並びの位置と前後値を照合でき、デバッグもしやすくなります。
ROW_NUMBER と一緒に使うパターン
複数の分析関数を同じSELECTで計算すると、各行の位置、順位、前後差、先頭値などを1つの結果へ集約できます。
ただし同じORDER BYに見えても関数ごとに求める意味が違うため、共通ウィンドウを機械的に使い回さない方が安全です。
CTEでROW_NUMBERを計算して1行へ絞った後、外側で別の分析を行うと、対象集合を段階的に制御できます。
複雑になったら中間結果の件数とキー重複を確認し、分析関数追加によって意図しない行消失がないかテストします。
パフォーマンス最適化
分析関数は並べ替えを伴うことが多いため、関数名の違いより入力件数とソート条件が性能を左右します。
万能な最適化を決め打ちせず、実行計画と実データ分布を見ながらボトルネックを特定します。
チューニング前後は同じバインド値と同じ統計条件で比較し、処理時間だけでなく結果件数と順位値が変わっていないことも確認します。
インデックス
PARTITION BYやORDER BYで使う列に適切な索引があると有利な場合がありますが、分析関数のソートが常に索引だけで解消されるとは限りません。
複合索引は列順、昇降順、絞り込み条件との組合せで効果が変わるため、SQL全体を見て設計します。
低選択性の列へ単独索引を追加するだけでは改善しないことも多く、不要な索引は更新コストを増やします。
性能対策は索引候補を作る前に、対象行数、ソート量、TEMP使用量、アクセス経路を確認するところから始めます。
実行計画確認
EXPLAIN PLANや実行統計でWINDOW SORT、TABLE ACCESS、索引アクセスなどを確認し、推測ではなく実際の処理を見ます。
同じSQLでも統計情報、バインド値、データ分布によって計画が変わるため、代表的な条件で比較する必要があります。
分析関数を1つ減らしただけで速くなるとは限らず、前段の結合や不要列の増加が支配的な場合もあります。
本番相当件数で測定し、CPU時間だけでなく論理読取り、物理I/O、TEMP消費、返却行数も記録します。
Top N の効率
全件へ順位を付けてから絞る方式は分かりやすい一方、単純なTop NならFETCH FIRSTなど別の行制限構文が適する場合があります。
グループごとのTop Nでは分析関数が自然ですが、全体Top Nと同じ最適化になるとは限りません。
返却行数が少なくても、その前に大量行のソートが必要なら処理量は小さくならないため、WHERE条件で入力を絞れるか確認します。
上位Nの意味に同点を含めるなら、性能だけを理由にROW_NUMBERへ置換せず結果の意味が変わらないか検証します。
WITH TIES(12c+)
FETCH FIRST n ROWS WITH TIESは境界行と同じソートキーを持つ追加行も返すため、全体Top Nで同点を含めたい場合に使えます。
WITH TIESを利用するにはORDER BYが必要で、境界の同値数によってn行を超えて返ることがあります。
SQL例: SELECT ename, sal FROM emp ORDER BY sal DESC FETCH FIRST 3 ROWS WITH TIES。
RANK <= nと似た結果になる場面はありますが、パーティションごとのTop Nを直接置き換える構文ではありません。
バージョン互換性とSQLの可読性を確認し、既存のROW_NUMBER方式から切り替える場合は同じテストデータで結果件数を比較します。
Rails / Java / Python 対応
分析関数はアプリケーション側で再実装するより、DBで順位や代表行を確定して返す方が簡潔になる場面があります。
ただしORMやドライバで生SQLを使うときは、バインド変数と結果列の扱いを明確にします。
アプリケーションから動的SQLを組み立てる場合でも、順位計算はDBへ任せつつ、識別子の許可リストと値のバインドを分離すると安全性を保ちやすくなります。
Rails ActiveRecord
ActiveRecordではselectへ分析関数式を追加し、必要ならfromサブクエリやCTEを組み合わせて順位列を扱えます。
ユーザー入力をORDER BY文字列へ直接連結するとSQLインジェクションの危険があるため、許可済み列名へマッピングします。
モデル属性に存在しない順位列は読み取り専用の計算結果として扱い、保存対象と混同しないようにします。
スコープ化すると再利用しやすい一方、複雑なwindow式を隠し過ぎると意図が分かりにくくなるためSQLログも確認します。
Java (JDBC)
JDBCでは分析関数を含むSELECTを通常のPreparedStatementで実行し、順位列をResultSetから数値として取得できます。
絞り込み値はバインドし、列名やASC・DESCのようにバインドできない要素はアプリ側の許可リストで制御します。
ページング用途では同じORDER BYを継続利用し、リクエストごとに並び基準が変わらないようAPI仕様へ固定します。
大量結果を扱う場合はfetch sizeやネットワーク転送量も影響するため、DB実行時間だけでなくアプリ側の計測も行います。
Python (oracledb)
python-oracledbからも分析関数を含むSQLをそのまま実行でき、バインド変数で条件値を安全に渡せます。
結果をpandasへ渡す場合は順位列の型とNULL処理を確認し、DB側の順序を必要とするなら最終ORDER BYもSQLへ書きます。
動的なランキング条件を組み立てるときは、値だけをバインドし、識別子は事前定義した候補から選択します。
分析処理をPythonへ持ち出す前に、DBで絞り込める行を減らしてネットワーク転送を抑えられないか比較します。
実践シナリオ
実際の要件では関数名より「何を同点とみなすか」「何件返したいか」「代表行をどう決めるか」が先に決まります。
典型シナリオごとに判断基準を置くと、同じSQLを別用途へ誤用しにくくなります。
シナリオごとの共通点は、データを並べる前に業務上の勝敗・同率・代表選定ルールを決め、そのルールをORDER BYと関数へ写すことです。
シナリオ1:スコアランキング(オリンピック方式)
同点を同順位とし、その人数分だけ次の順位を飛ばす競技方式ならRANKが要件をそのまま表現します。
100点が2人なら両方1位、次の90点は3位になるため、順位番号そのものに競技上の意味が残ります。
表彰対象を3位までとする場合はrnk <= 3で同順位者を含め、人数が固定にならないことをUI側も想定します。
同点時の表示順だけ固定したい場合は、順位計算とは別の最終ORDER BYに氏名やIDを追加します。
シナリオ2:EC 売上ランキング(順位表方式)
売上額の異なる値を1位、2位、3位と連続した階層で示したいならDENSE_RANKが扱いやすくなります。
同額店舗が複数あっても次の異なる売上額へ連続番号が付くため、ランク帯の欠番が発生しません。
ただし上位3ランクの店舗数は3件とは限らないので、カード表示数を固定したいUIでは別条件が必要です。
ランキング値と売上額を両方返し、画面側で「同率」を明示すると利用者が順位の意味を理解しやすくなります。
シナリオ3:Web ページング
安定したWebページングではROW_NUMBERのORDER BYへ更新日時だけでなく一意キーを加え、同時刻の順序も固定します。
1ページ20件なら1から20、21から40のようにrn範囲で切り出せますが、途中更新で行位置が動く点は残ります。
頻繁に追加されるタイムラインではOFFSET型よりキーセット型が適する場合があるため、要件と件数から方式を選びます。
ページ番号だけを求められても、並べ替えの安定性と更新中の一貫性を一緒に設計することが重要です。
シナリオ4:重複行の削除
自然キーが同じ行をPARTITION BYし、残したい順序でROW_NUMBERを付けると削除候補を明示できます。
更新日時が新しい行を残すならupdated_at DESC、同時刻の決着にid DESCを追加すると選択が決定的になります。
いきなりDELETEへつなげず、まずrn>1の件数と主キーをSELECTしてバックアップやトランザクション方針を確認します。
一度削除した後に同じ重複が再発するなら、SQLだけでなく一意制約や登録処理の設計も見直します。
シナリオ5:部門別 Top 3 給与
部門内の上位3順位を同額者込みで出すならRANK、上位3種類の給与額ならDENSE_RANK、各部門3人固定ならROW_NUMBERを選びます。
同じ「Top 3」でも3つの意味があるため、画面仕様や帳票仕様で期待件数を具体例とともに決めておくと安全です。
PARTITION BY deptnoを外すと会社全体順位へ変わるので、テストでは複数部門のデータを必ず用意します。
給与NULLを対象に含める場合はNULLS LASTなどを明示し、空値が上位に来ないことを確認します。
シナリオ6:各カテゴリの最新商品
category_idごとにpublished_at DESCでROW_NUMBERを振りrn=1を選ぶと、各カテゴリの最新商品を1件ずつ取得できます。
公開日時が同じ商品があるならproduct_id DESCなどを加えて、どちらを最新代表とするかを固定します。
同時刻商品をすべて最新として出したいならROW_NUMBERではなくRANK=1の方が要件に合います。
代表1件か同率全件かをAPI仕様で決めておくと、後から件数が増えた際の互換性問題を避けられます。
シナリオ7:Rails ページング実装
RailsでROW_NUMBERを使う場合は、順位計算を含むサブクエリをfromへ置き、外側のwhereでrn範囲を指定できます。
Arelや生SQLを使う場合でも、並び替え列をユーザー入力から無制限に受け取らない設計が必要です。
既存のlimitとoffsetで十分なら無理に分析関数へ置き換えず、複雑なグループ内ページングなど必要性がある場面で採用します。
生成SQLと実行計画をログで確認し、ORMの抽象化によって不要な列や結合が増えていないかを見ます。
シナリオ8:Python での分析
Pythonから大量データを取得してから順位付けするより、Oracle側でPARTITION BYを使って必要行だけ返すと転送量を減らせる場合があります。
一方で探索的分析ではpandas側のrankと比較したいこともあるため、同点方式の既定値が一致しているか確認します。
DBのRANKとpandasの順位メソッドはオプション名や欠損値処理が異なるため、同じ語だけで同一結果とみなさないようにします。
再現可能な検証ではSQL、バインド値、取得日時、Oracleバージョンを記録して差分を追えるようにします。
シナリオ9:Java Spring での分析
Spring JDBCやJPAのnative queryでも分析関数を利用でき、DTOへ順位列をマッピングして返せます。
ページング機能と独自ROW_NUMBERを二重に適用すると条件が複雑になるため、どの層が行制限を担当するか統一します。
検索条件を動的に組む場合は値をパラメータ化し、ORDER BY列は列挙型などの安全な候補から選びます。
結合後に重複行が増えるクエリでは、分析関数を適用する前の行集合が本当に期待どおりかを先に検証します。
シナリオ10:Docker Oracle でのテスト
コンテナ環境へ最小データセットを投入すると、RANK系の同点挙動やNULL順序をチームで再現しやすくなります。
本番と異なるOracleバージョンを使う場合は構文対応やオプティマイザ差があるため、機能確認と性能評価を分けて考えます。
テストデータには同点、NULL、単一行パーティション、空に近い集合など境界条件を含めます。
SQLの期待結果を自動テストへ固定すれば、ORDER BY列変更による順位仕様の回帰を検出しやすくなります。
トラブルシューティング
ランキングSQLの不具合は、関数そのものよりORDER BYやPARTITION BYの指定不足から発生することが多くあります。
期待値と実結果が違うときは、順位の意味、対象集合、同点、NULL、最終表示順を順番に切り分けます。
障害調査では実データ全体を眺めるより、問題行と前後数行だけを再現する最小ケースを作ると、TIE・NULL・区切り漏れを短時間で切り分けられます。
ORDER BY 忘れ
順位を決める基準がないまま行番号だけ欲しい設計は再現性がなく、環境や実行計画の変化で結果が揺れやすくなります。
分析関数では業務上の並び基準をORDER BYへ明示し、ROW_NUMBERなら必要に応じて一意キーまで追加します。
最終表示も固定したい場合はSELECT文末のORDER BYが別途必要で、分析用の指定だけでは画面順を保証できません。
テストがたまたま同じ順に見えても、保証されていない順序へ依存するコードは修正対象と考えます。
NULL の位置がバラバラ
ASCとDESCを切り替えたときにNULLの位置が反転するのは、Oracleの既定NULL順序が昇降順で異なるためです。
欠損値を常に末尾へ置きたいならNULLS LASTを明示し、コードから業務意図を読み取れるようにします。
NULLを最低値として扱うのか順位対象外にするのかは別の判断なので、並べ替え指定だけで済ませない場合もあります。
複数のORDER BY列がある場合は、それぞれのNULLの意味とタイブレークへの影響を確認します。
RANK と DENSE_RANK を混同
同点後に順位が飛ぶかどうかを見落とすと、上位N条件の結果件数や表示順位が仕様とずれます。
1・2・2・4を期待するならRANK、1・2・2・3を期待するならDENSE_RANKという小さなテストを用意します。
本番データで同点が少ないと違いが表面化しないため、単体テストには必ず重複値を含めます。
関数を置換するときはSQLが通ることだけでなく、境界順位の全行が期待どおり含まれるかを比較します。
PARTITION BY 忘れで全体順位
部門別順位を期待しているのにPARTITION BYを省略すると、全行を1つの集合として会社全体の順位が計算されます。
少量の単一部門テストでは気付きにくいので、複数グループを混ぜたデータで1位が複数現れることを確認します。
逆に全体順位が欲しい処理へ誤ってPARTITION BYを追加すると、順位の比較範囲が狭くなります。
「何ごとに1へ戻るか」を日本語で説明できれば、PARTITION BY列のレビューが簡単になります。
ページング遅い
後ろのページほど遅い場合は、ROW_NUMBERの計算だけでなく全体ソートや大量OFFSETがボトルネックになっていないか確認します。
絞り込み条件で対象を減らし、必要列だけ取得し、索引と実行計画を見た上でキーセットページングも比較します。
ページサイズを小さくするだけでは前段ソート量が減らない場合があり、見かけの返却件数と処理量は別です。
性能問題を関数名のせいにせず、待機イベントやI/Oを含む実測値から改善対象を決めます。
PostgreSQL からの移行
RANK、DENSE_RANK、ROW_NUMBERの基本概念は共通でも、NULL順序や周辺の行制限構文は移行時に確認が必要です。
同じSQLが構文上動いても、既定NULL位置や実行計画の違いで結果や性能が変わる可能性があります。
ORDER BYを一意にしておけばROW_NUMBERの再現性をDB間で比較しやすくなり、差分の切り分けにも役立ちます。
移行テストでは代表データだけでなく同点、NULL、境界N、複数パーティションを含むケースを両DBで照合します。
よくある質問(FAQ)
最後に、実装時に迷いやすい点を短く確認し、関数選択とレビューの基準を固めます。
回答は「順位の意味」「同点」「結果件数」「並びの再現性」の4点へ戻すと整理しやすくなります。
迷ったときは、RANK系は順位の意味を保持し、ROW_NUMBERは行を一意に選ぶ道具だと戻って考えると、多くの設計判断を整理できます。
特に境界順位の同点データをテストへ入れておけば、関数を変更した際の意味の違いを自動的に検出しやすくなります。
Q1. RANK と DENSE_RANK の違い
どちらも同点へ同じ順位を付けますが、RANKは同点人数を反映して次の順位を飛ばし、DENSE_RANKは次の異なる値へ連続順位を付けます。
欠番が業務上の順位を表すならRANK、段階番号を詰めたいならDENSE_RANKを選びます。
同点がないデータでは結果が同じなので、関数差を確認するときは重複値を含むテストが必要です。
上位N条件の返却行数にも影響するため、表示番号だけを見て選ばないようにします。
Q2. ROW_NUMBER の TIE 順序
ORDER BYの値が同じ行に対してもROW_NUMBERは別番号を付けますが、全順序が決まらなければどちらが先になるかは不定になり得ます。
再現性が必要ならORDER BY salary DESC, employee_idのように一意キーをタイブレークとして追加します。
タイブレーク列をRANKやDENSE_RANKのORDER BYへ追加すると同順位の定義まで変わるので、目的別に使い分けます。
表示順だけ決めたい場合は分析関数外のORDER BYへ補助列を加える選択肢もあります。
Q3. どれを使うべきか
同点を競技順位として残すならRANK、同点を同じ階層にしつつ番号を詰めるならDENSE_RANK、1行ずつ選ぶならROW_NUMBERが基本です。
迷ったら「N位」と「N行」のどちらを求めているかを確認すると判断しやすくなります。
同点を含む結果件数が変動してよいかどうかも、関数選択へ直結する重要な質問です。
性能差だけで選ばず、まず意味が一致する候補を決めてから実行計画を比較します。
Q4. PARTITION BY なしは?
PARTITION BYを省略すると、問合せ結果全体を1つのパーティションとして順位を計算します。
全社ランキングや全商品ランキングなら省略が自然ですが、部門別やカテゴリ別なら区切り列が必要です。
省略してもSQLエラーにはならないため、意図しない全体順位はテストで検出する必要があります。
各グループで1位が存在することを期待結果へ含めると、PARTITION BY漏れに気付きやすくなります。
Q5. NULLS FIRST/LAST
分析ORDER BYでは昇順の既定がNULLS LAST、降順の既定がNULLS FIRSTです。
欠損を常に最後へ置くなど業務ルールがあるなら、既定に任せずNULLS LASTを明示します。
NULLを順位対象へ含めること自体が不適切なら、WHEREで除外する設計も候補です。
昇降順の変更時にNULL位置が意図せず変わらないかをレビュー項目に加えます。
Q6. Top N の書き方
全体Top NはFETCH FIRSTやROW_NUMBER、同点を含む順位Top NはRANK、上位N種類の値はDENSE_RANKという整理ができます。
グループ別Top NはPARTITION BY付きの分析関数を使い、外側の問合せで順位条件を適用する形が分かりやすいです。
N件とN位は同じではないため、要件書では期待するサンプル件数を示します。
境界に同点があるテストデータを用意すると、誤った関数選択を早期に発見できます。
Q7. WITH TIES
FETCH FIRST n ROWS WITH TIESは境界のソートキーと同じ行を追加で返す行制限機能です。
ORDER BYが必要で、同点数によって指定行数より多く返る点はRANKによるTop Nと似ています。
ただしPARTITION BYごとのTop Nをそのまま表す機能ではないため、グループ内順位には分析関数を使います。
既存SQLを置換する場合はNULL順序と複数ソートキーを含めて結果の同一性を検証します。
Q8. Rails での使用
Railsではselectへwindow関数式を追加し、サブクエリやCTEを経由して計算済み順位を絞り込めます。
動的ORDER BYは許可列の一覧から選び、ユーザー文字列をそのままSQLへ連結しません。
順位列は通常のモデル永続属性ではないため、DTO的な読み取り値として扱います。
簡単なlimitとoffsetで十分な要件なら、可読性を優先して分析関数を使わない判断も有効です。
Q9. Java での使用
JDBCでは通常のSELECTとして実行でき、分析関数の結果列をResultSetから取得できます。
条件値はPreparedStatementへバインドし、列名など識別子は安全な候補へ制限します。
Spring Dataを使う場合もnative queryとページング機構の責任範囲を重ねないようにします。
DBの順位結果をアプリで再ソートすると意味が分かりにくくなるため、表示順が必要ならSQL側の最終ORDER BYを明示します。
Q10. Python での使用
python-oracledbでは分析関数を含むSQLをそのまま実行し、順位列を通常のSELECT列として取得できます。
値はバインド変数へ渡し、ORDER BY列の切替は事前定義したマッピングで安全に実装します。
pandasへ読み込んで再順位付けする場合は、同点処理とNULL処理の違いを比較してから使います。
DB側でTop Nまで絞れるなら、アプリへ全件転送するよりネットワーク負荷を抑えられる場合があります。
Q11. パフォーマンス
どの順位関数が常に最速という単純な結論はなく、入力行数、ソート、結合、索引、統計情報で実行コストが変わります。
実行計画と実測統計を取得し、WINDOW SORTやTEMP使用量など具体的なボトルネックを確認します。
単純な全体Top NではFETCH FIRSTが適することもありますが、意味が変わらないことを先に確かめます。
チューニング後は結果件数と順位値も回帰テストし、性能改善で仕様を壊していないか確認します。
Q12. 予防策
同点、NULL、複数パーティション、境界N、一意でないORDER BYを含むテストケースを用意するのが最も有効な予防策です。
SQLレビューではPARTITION BY単位、順位基準、タイブレーク、最終ORDER BY、期待行数をチェックします。
ROW_NUMBERで代表1行を選ぶ処理は、選定基準をコメントやテスト名へ残して将来の変更理由を追えるようにします。
本番で順位不整合が起きたときに再現できるよう、主要SQLのバージョンと実行条件を管理しておくと復旧が速くなります。