【完全比較】Oracle RANK / DENSE_RANK / ROW_NUMBER の違いと使い分け完全ガイド

【完全比較】Oracle RANK / DENSE_RANK / ROW_NUMBER の違いと使い分け完全ガイド

Oracle SQL 開発者がランキング処理で必ず使う 3 大分析関数、それが RANK / DENSE_RANK / ROW_NUMBER です:

SELECT ename, deptno, sal,
  RANK()       OVER (PARTITION BY deptno ORDER BY sal DESC) AS rnk,
  DENSE_RANK() OVER (PARTITION BY deptno ORDER BY sal DESC) AS dense_rnk,
  ROW_NUMBER() OVER (PARTITION BY deptno ORDER BY sal DESC) AS row_num
FROM emp;

Oracle 8i (1999) から提供される最頻用の分析関数群。しかし、3 つの違いを厳密に理解している開発者は少ない:

  • RANK(): 同順位に同じランク、次はスキップ(1, 2, 2, 4, 5)
  • DENSE_RANK(): 同順位に同じランク、次も連続(1, 2, 2, 3, 4)
  • ROW_NUMBER(): 必ず一意な連番、TIE は任意順序(1, 2, 3, 4, 5)

このセットの本質は、Oracle 公式 Database SQL Language Reference が定義:

「RANK 系は、ORDER BY 条件でソートされた行に順位を付与する分析関数。
  同順位の扱いが 3 関数で異なり、
  用途に応じて厳密に使い分ける必要がある」
目次

3 関数の違いを 1 表で

サラリー 5000, 3000, 3000, 2500 の 4 人の場合:

Sal   RANK  DENSE_RANK  ROW_NUMBER
----  ----  ----------  ----------
5000    1        1          1
3000    2        2          2       ← TIE
3000    2        2          3       ← TIE
2500    4        3          4
      ↑      ↑           ↑
    3 なし  連続的     必ず一意

具体的な使い分け

RANK: 「同順位はまとめて、次は飛ばす」オリンピック方式

-- 給与が高い上位 3 位(TIE 含む可能性あり)
SELECT ename, sal FROM (
  SELECT ename, sal, RANK() OVER (ORDER BY sal DESC) AS r FROM emp
) WHERE r <= 3;
-- 2 位が 2 人なら 4 人返る(3 位なし)

DENSE_RANK: 「同順位はまとめて、次は連続」順位表方式

-- 上位 3 位のサラリーの人(TIE 含む)
SELECT ename, sal FROM (
  SELECT ename, sal, DENSE_RANK() OVER (ORDER BY sal DESC) AS r FROM emp
) WHERE r <= 3;
-- 同順位含めて広く抽出

ROW_NUMBER: 「必ず一意な連番」ページング方式

-- ページング(10 行ごと)
SELECT * FROM (
  SELECT e.*, ROW_NUMBER() OVER (ORDER BY sal DESC) AS rn FROM emp e
) WHERE rn BETWEEN 11 AND 20;

典型的な用途

現場で最頻出の使い分け:

【ROW_NUMBER】
- ページング(OFFSET-FETCH 代替)
- 重複排除(DISTINCT より柔軟)
- Top N per group(大多数のケース)
- ユニーク行番号必要な処理

【RANK】
- 「上位 N 位」(オリンピック方式)
- スコアランキング(同点は同順位、次は飛ぶ)
- 順位が「離散的」でよい

【DENSE_RANK】
- Top N distinct values
- 順位表(同点は同順位、次は詰める)
- パーセンタイル計算
- 分位分割の前処理

現場で使う分析関数の主要機能:

  • OVER (PARTITION BY ... ORDER BY ...) の完全構文
  • NULLS FIRST / NULLS LAST による NULL 処理
  • Top N per group(部門別給与 Top 3 等、超典型)
  • ページング (ROW_NUMBER)
  • 重複排除 (ROW_NUMBER + PARTITION BY)
  • 集計関数版 (RANK(...) WITHIN GROUP)
  • 姉妹関数: NTILE, PERCENT_RANK, CUME_DIST
  • FIRST_VALUE / LAST_VALUE による参照
  • LAG / LEAD による前後行参照
  • パフォーマンス(適切なインデックス)
  • WITH TIES (12c+ FETCH 文の類似機能)

多くの日本語記事が「3 つは似ている」で終わりますが、実務では:

  • TIE の扱いによる結果件数の違い
  • PARTITION BY によるグループ内順位
  • NULLS FIRST/LAST の明示的指定の重要性
  • ROW_NUMBER + PARTITION BY による重複排除の定番パターン
  • ROW_NUMBER + サブクエリによるページング
  • DENSE_RANK によるMedian 計算
  • RANK の集計関数版による特定値の順位取得
  • NTILE による分位分割
  • PERCENT_RANK / CUME_DIST によるパーセンタイル
  • Rails / Java / Python アプリでの分析関数活用
  • OFFSET-FETCH (12c+) との性能比較

さらに、Oracle の 3 関数の本質的違いを数式で表現:

RANK(r)       = 「r より小さい行の数」 + 1
DENSE_RANK(r) = 「r より小さい ORDER BY 値の distinct 数」 + 1
ROW_NUMBER(r) = 「ORDER BY 順の一意な連番」(TIE は不定)

本記事では、Oracle RANK / DENSE_RANK / ROW_NUMBER の全てを実務レベルで完全比較・網羅。基本構文、TIE 処理の違い、OVER 句、10 大実用例、姉妹関数、パフォーマンス、Rails/Java/Python 統合、実践シナリオ、FAQまで。この1本で 3 つの分析関数をプロフェッショナルに使い分けられるようになります。


結論:TIE 処理と用途で選ぶ

時間がない方向けに、最速の使い分けを先に示します。

用途別チャート

一意な行番号が必要?
├── YES → ROW_NUMBER()
│   └── ページング、重複排除、Top N(TIE 気にしない)
└── NO
    ├── 同順位の後を「スキップ」する?
    │   └── YES → RANK()  ← オリンピック方式
    └── 同順位の後も「連続」する?
        └── YES → DENSE_RANK()  ← 順位表方式

3 関数の結果比較(1 行で理解)

Sal     RANK    DENSE_RANK  ROW_NUMBER
----    ----    ----------  ----------
5000      1         1           1
3000      2         2           2
3000      2         2           3
2500      4         3           4
2500      4         3           5
1000      6         4           6

基本構文

FUNCTION() OVER (
  [PARTITION BY partition_col]
  ORDER BY sort_col [ASC|DESC] [NULLS FIRST|LAST]
)

6つの必須知識

#トピック概要
①RANKTIE で GAP あり
②DENSE_RANKTIE で GAP なし
③ROW_NUMBER一意な連番
④PARTITION BYグループ内順位
⑤NULLS FIRST/LASTNULL 位置制御
⑥Top N per group定番パターン

詳細は以下で解説します。


3 関数の動作詳解

サンプルテーブル(emp)

CREATE TABLE emp (
  empno   NUMBER PRIMARY KEY,
  ename   VARCHAR2(50),
  deptno  NUMBER,
  sal     NUMBER
);

INSERT INTO emp VALUES (7839, 'KING',   10, 5000);
INSERT INTO emp VALUES (7782, 'CLARK',  10, 2450);
INSERT INTO emp VALUES (7934, 'MILLER', 10, 1300);
INSERT INTO emp VALUES (7788, 'SCOTT',  20, 3000);
INSERT INTO emp VALUES (7902, 'FORD',   20, 3000);   -- TIE
INSERT INTO emp VALUES (7566, 'JONES',  20, 2975);
INSERT INTO emp VALUES (7876, 'ADAMS',  20, 1100);
INSERT INTO emp VALUES (7369, 'SMITH',  20,  800);
COMMIT;

3 関数同時実行

SELECT deptno, ename, sal,
  RANK()       OVER (PARTITION BY deptno ORDER BY sal DESC) AS rnk,
  DENSE_RANK() OVER (PARTITION BY deptno ORDER BY sal DESC) AS d_rnk,
  ROW_NUMBER() OVER (PARTITION BY deptno ORDER BY sal DESC) AS rn
FROM emp
ORDER BY deptno, sal DESC;

-- 結果:
-- DEPTNO ENAME     SAL    RNK  D_RNK   RN
-- ------ -------  ----   ----  -----  ---
--     10 KING     5000     1     1     1
--     10 CLARK    2450     2     2     2
--     10 MILLER   1300     3     3     3
--     20 SCOTT    3000     1     1     1  ← TIE 開始
--     20 FORD     3000     1     1     2  ← TIE、ROW_NUMBER は連続
--     20 JONES    2975     3     2     3  ← RANK は 2 を飛ばして 3
--     20 ADAMS    1100     4     3     4     DENSE_RANK は 2
--     20 SMITH     800     5     4     5

3 関数の本質的違い

RANK():

「自分より上位の行数 + 1」
5000: 上位 0 行 → 1
3000, 3000: 上位 1 行 → 2
2975: 上位 3 行 → 4  ← 「上位」を「同順位含む」でカウント

DENSE_RANK():

「自分より上位の distinct 値の数 + 1」
5000: 上位 distinct 0 → 1
3000: 上位 distinct 1 → 2
2975: 上位 distinct 2 → 3  ← distinct でカウント

ROW_NUMBER():

「必ず一意な連番」
5000: 1
3000: 2
3000: 3  ← TIE でも一意
2975: 4

OVER 句の完全解説

PARTITION BY

グループ内順位:

-- 部門別の給与順位
RANK() OVER (PARTITION BY deptno ORDER BY sal DESC)
-- 各部門で独立して 1 位からカウント

なし = 全体順位:

-- 全社の給与順位
RANK() OVER (ORDER BY sal DESC)

ORDER BY

ソート順で順位付け:

-- 降順(高い方が 1 位)
RANK() OVER (ORDER BY sal DESC)

-- 昇順(低い方が 1 位)
RANK() OVER (ORDER BY sal ASC)

複数列:

-- 部門優先、次に給与
RANK() OVER (ORDER BY deptno, sal DESC)

NULLS FIRST / NULLS LAST

NULL の位置制御(明示推奨):

-- NULL を最初に
ORDER BY sal DESC NULLS FIRST

-- NULL を最後に
ORDER BY sal DESC NULLS LAST

-- ⚠️ デフォルト:
-- DESC → NULLS FIRST
-- ASC  → NULLS LAST

PARTITION BY + ORDER BY

最も一般的:

SELECT deptno, ename, sal,
  ROW_NUMBER() OVER (
    PARTITION BY deptno       -- 部門ごとに
    ORDER BY sal DESC          -- 給与順
       NULLS LAST              -- NULL は最後
  ) AS rn
FROM emp;

実用例10選

例1: 部門別給与 Top 3

RANK 版(TIE 含む Top 3 位):

SELECT deptno, ename, sal, r
FROM (
  SELECT deptno, ename, sal,
    RANK() OVER (PARTITION BY deptno ORDER BY sal DESC) AS r
  FROM emp
)
WHERE r <= 3;

DENSE_RANK 版(Top 3 位の値):

SELECT deptno, ename, sal, dr
FROM (
  SELECT deptno, ename, sal,
    DENSE_RANK() OVER (PARTITION BY deptno ORDER BY sal DESC) AS dr
  FROM emp
)
WHERE dr <= 3;

ROW_NUMBER 版(正確に 3 行):

SELECT deptno, ename, sal, rn
FROM (
  SELECT deptno, ename, sal,
    ROW_NUMBER() OVER (PARTITION BY deptno ORDER BY sal DESC) AS rn
  FROM emp
)
WHERE rn <= 3;

例2: ページング(OFFSET-FETCH 代替)

-- 11-20 行目を取得
SELECT * FROM (
  SELECT e.*,
    ROW_NUMBER() OVER (ORDER BY sal DESC) AS rn
  FROM emp e
)
WHERE rn BETWEEN 11 AND 20;

-- 12c+ の OFFSET-FETCH との比較
SELECT * FROM emp
ORDER BY sal DESC
OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;

例3: 重複排除(超典型パターン)

-- email 重複を最新 created_at で 1 行だけ残す
DELETE FROM users
WHERE ROWID IN (
  SELECT ROWID FROM (
    SELECT ROWID,
      ROW_NUMBER() OVER (
        PARTITION BY email 
        ORDER BY created_at DESC
      ) AS rn
    FROM users
  )
  WHERE rn > 1
);

例4: 各部門の最高給与者

SELECT deptno, ename, sal
FROM (
  SELECT deptno, ename, sal,
    ROW_NUMBER() OVER (
      PARTITION BY deptno ORDER BY sal DESC
    ) AS rn
  FROM emp
)
WHERE rn = 1;

例5: Median(中央値)計算

SELECT AVG(sal) AS median_sal
FROM (
  SELECT sal,
    DENSE_RANK() OVER (ORDER BY sal) AS r_asc,
    DENSE_RANK() OVER (ORDER BY sal DESC) AS r_desc
  FROM emp
)
WHERE r_asc = r_desc OR r_asc + 1 = r_desc OR r_asc = r_desc + 1;

-- または Oracle の MEDIAN 関数
SELECT MEDIAN(sal) FROM emp;

例6: N 番目の値

-- 部門別 2 番目に高い給与
SELECT deptno, sal
FROM (
  SELECT deptno, sal,
    DENSE_RANK() OVER (PARTITION BY deptno ORDER BY sal DESC) AS r
  FROM emp
)
WHERE r = 2;

例7: パーセンタイル分位分割

-- 給与を 4 分位に分割
SELECT deptno, ename, sal,
  NTILE(4) OVER (ORDER BY sal DESC) AS quartile
FROM emp;

-- 上位 25% のみ
SELECT deptno, ename, sal
FROM (
  SELECT deptno, ename, sal,
    NTILE(4) OVER (ORDER BY sal DESC) AS quartile
  FROM emp
)
WHERE quartile = 1;

例8: 累積・変化(LAG/LEAD)

SELECT deptno, ename, sal,
  LAG(sal) OVER (PARTITION BY deptno ORDER BY sal) AS prev_sal,
  sal - LAG(sal) OVER (PARTITION BY deptno ORDER BY sal) AS diff,
  LEAD(sal) OVER (PARTITION BY deptno ORDER BY sal) AS next_sal
FROM emp;

例9: 最初/最後の値(FIRST_VALUE/LAST_VALUE)

SELECT deptno, ename, sal,
  FIRST_VALUE(ename) OVER (
    PARTITION BY deptno ORDER BY sal DESC
  ) AS top_earner,
  LAST_VALUE(ename) OVER (
    PARTITION BY deptno ORDER BY sal DESC
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  ) AS bottom_earner
FROM emp;

例10: 集計関数版 RANK

-- 特定給与の順位取得
SELECT RANK(3000) WITHIN GROUP (ORDER BY sal DESC) AS rank_of_3000
FROM emp;
-- 「3000 だったらこの順位」

SELECT DENSE_RANK(3000) WITHIN GROUP (ORDER BY sal DESC) AS d_rank_of_3000
FROM emp;

姉妹分析関数

NTILE(分位分割)

-- 4 分割
NTILE(4) OVER (ORDER BY sal DESC)
-- 1: 上位 25%
-- 2: 25-50%
-- 3: 50-75%
-- 4: 下位 25%

PERCENT_RANK(0-1 の比率)

PERCENT_RANK() OVER (ORDER BY sal DESC)
-- 最高: 0
-- 最低: 1
-- 中間: 比率

CUME_DIST(累積分布)

CUME_DIST() OVER (ORDER BY sal DESC)
-- 累積確率

FIRST_VALUE / LAST_VALUE

FIRST_VALUE(sal) OVER (PARTITION BY deptno ORDER BY hire_date)
-- パーティション内の最初の値

LAG / LEAD

LAG(sal, 1, 0) OVER (ORDER BY hire_date)   -- 1 行前、なければ 0
LEAD(sal, 1, 0) OVER (ORDER BY hire_date)  -- 1 行後

ROW_NUMBER と一緒に使うパターン

-- 前後行との差分
SELECT sal, 
  sal - LAG(sal, 1, 0) OVER (ORDER BY sal) AS diff_from_prev,
  ROW_NUMBER() OVER (ORDER BY sal) AS rn
FROM emp;

パフォーマンス最適化

インデックス

-- ORDER BY と PARTITION BY 列にインデックス
CREATE INDEX idx_emp_dept_sal ON emp(deptno, sal DESC);

-- 分析関数は Sort Operation を回避可

実行計画確認

EXPLAIN PLAN FOR
SELECT ename, sal,
  ROW_NUMBER() OVER (ORDER BY sal DESC) AS rn
FROM emp;

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

-- WINDOW SORT が現れる
-- インデックス活用で SORT 回避可

実行計画関連は Oracle EXPLAIN PLAN 完全ガイドの記事も参照してください。

Top N の効率

ROW_NUMBER でサブクエリ:

SELECT * FROM (
  SELECT e.*, ROW_NUMBER() OVER (ORDER BY sal DESC) AS rn FROM emp e
) WHERE rn <= 10;

12c+ FETCH FIRST:

SELECT * FROM emp ORDER BY sal DESC FETCH FIRST 10 ROWS ONLY;

FETCH FIRST の方が最適化される傾向。

WITH TIES(12c+)

-- TIE 含む Top 3
SELECT * FROM emp
ORDER BY sal DESC
FETCH FIRST 3 ROWS WITH TIES;

-- RANK <= 3 と類似の効果

Rails / Java / Python 対応

Rails ActiveRecord

class Employee < ApplicationRecord
  # 部門別 Top N
  def self.top_n_per_dept(n)
    connection.select_all(<<~SQL).to_a
      SELECT * FROM (
        SELECT e.*,
          ROW_NUMBER() OVER (
            PARTITION BY deptno ORDER BY sal DESC
          ) AS rn
        FROM employees e
      )
      WHERE rn <= #{n.to_i}
    SQL
  end
  
  # ページング
  def self.paginated(page, per_page)
    offset = (page - 1) * per_page
    connection.select_all(<<~SQL).to_a
      SELECT * FROM (
        SELECT e.*,
          ROW_NUMBER() OVER (ORDER BY id) AS rn
        FROM employees e
      )
      WHERE rn BETWEEN #{offset + 1} AND #{offset + per_page}
    SQL
  end
  
  # 重複排除
  def self.remove_duplicates_by_email
    connection.execute(<<~SQL)
      DELETE FROM employees WHERE ROWID IN (
        SELECT ROWID FROM (
          SELECT ROWID,
            ROW_NUMBER() OVER (
              PARTITION BY email ORDER BY created_at DESC
            ) AS rn
          FROM employees
        )
        WHERE rn > 1
      )
    SQL
  end
end

Rails 8 系の詳細は Rails 8 アップグレードガイドの記事、find/find_by/where の記事も参照してください。

Java (JDBC)

public List<Employee> getTopNPerDept(Connection conn, int n) 
    throws SQLException {
  String sql = """
      SELECT * FROM (
        SELECT e.*,
          ROW_NUMBER() OVER (
            PARTITION BY deptno ORDER BY sal DESC
          ) AS rn
        FROM employees e
      )
      WHERE rn <= ?
      """;
  try (PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.setInt(1, n);
    try (ResultSet rs = ps.executeQuery()) {
      List<Employee> result = new ArrayList<>();
      while (rs.next()) {
        result.add(mapEmployee(rs));
      }
      return result;
    }
  }
}

Python (oracledb)

import oracledb

def top_n_per_dept(conn, n):
    cursor = conn.cursor()
    cursor.execute("""
        SELECT deptno, ename, sal, rn FROM (
          SELECT deptno, ename, sal,
            ROW_NUMBER() OVER (
              PARTITION BY deptno ORDER BY sal DESC
            ) AS rn
          FROM emp
        )
        WHERE rn <= :n
    """, [n])
    return cursor.fetchall()

def paginate(conn, table, page, per_page, order_by='id'):
    offset = (page - 1) * per_page
    cursor = conn.cursor()
    cursor.execute(f"""
        SELECT * FROM (
          SELECT t.*, ROW_NUMBER() OVER (ORDER BY {order_by}) AS rn
          FROM {table} t
        )
        WHERE rn BETWEEN :1 AND :2
    """, [offset + 1, offset + per_page])
    return cursor.fetchall()

実践シナリオ

シナリオ1:スコアランキング(オリンピック方式)

-- ゲームスコアの順位(同点は同順位、次は飛ぶ)
SELECT player_name, score,
  RANK() OVER (ORDER BY score DESC) AS ranking
FROM game_scores;

-- 上位 10 位(TIE 含む可能性)
SELECT * FROM (
  SELECT player_name, score,
    RANK() OVER (ORDER BY score DESC) AS ranking
  FROM game_scores
)
WHERE ranking <= 10;

シナリオ2:EC 売上ランキング(順位表方式)

-- 上位 5 種類の売上商品(同売上は同順位、次は詰める)
SELECT product_name, total_sales, dr
FROM (
  SELECT product_name, total_sales,
    DENSE_RANK() OVER (ORDER BY total_sales DESC) AS dr
  FROM sales_summary
)
WHERE dr <= 5;

シナリオ3:Web ページング

-- 第 3 ページ(1 ページ 20 件)
SELECT * FROM (
  SELECT p.*, ROW_NUMBER() OVER (ORDER BY posted_at DESC) AS rn
  FROM posts p
)
WHERE rn BETWEEN 41 AND 60;

-- 12c+ の書き方
SELECT * FROM posts
ORDER BY posted_at DESC
OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY;

シナリオ4:重複行の削除

-- users テーブルで email 重複、最新の 1 件だけ残す
DELETE FROM users
WHERE ROWID IN (
  SELECT ROWID FROM (
    SELECT ROWID,
      ROW_NUMBER() OVER (
        PARTITION BY email ORDER BY created_at DESC
      ) AS rn
    FROM users
  )
  WHERE rn > 1
);
COMMIT;

シナリオ5:部門別 Top 3 給与

-- 各部門の給与 Top 3(TIE 含む)
SELECT * FROM (
  SELECT deptno, ename, sal,
    DENSE_RANK() OVER (
      PARTITION BY deptno ORDER BY sal DESC
    ) AS dr
  FROM emp
)
WHERE dr <= 3
ORDER BY deptno, sal DESC;

シナリオ6:各カテゴリの最新商品

-- カテゴリ別に最新の 1 商品
SELECT category_id, product_name, created_at
FROM (
  SELECT p.*,
    ROW_NUMBER() OVER (
      PARTITION BY category_id ORDER BY created_at DESC
    ) AS rn
  FROM products p
)
WHERE rn = 1;

シナリオ7:Rails ページング実装

class PostsController < ApplicationController
  def index
    page = params[:page].to_i.clamp(1, 1000)
    per_page = 20
    
    @posts = Post.paginated(page, per_page)
    @total = Post.count
    @total_pages = (@total.to_f / per_page).ceil
  end
end

シナリオ8:Python での分析

import oracledb
import pandas as pd

def analyze_top_earners(conn):
    query = """
        SELECT deptno, ename, sal, rnk, dr, rn FROM (
          SELECT deptno, ename, sal,
            RANK()       OVER (PARTITION BY deptno ORDER BY sal DESC) AS rnk,
            DENSE_RANK() OVER (PARTITION BY deptno ORDER BY sal DESC) AS dr,
            ROW_NUMBER() OVER (PARTITION BY deptno ORDER BY sal DESC) AS rn
          FROM emp
        )
        WHERE rnk <= 3
        ORDER BY deptno, sal DESC
    """
    cursor = conn.cursor()
    cursor.execute(query)
    columns = [d[0] for d in cursor.description]
    df = pd.DataFrame(cursor.fetchall(), columns=columns)
    return df

シナリオ9:Java Spring での分析

@Repository
public class SalesRankingRepository {
    @PersistenceContext
    private EntityManager em;
    
    public List<Object[]> getTopSellingByRegion(int topN) {
        String sql = """
            SELECT * FROM (
              SELECT region, product, sales,
                DENSE_RANK() OVER (
                  PARTITION BY region ORDER BY sales DESC
                ) AS dr
              FROM sales_summary
            )
            WHERE dr <= ?1
            """;
        return em.createNativeQuery(sql)
                 .setParameter(1, topN)
                 .getResultList();
    }
}

シナリオ10:Docker Oracle でのテスト

docker exec -it oracle-xe sqlplus scott/tiger <<EOF
CREATE TABLE scores (name VARCHAR2(20), score NUMBER);
INSERT INTO scores VALUES ('Alice', 95);
INSERT INTO scores VALUES ('Bob',   90);
INSERT INTO scores VALUES ('Carol', 90);   -- TIE
INSERT INTO scores VALUES ('Dave',  85);
INSERT INTO scores VALUES ('Eve',   80);
COMMIT;

-- 3 関数比較
SELECT name, score,
  RANK()       OVER (ORDER BY score DESC) AS rnk,
  DENSE_RANK() OVER (ORDER BY score DESC) AS dr,
  ROW_NUMBER() OVER (ORDER BY score DESC) AS rn
FROM scores;

-- 結果:
-- NAME  SCORE  RNK  DR  RN
-- Alice   95    1   1   1
-- Bob     90    2   2   2
-- Carol   90    2   2   3
-- Dave    85    4   3   4  ← RANK は 3 飛ばし、DR は連続
-- Eve     80    5   4   5
EOF

Docker 関連は docker daemon 接続エラーの記事、Docker no space left on device の記事も参照してください。


トラブルシューティング

ORDER BY 忘れ

ORA-30485: missing ORDER BY expression in the window specification

分析関数は ORDER BY 必須。

NULL の位置がバラバラ

明示指定推奨:

ORDER BY sal DESC NULLS LAST

RANK と DENSE_RANK を混同

用途で厳密に選ぶ:

  • 「上位 N 位」→ RANK
  • 「上位 N の値」→ DENSE_RANK
  • 「正確に N 行」→ ROW_NUMBER

PARTITION BY 忘れで全体順位

部門別なら PARTITION BY 必須:

PARTITION BY deptno ORDER BY sal DESC

ページング遅い

インデックス + Top N 最適化:

-- 12c+
FETCH FIRST 20 ROWS ONLY

PostgreSQL からの移行

PG も同じ 3 関数、構文互換性高い。


よくある質問(FAQ)

Q1. RANK と DENSE_RANK の違い

  • RANK: TIE で GAP、5000, 3000, 3000, 2500 → 1, 2, 2, 4
  • DENSE_RANK: GAP なし → 1, 2, 2, 3

Q2. ROW_NUMBER の TIE 順序

不定、明示的にソートしないと再現不可。

Q3. どれを使うべきか

  • 一意番号: ROW_NUMBER
  • 順位(オリンピック): RANK
  • 順位(順位表): DENSE_RANK

Q4. PARTITION BY なしは?

全体を 1 グループとして扱う。

Q5. NULLS FIRST/LAST

明示推奨、デフォルトは DESC/ASC で異なる。

Q6. Top N の書き方

  • ROW_NUMBER + サブクエリ: 全 Oracle バージョン
  • FETCH FIRST N ROWS ONLY: 12c+

Q7. WITH TIES

RANK と同等、FETCH FIRST に付ける。

Q8. Rails での使用

raw SQL + connection.select_all。

Q9. Java での使用

PreparedStatement + Native SQL。

Q10. Python での使用

oracledb.cursor.execute。

Q11. パフォーマンス

インデックス設計 + 12c+ FETCH FIRST。

Q12. 予防策

  • ORDER BY 必須
  • NULLS FIRST/LAST 明示
  • PARTITION BY 忘れずに
  • 用途に応じて 3 つを使い分け

参考リンク

Oracle 公式


まとめ

Oracle RANK / DENSE_RANK / ROW_NUMBER の要点を再整理します。

3 関数の本質

RANK()       - TIE で GAP あり  (1, 2, 2, 4, 5)
DENSE_RANK() - TIE で GAP なし  (1, 2, 2, 3, 4)
ROW_NUMBER() - 必ず一意な連番    (1, 2, 3, 4, 5)

結果比較表

SalRANKDENSE_RANKROW_NUMBER
5000111
3000222
3000223
2500434
2500435
1000646

用途別使い分け

用途関数
ページングROW_NUMBER
重複排除ROW_NUMBER
上位 N 位(オリンピック)RANK
上位 N の値(順位表)DENSE_RANK
各グループ 1 行ROW_NUMBER
Median 計算DENSE_RANK
N 番目の値DENSE_RANK

基本構文

FUNCTION() OVER (
  PARTITION BY partition_col     -- グループ内順位
  ORDER BY sort_col DESC          -- ソート順
     NULLS LAST                   -- NULL 位置
)

定番パターン

-- Top N per group
SELECT * FROM (
  SELECT t.*, ROW_NUMBER() OVER (
    PARTITION BY group_col ORDER BY sort_col DESC
  ) AS rn FROM tab t
) WHERE rn <= N;

-- ページング
SELECT * FROM (
  SELECT t.*, ROW_NUMBER() OVER (ORDER BY id) AS rn FROM tab t
) WHERE rn BETWEEN start_row AND end_row;

-- 重複排除
DELETE FROM tab WHERE ROWID IN (
  SELECT ROWID FROM (
    SELECT ROWID, ROW_NUMBER() OVER (
      PARTITION BY dup_col ORDER BY created_at DESC
    ) AS rn FROM tab
  ) WHERE rn > 1
);

姉妹関数

関数用途
NTILE(N)N 分位分割
PERCENT_RANK0-1 の比率
CUME_DIST累積分布
FIRST_VALUE最初の値
LAST_VALUE最後の値
LAG前行参照
LEAD後行参照

実務チェックリスト

□ 用途に応じて 3 関数を厳密に使い分け
□ PARTITION BY で グループ内順位
□ ORDER BY 必須
□ NULLS FIRST/LAST 明示
□ Top N per group は ROW_NUMBER + サブクエリ
□ ページングは ROW_NUMBER or FETCH FIRST
□ 重複排除は ROW_NUMBER + PARTITION BY
□ 12c+ は FETCH FIRST WITH TIES 検討
□ インデックスで SORT 回避
□ Rails/Java/Python は raw SQL

これらの知識は、Oracle での SQL 開発・レポート生成・データ分析・ページング・重複排除・BI ツール・Rails/Java/Python アプリ開発など、あらゆる場面で活用できます。本記事をブックマークしておけば、Oracle の 3 大分析関数をプロフェッショナルとして使い分けられるようになります。


本記事は2026年6月時点の情報をもとに、Oracle Database 12c〜23ai での動作確認・公式ドキュメントに基づき作成しています。Oracle のバージョンにより挙動が異なる場合があるため、最新の情報は Oracle 公式ドキュメント(docs.oracle.com)もあわせてご確認ください。