- SQL
SQL ROW_NUMBER・RANK・DENSE_RANKの違いと上位n件取得
ROW_NUMBER・RANK・DENSE_RANKは、どれも行に番号を振るウィンドウ関数で、違いは「同点の行をどう扱うか」の1点です。 ROW_NUMBER は同点でも別の番号(1, 2, 3, 4)、RANK は同点に同じ番号を振って次を飛ばし(1, 2, 2, 4)、DENSE_RANK は同じ番号を振って次を詰めます(1, 2, 2, 3)。
この記事では、同点を含む12行のデータに3つの関数を同時にかけた実行結果を並べ、番号の進み方を見比べます。そのうえで、グループごとの上位n件をサブクエリ・CTE・QUALIFY・LATERAL の4通りで書き分け、WHERE に直接書いたときのエラー文も再現しました。PostgreSQL 16.15、MySQL 8.0.46、DuckDB 1.5.6 で動かした結果を載せています。
ROW_NUMBER・RANK・DENSE_RANKの早見表
点数が 95, 91, 91, 88 の4行に、点数の高い順で番号を振った場合の違いです。
| 関数 | 95 | 91 | 91 | 88 | 同点の扱い | 向いている用途 |
|---|---|---|---|---|---|---|
ROW_NUMBER() |
1 | 2 | 3 | 4 | 別の番号を振る | 連番、ちょうどn件、重複の整理、ページ分け |
RANK() |
1 | 2 | 2 | 4 | 同じ番号、次は飛ぶ | 順位表(同率2位の次は4位) |
DENSE_RANK() |
1 | 2 | 2 | 3 | 同じ番号、次は詰める | 上位n種類の値、段階の番号付け |
やりたいことから書き方を引く表です。
| やりたいこと | 書き方 |
|---|---|
| 全体に連番を振る | ROW_NUMBER() OVER (ORDER BY score DESC, id) |
| グループごとに番号を振り直す | ROW_NUMBER() OVER (PARTITION BY team ORDER BY score DESC, id) |
| 同点を同じ順位にする | RANK() OVER (ORDER BY score DESC) |
| 順位を飛ばさずに振る | DENSE_RANK() OVER (ORDER BY score DESC) |
| グループごと上位n件 | サブクエリか CTE で番号を振り、外側で WHERE rn <= n |
| 同上を1つの SELECT で | QUALIFY rn <= n(BigQuery・Snowflake・DuckDB など) |
| 各グループの1件だけ | WHERE rn = 1、PostgreSQL なら DISTINCT ON も可 |
| 4つのグループに等分する | NTILE(4) OVER (ORDER BY score DESC) |
| 上位何%かを出す | PERCENT_RANK() OVER (ORDER BY score DESC) |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「SQL ROW_NUMBER・RANK・DENSE_RANKの違いと上位n件取得」
検証環境とサンプルデータ(コピペ用)
| 項目 | 内容 |
|---|---|
| PostgreSQL | 16.15 |
| MySQL | 8.0.46 |
| DuckDB | 1.5.6(QUALIFY の動作確認用。Python から実行) |
| BigQuery・Snowflake・Oracle・SQL Server | 未実行 |
3つのチームの点数を表す scores テーブルを使います。同点の組を3つ(91点・88点・85点が2人ずつ)入れてあります。PostgreSQL・MySQL・DuckDB のどれにもそのまま貼れます。
DROP TABLE IF EXISTS scores;
CREATE TABLE scores (
id INT PRIMARY KEY,
team VARCHAR(10) NOT NULL,
term VARCHAR(10) NOT NULL,
name VARCHAR(20) NOT NULL,
score INT NOT NULL
);
INSERT INTO scores (id, team, term, name, score) VALUES
(1, 'A', '前期', '佐藤', 95),
(2, 'A', '前期', '鈴木', 88),
(3, 'A', '後期', '高橋', 88),
(4, 'A', '後期', '田中', 80),
(5, 'A', '後期', '伊藤', 72),
(6, 'B', '前期', '渡辺', 91),
(7, 'B', '前期', '山本', 91),
(8, 'B', '前期', '中村', 85),
(9, 'B', '後期', '小林', 85),
(10, 'B', '後期', '加藤', 60),
(11, 'C', '前期', '吉田', 77),
(12, 'C', '後期', '山田', 70);
3つの関数を同じデータに同時にかけた結果
SELECT name, team, score,
ROW_NUMBER() OVER (ORDER BY score DESC, id) AS row_num,
RANK() OVER (ORDER BY score DESC) AS rnk,
DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rnk
FROM scores
ORDER BY score DESC, id;
実行結果(PostgreSQL 16.15。MySQL 8.0.46 でも同じ値でした)です。
name | team | score | row_num | rnk | dense_rnk
------+------+-------+---------+-----+-----------
佐藤 | A | 95 | 1 | 1 | 1
渡辺 | B | 91 | 2 | 2 | 2
山本 | B | 91 | 3 | 2 | 2
鈴木 | A | 88 | 4 | 4 | 3
高橋 | A | 88 | 5 | 4 | 3
中村 | B | 85 | 6 | 6 | 4
小林 | B | 85 | 7 | 6 | 4
田中 | A | 80 | 8 | 8 | 5
吉田 | C | 77 | 9 | 9 | 6
伊藤 | A | 72 | 10 | 10 | 7
山田 | C | 70 | 11 | 11 | 8
加藤 | B | 60 | 12 | 12 | 9
(12 rows)
番号の進み方を整理すると、次のようになります。
| 見るところ | ROW_NUMBER | RANK | DENSE_RANK |
|---|---|---|---|
| 91点の2人 | 2 と 3 | 2 と 2 | 2 と 2 |
| その次の88点 | 4 | 4(3を飛ばす) | 3(詰める) |
| 最後の行の番号 | 12(行数と同じ) | 12 | 9(値の種類の数) |
| 番号の重複 | なし | あり | あり |
| 番号の飛び | なし | あり | なし |
最後の行の番号を見ると性質がよく分かります。 ROW_NUMBER は行数、DENSE_RANK は点数の種類の数になります。RANK は「自分より上に何人いるか+1」です。
OVER句の読み方(PARTITION BY と ORDER BY)
3つの関数は、必ず OVER 句と一緒に書きます。
関数名() OVER (PARTITION BY グループ分けの列 ORDER BY 並べ替えの列)
| 部分 | 役割 | 省いたとき |
|---|---|---|
PARTITION BY |
番号を振り直す単位(グループ)を決める | 全体を1グループとして扱う |
ORDER BY |
グループの中で番号を振る順番を決める | 順番が決まらない(後述) |
OVER の中の ORDER BY は番号を振る順番を決めるもので、結果の表示順は文末の ORDER BY が決めます。表示順もそろえたいときは、文末にも ORDER BY を書きます。
チームごとに番号を振り直す
SELECT team, name, score,
ROW_NUMBER() OVER (PARTITION BY team ORDER BY score DESC, id) AS row_num,
RANK() OVER (PARTITION BY team ORDER BY score DESC) AS rnk,
DENSE_RANK() OVER (PARTITION BY team ORDER BY score DESC) AS dense_rnk
FROM scores
ORDER BY team, score DESC, id;
team | name | score | row_num | rnk | dense_rnk
------+------+-------+---------+-----+-----------
A | 佐藤 | 95 | 1 | 1 | 1
A | 鈴木 | 88 | 2 | 2 | 2
A | 高橋 | 88 | 3 | 2 | 2
A | 田中 | 80 | 4 | 4 | 3
A | 伊藤 | 72 | 5 | 5 | 4
B | 渡辺 | 91 | 1 | 1 | 1
B | 山本 | 91 | 2 | 1 | 1
B | 中村 | 85 | 3 | 3 | 2
B | 小林 | 85 | 4 | 3 | 2
B | 加藤 | 60 | 5 | 5 | 3
C | 吉田 | 77 | 1 | 1 | 1
C | 山田 | 70 | 2 | 2 | 2
(12 rows)
チームが変わるたびに、番号が1から始まり直しています。
複数の列で PARTITION BY する
PARTITION BY にはカンマ区切りで複数の列を書けます。 チームと期の組み合わせごとに順位を付ける例です。
SELECT team, term, name, score,
RANK() OVER (PARTITION BY team, term ORDER BY score DESC) AS rk
FROM scores
ORDER BY team, term DESC, rk, id;
team | term | name | score | rk
------+------+------+-------+----
A | 後期 | 高橋 | 88 | 1
A | 後期 | 田中 | 80 | 2
A | 後期 | 伊藤 | 72 | 3
A | 前期 | 佐藤 | 95 | 1
A | 前期 | 鈴木 | 88 | 2
B | 後期 | 小林 | 85 | 1
B | 後期 | 加藤 | 60 | 2
B | 前期 | 渡辺 | 91 | 1
B | 前期 | 山本 | 91 | 1
B | 前期 | 中村 | 85 | 3
C | 後期 | 山田 | 70 | 1
C | 前期 | 吉田 | 77 | 1
(12 rows)
同じ OVER 句を何度も書かない(WINDOW句)
同じウィンドウ定義を繰り返すときは、WINDOW 句で名前を付けられます。PostgreSQL・MySQL の両方で実行できました。
SELECT team, name, score,
ROW_NUMBER() OVER w AS row_num,
RANK() OVER w AS rnk,
DENSE_RANK() OVER w AS dense_rnk
FROM scores
WHERE team = 'B'
WINDOW w AS (ORDER BY score DESC)
ORDER BY score DESC, row_num;
同順位の決着を付ける(ORDER BY の第2キー)
ROW_NUMBER で同点の行のどちらが先になるかは、ORDER BY に書いた列だけで決まります。 点数だけを指定すると、同点の2人の順番はデータベース任せになり、実行のたびに同じとは限りません。主キーなど重複しない列を2番目に足すと、順番が固定されます。
SELECT name, score,
ROW_NUMBER() OVER (ORDER BY score DESC, id ASC) AS by_id_asc,
ROW_NUMBER() OVER (ORDER BY score DESC, id DESC) AS by_id_desc
FROM scores WHERE team = 'B'
ORDER BY score DESC, id;
name | score | by_id_asc | by_id_desc
------+-------+-----------+------------
渡辺 | 91 | 1 | 2
山本 | 91 | 2 | 1
中村 | 85 | 3 | 4
小林 | 85 | 4 | 3
加藤 | 60 | 5 | 5
(5 rows)
第2キーの向きを変えると、同点の中の順番だけが入れ替わります。
| 決着の付け方 | ORDER BY の例 |
|---|---|
| 登録が早い順 | ORDER BY score DESC, id |
| 新しい日時を優先 | ORDER BY score DESC, updated_at DESC, id |
| 名前順 | ORDER BY score DESC, name, id |
| 同点は同じ順位でよい | RANK か DENSE_RANK を使う |
RANK と DENSE_RANK でも、ORDER BY に第2キーを足すと「点数とidの組」で比べることになり、同点が同点として扱われなくなります。同率の順位を出したいときは、RANK・DENSE_RANK の ORDER BY には順位の基準にする列だけを書きます。
グループごとに上位n件を取る4つの書き方
「チームごとに点数の高い2人」を取ります。ウィンドウ関数の結果は WHERE で直接絞れないので、いったん番号を振ってから外側で絞るのが基本形です。
1. サブクエリ(FROM句の中で番号を振る)
SELECT team, name, score, rn
FROM (
SELECT team, name, score,
ROW_NUMBER() OVER (PARTITION BY team ORDER BY score DESC, id) AS rn
FROM scores
) t
WHERE rn <= 2
ORDER BY team, rn;
team | name | score | rn
------+------+-------+----
A | 佐藤 | 95 | 1
A | 鈴木 | 88 | 2
B | 渡辺 | 91 | 1
B | 山本 | 91 | 2
C | 吉田 | 77 | 1
C | 山田 | 70 | 2
(6 rows)
2. CTE(WITH句で名前を付ける)
CTE(共通テーブル式)は、サブクエリに名前を付けて先に書く方法です。結果は上と同じ6件でした。
WITH ranked AS (
SELECT team, name, score,
ROW_NUMBER() OVER (PARTITION BY team ORDER BY score DESC, id) AS rn
FROM scores
)
SELECT team, name, score, rn
FROM ranked
WHERE rn <= 2
ORDER BY team, rn;
3. QUALIFY(1つの SELECT で絞る)
QUALIFY は、ウィンドウ関数の結果で行を絞るための句です。集計結果を絞る HAVING の、ウィンドウ関数版にあたります。Snowflake の公式ドキュメントで、FROM → WHERE → GROUP BY → HAVING → WINDOW → QUALIFY → DISTINCT → ORDER BY → LIMIT の順に評価されること、SELECT で付けた別名を QUALIFY で使えることを確認しました。
手元では DuckDB 1.5.6 で実行しました(BigQuery・Snowflake での実行は未実行です)。
SELECT team, name, score,
ROW_NUMBER() OVER (PARTITION BY team ORDER BY score DESC, id) AS rn
FROM scores
QUALIFY rn <= 2
ORDER BY team, rn;
┌─────────┬─────────┬───────┬───────┐
│ team │ name │ score │ rn │
│ varchar │ varchar │ int32 │ int64 │
├─────────┼─────────┼───────┼───────┤
│ A │ 佐藤 │ 95 │ 1 │
│ A │ 鈴木 │ 88 │ 2 │
│ B │ 渡辺 │ 91 │ 1 │
│ B │ 山本 │ 91 │ 2 │
│ C │ 吉田 │ 77 │ 1 │
│ C │ 山田 │ 70 │ 2 │
└─────────┴─────────┴───────┴───────┘
SELECT に番号の列を出したくないときは、QUALIFY の中に関数を直接書けます。
SELECT team, name, score
FROM scores
QUALIFY RANK() OVER (PARTITION BY team ORDER BY score DESC) <= 2
ORDER BY team, score DESC, id;
PostgreSQL 16.15 と MySQL 8.0.46 は QUALIFY に対応しておらず、同じSQLは構文エラーになりました(PostgreSQL は syntax error at or near "rk"、MySQL は ERROR 1064)。この2つではサブクエリか CTE を使います。
4. LATERAL(グループごとに ORDER BY と LIMIT を実行する)
LATERAL は、左側のテーブルの1行ごとに、右側のサブクエリを実行する結合です。「チームごとに、点数順に並べて2件取る」をそのまま書けます。PostgreSQL 16.15 と MySQL 8.0.46 で同じ6件を確認しました(MySQL は 8.0.14 以降で使えることを公式ドキュメントで確認)。
SELECT t.team, s.name, s.score
FROM (SELECT DISTINCT team FROM scores) t
CROSS JOIN LATERAL (
SELECT name, score
FROM scores
WHERE team = t.team
ORDER BY score DESC, id
LIMIT 2
) s
ORDER BY t.team, s.score DESC;
チームの一覧を持つ別のテーブルがあるなら、(SELECT DISTINCT team FROM scores) の代わりにそのテーブルを置けます。SQL Server では同じ考え方を CROSS APPLY と TOP で書きます(未実行)。
4つの書き方の選び方
| 書き方 | 使えるDB | 合う場面 |
|---|---|---|
| サブクエリ | ウィンドウ関数のあるDB全般 | まず覚える基本形 |
| CTE | 同上(MySQL は 8.0 以降) | 番号を振った結果を何度か使う、段階を分けて読みやすくしたい |
| QUALIFY | BigQuery・Snowflake・DuckDB など | 対応DBで短く書きたい |
| LATERAL | PostgreSQL、MySQL 8.0.14 以降など | グループの数が少なく、各グループの行が多い。並べ替えに使う列にインデックスがある |
上位n件は関数の選び方で件数が変わる
同じ「チームごと上位2」でも、同点があると関数によって返る件数が変わります。 サブクエリの中の関数だけを入れ替えて実行した結果です。
| 関数 | 条件 | 件数 | チームA | チームB |
|---|---|---|---|---|
ROW_NUMBER() |
rn <= 2 |
6件 | 佐藤・鈴木 | 渡辺・山本 |
RANK() |
rk <= 2 |
7件 | 佐藤・鈴木・高橋 | 渡辺・山本 |
DENSE_RANK() |
dr <= 2 |
9件 | 佐藤・鈴木・高橋 | 渡辺・山本・中村・小林 |
DENSE_RANK の実行結果です。
team | name | score | dr
------+------+-------+----
A | 佐藤 | 95 | 1
A | 鈴木 | 88 | 2
A | 高橋 | 88 | 2
B | 山本 | 91 | 1
B | 渡辺 | 91 | 1
B | 中村 | 85 | 2
B | 小林 | 85 | 2
C | 吉田 | 77 | 1
C | 山田 | 70 | 2
(9 rows)
チームBは1位が2人いるので、RANK では2位が空席になり(次は3位)、2人だけが返ります。DENSE_RANK は「上から2種類の点数」なので4人です。
| 求めたい結果 | 選ぶ関数 |
|---|---|
| 各グループからちょうどn行 | ROW_NUMBER(第2キーで決着を付ける) |
| n位以内の人を全員(同率を含む) | RANK |
| 上からn種類の値に当たる行を全員 | DENSE_RANK |
| 各グループの最新1件・最大1件 | ROW_NUMBER で rn = 1 |
WHEREに直接書くとエラーになる
ウィンドウ関数は、WHERE・GROUP BY・HAVING の処理が終わったあとに計算されます。そのため WHERE の中には書けません。
ウィンドウ関数を WHERE に書いた場合
SELECT team, name, score
FROM scores
WHERE ROW_NUMBER() OVER (PARTITION BY team ORDER BY score DESC) <= 2;
| DB | エラー文 |
|---|---|
| PostgreSQL 16.15 | ERROR: window functions are not allowed in WHERE |
| MySQL 8.0.46 | ERROR 3593 (HY000): You cannot use the window function 'row_number' in this context.' |
| DuckDB 1.5.6 | Binder Error: WHERE clause cannot contain window functions! |
SELECT で付けた別名を WHERE で使った場合
SELECT team, name, score,
ROW_NUMBER() OVER (PARTITION BY team ORDER BY score DESC) AS rn
FROM scores
WHERE rn <= 2;
| DB | エラー文 |
|---|---|
| PostgreSQL 16.15 | ERROR: column "rn" does not exist |
| MySQL 8.0.46 | ERROR 1054 (42S22): Unknown column 'rn' in 'where clause' |
WHERE は SELECT より先に処理されるので、その時点では rn という列がまだありません。どちらも、番号を振る部分をサブクエリか CTE に入れ、外側の WHERE で絞ると解消します。
OVER を付け忘れた場合
SELECT team, name, score, ROW_NUMBER() AS rn FROM scores;
| DB | エラー文 |
|---|---|
| PostgreSQL 16.15 | ERROR: window function row_number requires an OVER clause |
| MySQL 8.0.46 | ERROR 1064 (42000): You have an error in your SQL syntax; ... near 'AS rn FROM scores' at line 1 |
ORDER BY の無い RANK は全員1位になる
エラーにはなりませんが、OVER の中に ORDER BY が無い RANK は、グループの全員が1になります。
SELECT team, name, score, RANK() OVER (PARTITION BY team) AS rk
FROM scores ORDER BY team, id;
PostgreSQL・MySQL とも、12行すべての rk が 1 でした。並べ替えの基準が無いと、全員が同順位として扱われるためです。RANK・DENSE_RANK には必ず ORDER BY を書きます。
全体の上位n件と、同率を含める書き方
グループ分けをしない「全体の上位3件」は、ウィンドウ関数を使わずに ORDER BY と LIMIT で取れます。
SELECT name, score FROM scores ORDER BY score DESC LIMIT 3;
同率の行も含めたいときは、PostgreSQL の FETCH FIRST ... WITH TIES が使えます(PostgreSQL 13 で加わった構文として知られています)。上位2件を指定して実行した結果(PostgreSQL 16.15)です。
SELECT name, score FROM scores
ORDER BY score DESC
FETCH FIRST 2 ROWS WITH TIES;
name | score
------+-------
佐藤 | 95
山本 | 91
渡辺 | 91
(3 rows)
2件目と同点の行まで含めて3件が返りました。MySQL 8.0.46 ではこの構文は ERROR 1064 になったので、RANK で rk <= 2 と書きます。
各グループの1件だけなら DISTINCT ON(PostgreSQL)
SELECT DISTINCT ON (team) team, name, score
FROM scores
ORDER BY team, score DESC, id;
team | name | score
------+------+-------
A | 佐藤 | 95
B | 渡辺 | 91
C | 吉田 | 77
(3 rows)
ROW_NUMBER() ... rn = 1 と同じ3件です。DISTINCT ON は PostgreSQL 独自の書き方なので、他のDBでも動かすSQLには ROW_NUMBER を使います。
MySQL 5.7以前などウィンドウ関数が無い環境の代替
MySQL でウィンドウ関数が使えるのは 8.0 からです。それより前の版では、次の2つの書き方が使われてきました。手元に 5.7 は無いため、どちらも MySQL 8.0.46 で実行して結果を確かめています(5.7 での実行は未実行です)。
相関サブクエリで「自分より上の人数」を数える
SELECT s.team, s.name, s.score
FROM scores s
WHERE (
SELECT COUNT(*)
FROM scores x
WHERE x.team = s.team
AND (x.score > s.score OR (x.score = s.score AND x.id < s.id))
) < 2
ORDER BY s.team, s.score DESC, s.id;
「同じチームで自分より上にいる人が2人未満」の行を残します。結果は ROW_NUMBER 版と同じ6件でした。x.score = s.score AND x.id < s.id が同点の決着(第2キー)にあたります。
RANK と同じ順位を出すなら、「自分より点数が高い人数+1」を数えます。
SELECT s.team, s.name, s.score,
(SELECT COUNT(*) + 1 FROM scores x
WHERE x.team = s.team AND x.score > s.score) AS rk
FROM scores s
WHERE s.team = 'B'
ORDER BY rk, s.id;
+------+--------+-------+------+
| team | name | score | rk |
+------+--------+-------+------+
| B | 渡辺 | 91 | 1 |
| B | 山本 | 91 | 1 |
| B | 中村 | 85 | 3 |
| B | 小林 | 85 | 3 |
| B | 加藤 | 60 | 5 |
+------+--------+-------+------+
この書き方は、行ごとにサブクエリが動くので、行数が多いテーブルでは時間が掛かりやすくなります。(team, score) のインデックスがあると助けになります。
ユーザー変数で連番を作る
SELECT team, name, score, rn FROM (
SELECT team, name, score,
@rn := IF(@prev = team, @rn + 1, 1) AS rn,
@prev := team AS prev_team
FROM (SELECT * FROM scores ORDER BY team, score DESC, id) s
CROSS JOIN (SELECT @rn := 0, @prev := '') init
) t
WHERE rn <= 2
ORDER BY team, rn;
MySQL 8.0.46 では同じ6件が返り、あわせて次の警告が出ました。
Warning 1287: Setting user variables within expressions is deprecated and will be
removed in a future release.
式の中での変数代入は将来の版で使えなくなる予定と案内されているので、8.0 以降ではウィンドウ関数に置き換えるのがおすすめです。
NTILE・PERCENT_RANK・CUME_DIST の使い方
順位に関係するウィンドウ関数は、ほかにもあります。
| 関数 | 返す値 | 計算のしかた |
|---|---|---|
NTILE(n) |
1〜n のグループ番号 | 並べた行をn個のグループにほぼ等分する |
PERCENT_RANK() |
0〜1 | (RANK − 1) ÷ (行数 − 1) |
CUME_DIST() |
0より大きく1以下 | 自分と同順位までの行数 ÷ 行数 |
計算式は MySQL 8.0 の公式ドキュメントで確認しました。小数の表示をそろえるために DECIMAL へ変換しています。
SELECT name, score,
NTILE(4) OVER (ORDER BY score DESC, id) AS quartile,
ROUND(CAST(PERCENT_RANK() OVER (ORDER BY score DESC) AS DECIMAL(5,3)), 3) AS pct_rank,
ROUND(CAST(CUME_DIST() OVER (ORDER BY score DESC) AS DECIMAL(5,3)), 3) AS cume
FROM scores
ORDER BY score DESC, id;
name | score | quartile | pct_rank | cume
------+-------+----------+----------+-------
佐藤 | 95 | 1 | 0.000 | 0.083
渡辺 | 91 | 1 | 0.091 | 0.250
山本 | 91 | 1 | 0.091 | 0.250
鈴木 | 88 | 2 | 0.273 | 0.417
高橋 | 88 | 2 | 0.273 | 0.417
中村 | 85 | 2 | 0.455 | 0.583
小林 | 85 | 3 | 0.455 | 0.583
田中 | 80 | 3 | 0.636 | 0.667
吉田 | 77 | 3 | 0.727 | 0.750
伊藤 | 72 | 4 | 0.818 | 0.833
山田 | 70 | 4 | 0.909 | 0.917
加藤 | 60 | 4 | 1.000 | 1.000
(12 rows)
読み取れる点は次の3つです。
- NTILE は行数で等分するので、同じ85点の中村と小林がグループ2と3に分かれています。同点を同じグループにしたいときは、PERCENT_RANK の値で区切る方法があります。
- PERCENT_RANK は RANK から計算されるので、同点の2人は同じ値です。渡辺は RANK が2、行数が12なので (2 − 1) ÷ 11 = 0.091 です。
- CUME_DIST は「ここまでで全体の何割か」を表します。91点の2人は、3人目までが含まれるので 3 ÷ 12 = 0.250 です。
よく使う応用パターン
集計した結果に順位を付ける
ウィンドウ関数は GROUP BY のあとに計算されるので、集計関数を ORDER BY の中に書けます。
SELECT team, SUM(score) AS total,
RANK() OVER (ORDER BY SUM(score) DESC) AS team_rank
FROM scores
GROUP BY team
ORDER BY team_rank;
team | total | team_rank
------+-------+-----------
A | 423 | 1
B | 412 | 2
C | 147 | 3
(3 rows)
連番でページを分ける
4〜6番目の行を取る例です。
SELECT id, name, score FROM (
SELECT id, name, score,
ROW_NUMBER() OVER (ORDER BY score DESC, id) AS rn
FROM scores
) t
WHERE rn BETWEEN 4 AND 6
ORDER BY rn;
id | name | score
----+------+-------
2 | 鈴木 | 88
3 | 高橋 | 88
8 | 中村 | 85
(3 rows)
ページ分けでは、ページをまたいで同じ行が出たり抜けたりしないよう、第2キーに id を入れて順番を固定します。
NULL が先頭に来るか末尾に来るかはDBで違う
並べ替えの列に NULL があると、降順のとき PostgreSQL は NULL を先頭、MySQL は末尾に置きました。
SELECT v, ROW_NUMBER() OVER (ORDER BY v DESC) AS rn_desc
FROM (SELECT 10 AS v UNION ALL SELECT NULL UNION ALL SELECT 30) x
ORDER BY rn_desc;
| v | PostgreSQL 16.15 の rn_desc | MySQL 8.0.46 の rn_desc |
|---|---|---|
| 30 | 2 | 1 |
| 10 | 3 | 2 |
| NULL | 1 | 3 |
PostgreSQL では ORDER BY v DESC NULLS LAST と書くと、30 が1、10 が2、NULL が3になりました。MySQL には NULLS LAST の指定が無いので、ORDER BY v IS NULL, v DESC のように NULL かどうかを先に並べ替えの基準にします。MySQL 8.0.46 と PostgreSQL 16.15 の両方で、30 が1、10 が2、NULL が3になることを確認しました。
DBごとの対応状況
| 機能 | PostgreSQL | MySQL | そのほか |
|---|---|---|---|
| ROW_NUMBER・RANK・DENSE_RANK | 16.15 で実行確認 | 8.0.46 で実行確認(8.0 以降) | Oracle・SQL Server・SQLite 3.25 以降などで利用可(未実行) |
| NTILE・PERCENT_RANK・CUME_DIST | 実行確認 | 実行確認 | 同上(未実行) |
| WINDOW 句 | 実行確認 | 実行確認 | DBによる |
| QUALIFY | 非対応(構文エラーを確認) | 非対応(構文エラーを確認) | DuckDB 1.5.6 で実行確認。Snowflake は公式ドキュメントで確認。BigQuery は未実行 |
| LATERAL | 実行確認 | 実行確認(8.0.14 以降) | SQL Server・Oracle は APPLY(未実行) |
| FETCH FIRST ... WITH TIES | 実行確認 | 非対応(構文エラーを確認) | DBによる |
| DISTINCT ON | 実行確認 | 非対応(構文エラーを確認) | PostgreSQL 独自 |
「未実行」と書いた行は一般に知られた仕様として載せています。使う版のドキュメントもあわせて確かめてください。
よくある質問
Q. RANK と DENSE_RANK はどちらを使えばよいですか? A. 「同率2位が2人いたら、次は4位」という順位表にしたいなら RANK、「次は3位」と詰めたいなら DENSE_RANK です。上位n件の抽出に使うと件数が変わり、今回のデータでは RANK が7件、DENSE_RANK が9件でした。
Q. ROW_NUMBER の結果が実行のたびに変わることがあります。なぜですか?
A. ORDER BY に指定した列が同じ値の行同士は、順番が決まっていないためです。ORDER BY score DESC, id のように、重複しない列を最後に足すと固定されます。
Q. WHERE で順位を絞るとエラーになります。どう書けばよいですか?
A. ウィンドウ関数は WHERE より後に計算されるので、WHERE には書けません。番号を振る SELECT をサブクエリか CTE に入れ、外側で WHERE rn <= 2 と絞ります。BigQuery・Snowflake・DuckDB などでは QUALIFY で1つの SELECT にまとめられます。
Q. PARTITION BY と GROUP BY は何が違いますか? A. GROUP BY はグループを1行にまとめますが、PARTITION BY は行をまとめずに、グループの中での番号や順位を各行に付けます。元の行をすべて残したまま順位を知りたいときは PARTITION BY を使います。
まとめ
- 3関数の違いは同点の扱い。ROW_NUMBER は 1, 2, 3, 4、RANK は 1, 2, 2, 4、DENSE_RANK は 1, 2, 2, 3 です。
- PARTITION BY でグループごとに番号を振り直す。複数の列もカンマ区切りで指定できます。
- ROW_NUMBER には第2キーを足して順番を固定する。主キーなど重複しない列が向いています。
- グループごと上位n件は、番号を振ってから外側で絞る。サブクエリ・CTE が基本で、対応DBなら QUALIFY、グループが少なければ LATERAL も選べます。
- 上位n件は関数で件数が変わる。今回は ROW_NUMBER が6件、RANK が7件、DENSE_RANK が9件でした。
- WHERE に直接書くとエラーになる。PostgreSQL は
window functions are not allowed in WHERE、MySQL はERROR 3593です。 - NTILE は等分、PERCENT_RANK は割合。同点を同じグループにしたいときは割合の値で区切ります。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「SQL ROW_NUMBER・RANK・DENSE_RANKの違いと上位n件取得」
株式会社RJCでは、SQLのウィンドウ関数の使い方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。