コマンド道場

順位を付ける関数(ROW_NUMBER / RANK / DENSE_RANK)の使い方

行はそのまま、順位の列が増える

shain社員

namesalary
鈴木980000
佐藤720000
山田700000
清水650000
加藤610000
中村530000

順位の列を足す順位を付けた結果

namesalaryjun_i
鈴木9800001
佐藤7200002
山田7000003
清水6500004
加藤6100005
中村5300006

rank() OVER (ORDER BY salary DESC) AS jun_i

  • 青い列= 式から作った列。元の表には無い列です
  • 社員 18順位を付けた結果 18(行数は変わりません。減らすのは WHERE の仕事です)

元の行は 1 つも減りません。増えるのは順位の列だけです。給与の高い順に 1 から振られているのが見えます。

順位を付ける関数は、行を減らさずに「順位」の列を足します。 GROUP BY のように行が潰れないので、誰が何位かをそのまま出せます。row_number() rank() dense_rank() の 3 つがあり、違いが出るのは同額の行があるときだけです。

主要 7 製品で通用 そのまま使える: PostgreSQL / MySQL / MariaDB / SQLite / SQL Server / Oracle Database / IBM Db2

製品ごとの対応表(根拠つき)を見る

このページの実行結果と図は、すべて本物のデータベースに同じ SQL を流して得たものです。手で書いた「たぶんこうなる」ではありません。

01順位は OVER の中の並びで決まる

結論: rank() OVER (ORDER BY 列 DESC) と書くと、その並びでの順位が列として付きます。

OVER が付く関数をウィンドウ関数と呼びます。OVER の中に書いた ORDER BY が「何の順で順位を付けるか」を決めます。

集約関数と違って行は潰れません。18 人いれば 18 行のまま、順位の列だけが増えます。

⚠️ OVER の中の ORDER BY と、いちばん外側の ORDER BY は別物です。 前者は順位の基準、後者は結果の並べ方です。片方だけ書いても SQL としては成立するので、書き忘れても気づきにくい場所です。

2 つの ORDER BY はそれぞれ何をしているか
  1. SELECT rank() OVER (ORDER BY salary DESC)★順位を付ける基準(給与の高い順)
  2. AS jun_i,作った列に名前を付ける
  3. name, salary元の列はそのまま出せる
  4. FROM shain行は減らない
  5. WHERE taishoku_on IS NULL在籍中だけを対象にする
  6. ORDER BY jun_i, shain_id;★結果の並べ方(順位の基準とは別)

同じ ORDER BY でも、OVER の中にあるか外にあるかで役割がまったく違います。中は「順位の付け方」、外は「表示の並び」です。

在籍中の社員に給与の高い順で順位を付ける
SELECT rank() OVER (ORDER BY salary DESC) AS jun_i,
       name,
       salary
FROM shain
WHERE taishoku_on IS NULL
ORDER BY jun_i, shain_id;
jun_inamesalary
1鈴木980000
2佐藤720000
3山田700000
4清水650000
5加藤610000
6中村530000
6伊藤530000
8田中520000
9井上505000
10吉田468000
11斎藤455000
12小林445050
13大野432000
14松本421000
15渡辺400000
16木村398000

16

16 行返ります。18 人から退職者 2 人が WHERE で外れた数で、順位を付けたこと自体では 1 行も減っていません。

023 つの違いは同額の行でだけ出る

結論: 同額が無ければ 3 つとも同じ結果になります。 違いが出るのは同じ値の行が並んだときだけです。

このデータには給与が同額の社員が 2 人います(中村と伊藤の 530000)。そこで 3 つを横に並べると、違いがはっきり見えます。

  • rank() … 同額は同じ順位。次の順位は飛ばす(6, 6 の次は 8)
  • dense_rank() … 同額は同じ順位。次の順位は飛ばさない(6, 6 の次は 7)
  • row_number() … 同額でも必ず別の番号を振る(6, 7)

⚠️ 食い違いは同額の行だけでは終わりません。 dense_rank() は飛ばさない一方で rank() は飛ばすので、同額より下の行はずっと 1 つずれたままになります(田中は rank() で 8 位、dense_rank() で 7 位)。同額が 1 組あるだけで、以降の全行に差が出ます。

「◯位タイ」を表したいなら rank()、「上から何段階目か」を知りたいなら dense_rank()とにかく重複しない通し番号が欲しいなら row_number() です。

3 つを横に並べて見比べる
SELECT name,
       salary,
       rank()       OVER (ORDER BY salary DESC) AS rank_,
       dense_rank() OVER (ORDER BY salary DESC) AS dense_,
       row_number() OVER (ORDER BY salary DESC) AS row_
FROM shain
WHERE taishoku_on IS NULL
ORDER BY salary DESC, shain_id;
namesalaryrank_dense_row_
鈴木980000111
佐藤720000222
山田700000333
清水650000444
加藤610000555
中村530000667
伊藤530000666
田中520000878
井上505000989
吉田46800010910
斎藤455000111011
小林445050121112
大野432000131213
松本421000141314
渡辺400000151415
木村398000161516

16

同額より上の 5 行は 3 つとも同じ値です。食い違いは中村・伊藤の行から始まります。rank は 7 を欠番にし、row_number は 6 と 7 に分け、dense_rank だけはそこから下もずれ続けます(田中は rank 8 / dense_rank 7)。

03⚠️ よくある間違い:同額のときの row_number は当てにしない

結論: 同額の行のうちどちらが先に番号をもらうかは、決まっていません。

上の実行結果では伊藤が 6、中村が 7 になっていますが、これは保証された順序ではありません。 OVER (ORDER BY salary DESC) は「給与の降順」としか言っておらず、給与が同じ 2 行のどちらを先にするかは何も指定していないからです。

あなたの手元では逆になることがあります。 製品が違えば、同じ製品でも実行計画が変われば、変わりえます。

通し番号を再現できる形にしたいなら、OVER の中の ORDER BY に決着が付く列を足してください。 ここでは社員番号を足せば、いつでも同じ結果になります。

決着が付く列を足して再現できるようにする
SELECT name,
       salary,
       row_number() OVER (ORDER BY salary DESC, shain_id) AS row_
FROM shain
WHERE taishoku_on IS NULL
ORDER BY row_;
namesalaryrow_
鈴木9800001
佐藤7200002
山田7000003
清水6500004
加藤6100005
中村5300006
伊藤5300007
田中5200008
井上5050009
吉田46800010
斎藤45500011
小林44505012
大野43200013
松本42100014
渡辺40000015
木村39800016

16

中村(社員番号 4)が 6、伊藤(社員番号 10)が 7 に固定されます。同額の 2 人の間に社員番号という決着が付いたためです。

04PARTITION BY でまとまりごとに振り直す

結論: OVER (PARTITION BY 列 ORDER BY …) と書くと、そのまとまりごとに順位が 1 から振り直されます。

「部署ごとの給与順位」のように、全体ではなくグループの中での順位が欲しいときに使います。GROUP BY と違って行は潰れないので、誰が何位かがそのまま出ます。

⚠️ 所属が未設定の社員(busho_id が NULL)も、NULL 同士でひとつのまとまりとして扱われます。等しさの判定とは違う扱いなので、WHERE= NULL の話と混同しないでください。

部署ごとに 1 から振り直す
SELECT busho_id,
       name,
       salary,
       rank() OVER (PARTITION BY busho_id ORDER BY salary DESC) AS busho_jun_i
FROM shain
ORDER BY busho_id, busho_jun_i, shain_id;
busho_idnamesalarybusho_jun_i
1鈴木9800001
2佐藤7200001
2中村5300002
2田中5200003
2小林4450504
3山田7000001
3伊藤5300002
3高橋4800003
3松本4210004
4清水6500001
4山本5120002
4斎藤4550003
5加藤6100001
5吉田4680002
7井上5050001
7木村3980002
NULL大野4320001
NULL渡辺4000002

18

部署が変わるたびに順位が 1 に戻ります。末尾の 2 行は所属が未設定の社員で、NULL 同士がひとつのまとまりとして 1 位・2 位になっています。

05⚠️ よくある間違い:WHERE では使えない

結論: WHERE にウィンドウ関数は書けません。エラーになります。

WHEREウィンドウ関数より先に評価されるためです。「どの行を残すか」が決まる時点では、まだ順位は付いていません。

そのため「上位 3 位だけ表示」は 1 つの SELECT では書けません。いったん順位を付けた結果を表として作ってから、その外側で絞り込みます。

書き方は 2 つあり、どちらでも同じことができます。

  • FROM の中に副問い合わせを置く(派生表)
  • WITH で名前を付けてから使う

⚠️ HAVING も同じ理由で使えません。順位で絞りたくなったら、必ず一段外に出すと覚えてください。

WHERE に書くとエラーになる
SELECT name, salary
FROM shain
WHERE rank() OVER (ORDER BY salary DESC) <= 3;
ERROR: window functions are not allowed in WHERE

「window functions are not allowed in WHERE」と出ます。書き方の間違いではなく、評価される順番の問題です。

順位を付けてから、外側で絞り込む
SELECT name, salary, jun_i
FROM (SELECT name,
             salary,
             rank() OVER (ORDER BY salary DESC) AS jun_i
      FROM shain
      WHERE taishoku_on IS NULL) t
WHERE jun_i <= 3
ORDER BY jun_i;
namesalaryjun_i
鈴木9800001
佐藤7200002
山田7000003

3

3 行返ります。内側で順位を付け終えた表を作り、その外側の WHERE で 3 位までに絞っています。

自分で打ってみる

このページの例と同じデータが入った本物のデータベースを、この場(あなたのブラウザの中)で起動できます。打った SQL がサーバーへ送られることはありません。

押すと初回だけ約 9MB を読み込みます(2 回目以降はキャッシュされます)。

解いてみる

読んだだけでは書けるようになりません。実行結果で採点します。

ここで出てくる関数を引く

関連するトピック

根拠(一次情報)