Loading

ROUTE

ルートゼロの
アクティビティ

SQLのRANK・DENSE_RANK・ROW_NUMBERの違い|同点時の順位

2

SQLで順位を付けたら「1位、1位、3位」になったり、同点なのに番号が分かれたりしませんか。RANK・DENSE_RANK・ROW_NUMBERは、同点をどう扱うかが違います。まず結果の違いを見てから、手元で確かめましょう。

結論:同点をどう扱いたいかで選ぶ

3つともSQLのウィンドウ関数です。行をまとめて消さず、並べたデータの各行に順位や番号を付けられます。

関数 同点の扱い 4行の例(点数90,90,80,70) 選ぶ場面
ROW_NUMBER() 毎行に異なる連番 1,2,3,4 一覧へ1行ずつ番号を振りたい
RANK() 同点は同順位/次の順位は飛ぶ 1,1,3,4 競技の順位のように欠番を残したい
DENSE_RANK() 同点は同順位/次の順位は詰める 1,1,2,3 異なる点数に1位、2位と番号を振りたい

用語解説:ウィンドウ関数
ある行を残したまま、その行と関連する複数行の情報を使って計算するSQLの関数。3つの順位関数は `OVER (ORDER BY …)` の並べ方に従います。

たとえば90点が2人いるとき、`RANK()`は両者を1位とし、次の80点は3位です。`DENSE_RANK()`では80点は2位です。


4人のデータで結果を再現する

次のSQLはPostgreSQLとSQLiteで試せる構成です。`WITH … VALUES`で練習用データをその場に用意するため、テーブルを作成しなくても結果を確認できます。

WITH scores(id, name, points) AS (
  VALUES (1, '佐藤', 90), (2, '鈴木', 90),
         (3, '田中', 80), (4, '高橋', 70)
)
SELECT
  name, points,
  ROW_NUMBER() OVER (ORDER BY points DESC, id) AS row_no,
  RANK() OVER (ORDER BY points DESC) AS rank_no,
  DENSE_RANK() OVER (ORDER BY points DESC) AS dense_no
FROM scores
ORDER BY points DESC, id;
名前 点数 row_no rank_no dense_no
佐藤 90 1 1 1
鈴木 90 2 1 1
田中 80 3 3 2
高橋 70 4 4 3

`DESC`は大きい値から並べる指定です。`id`は同点の2人を安定した順番に並べるため、`ROW_NUMBER()`だけに加えています。ここが重要です。


ROW_NUMBERだけidを加える理由

`ROW_NUMBER()`は同じ点数の行にも別々の番号を付けます。`ORDER BY points DESC`だけだと、同点のどちらを先にするか決める情報が足りません。番号を再現可能にしたいときは、一意の`id`などを追加します。

ただし、`RANK()`や`DENSE_RANK()`にも同じ`id`を追加すると、点数が同じでもSQLからは異なる並び順の値として扱われ、同順位になりません。同点の基準は`points`だけにするのが今回の目的です。

用語解説:同順位(peer)
`OVER (ORDER BY …)`に指定した並べ替え値が同じ行のこと。RANKとDENSE_RANKでは同順位を共有します。

(ROW_NUMBERによる重複・並びの整理については『SQL重複除外のベストプラクティス徹底比較』をご参照ください)


「上位2位」を抽出するときの注意点

`ROW_NUMBER()`の「先頭2行」は常に2行ですが、`RANK()`の「2位以内」は同順位を含むため、必ずしも2行ではありません。サンプルでは90点の2人だけが2位以内です。

ウィンドウ関数を計算した列名は、通常同じ`SELECT`の`WHERE`で直接使えません。絞り込む場合は、いったんCTE(`WITH`で名前を付けた一時的な問い合わせ結果)やサブクエリ(SQLの中に入れた別の問い合わせ)で順位を付け、その外側で条件を指定します。

WITH scores(id, name, points) AS (
  VALUES (1, '佐藤', 90), (2, '鈴木', 90),
         (3, '田中', 80), (4, '高橋', 70)
), ranked AS (
  SELECT name, points,
         RANK() OVER (ORDER BY points DESC) AS rank_no
  FROM scores
)
SELECT name, points, rank_no
FROM ranked
WHERE rank_no <= 2
ORDER BY points DESC, name;

この例では90点の2行が返ります。`RANK()`の次は3位なので、80点は含まれません。

(ROW_NUMBERを使った最新データ取得の考え方については『MySQLのROW_NUMBER()実践ガイド』をご参照ください)

結果が想定と違うときの確認順

  1. `OVER (ORDER BY …)`に、順位の基準となる列が入っているか。最後の`ORDER BY`は結果の表示順であり、順位計算の基準とは別です。
  2. 同点を残したいのに、`RANK()`の`ORDER BY`へ`id`など一意の列を追加していないか。
  3. `ROW_NUMBER()`で同点の順番が変わるなら、別の一意な列で順序を確定できるか。
  4. 「上位N行」と「N位以内」は違う要件です。抽出したいのが行数か順位かを先に確認します。

RANKとDENSE_RANKは同じ点数なら必ず同順位ですか?

`OVER (ORDER BY …)`で比較する値が同じなら同順位になります。点数以外の列も含めると、比較対象の組み合わせが異なり同順位にならない場合があります。

GROUP BYとの違いは何ですか?

GROUP BYは通常、同じグループの行をまとめて集計します。今回の順位関数は元の行を残しつつ、各行へ計算した順位を付けます。

次の学習:順位を使う目的を小さく決める

まずサンプルの点数を90、80、80、70に変えて、3列の順位がどう変わるか予測してみてください。その後、部署ごとの順位が必要になったら`PARTITION BY`を学ぶと、同じ考え方をグループ単位へ広げられます。

一次資料:PostgreSQL公式「Window Functions」

どの順位を残したいかを言葉にしてから、SQLを1行ずつ変えて試してみてください。

ルートゼロの採用情報を見る

すべてを開示している会社です

ポジティブな面もネガティブな面も、包み隠さず誠実にお答えします。
気になることがあれば、面談の際にすべてオープンにお話します。