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()実践ガイド』をご参照ください)
結果が想定と違うときの確認順
- `OVER (ORDER BY …)`に、順位の基準となる列が入っているか。最後の`ORDER BY`は結果の表示順であり、順位計算の基準とは別です。
- 同点を残したいのに、`RANK()`の`ORDER BY`へ`id`など一意の列を追加していないか。
- `ROW_NUMBER()`で同点の順番が変わるなら、別の一意な列で順序を確定できるか。
- 「上位N行」と「N位以内」は違う要件です。抽出したいのが行数か順位かを先に確認します。
RANKとDENSE_RANKは同じ点数なら必ず同順位ですか?
`OVER (ORDER BY …)`で比較する値が同じなら同順位になります。点数以外の列も含めると、比較対象の組み合わせが異なり同順位にならない場合があります。
GROUP BYとの違いは何ですか?
GROUP BYは通常、同じグループの行をまとめて集計します。今回の順位関数は元の行を残しつつ、各行へ計算した順位を付けます。
次の学習:順位を使う目的を小さく決める
まずサンプルの点数を90、80、80、70に変えて、3列の順位がどう変わるか予測してみてください。その後、部署ごとの順位が必要になったら`PARTITION BY`を学ぶと、同じ考え方をグループ単位へ広げられます。
一次資料:PostgreSQL公式「Window Functions」
どの順位を残したいかを言葉にしてから、SQLを1行ずつ変えて試してみてください。