今回は、MySQL 8のウィンドウ関数を使ってランキングを出す方法を、新人エンジニア向けに解説します。
特に、ROW_NUMBER、RANK、DENSE_RANKを使って、売上ランキング、点数ランキング、カテゴリ別ランキング、上位3件だけを取得するSQLを書けるようにしていきます。
ランキングをSQLで出したい場面は、実務でもよくあります。
売上金額が高い商品ランキングを出したい
ユーザーのポイントランキングを出したい
カテゴリごとに売上上位3件を出したい
部署ごとの成績ランキングを出したい
同点の場合の順位をどう扱うか決めたい
このような処理で便利なのが、MySQL 8から使えるウィンドウ関数です。
MySQL公式ドキュメントでは、ウィンドウ関数は集計のような操作を行うものの、GROUP BYのように行を1行へまとめず、各行ごとに結果を生成すると説明されています。さらに、ROW_NUMBER、RANK、DENSE_RANKなどはウィンドウ関数として使われ、OVER句が必須です。
ウィンドウ関数とは何か
ウィンドウ関数とは、検索結果の行を見ながら、各行に対して順位や集計結果を付けられる関数です。
少し難しく聞こえますよね。
たとえるなら、クラスのテスト結果一覧を見ながら、1人ずつに順位を書き込んでいく作業です。
生徒の行は消えません。
各生徒の行に「あなたは1位」「あなたは2位」という情報が追加されます。
GROUP BYとの違いを見てみましょう。
| 機能 | 何をするか | 行数はどうなるか |
|---|---|---|
| GROUP BY | 行をグループにまとめる | 少なくなる |
| ウィンドウ関数 | 各行に順位や集計値を付ける | 基本的に元の行数のまま |
GROUP BYは、複数の行を1つにまとめる処理です。
ウィンドウ関数は、行を残したまま順位や集計値を追加する処理です。
ここが非常に重要です。
今回使うサンプルテーブル
今回は、商品ごとの売上を記録するsalesテーブルを使います。
CREATE TABLE sales (
sale_id INT PRIMARY KEY AUTO_INCREMENT,
product_name VARCHAR(100) NOT NULL,
category_name VARCHAR(100) NOT NULL,
sales_amount INT NOT NULL
);
サンプルデータを入れます。
INSERT INTO sales (product_name, category_name, sales_amount) VALUES
('ノートPC', '家電', 300000),
('スマートフォン', '家電', 300000),
('イヤホン', '家電', 120000),
('キーボード', '家電', 80000),
('デスクチェア', '家具', 200000),
('デスク', '家具', 200000),
('本棚', '家具', 150000),
('ライト', '家具', 70000),
('Java入門書', '書籍', 90000),
('SQL入門書', '書籍', 90000),
('Spring Boot本', '書籍', 120000);
データのイメージです。
| sale_id | product_name | category_name | sales_amount |
|---|---|---|---|
| 1 | ノートPC | 家電 | 300000 |
| 2 | スマートフォン | 家電 | 300000 |
| 3 | イヤホン | 家電 | 120000 |
| 4 | キーボード | 家電 | 80000 |
| 5 | デスクチェア | 家具 | 200000 |
| 6 | デスク | 家具 | 200000 |
| 7 | 本棚 | 家具 | 150000 |
| 8 | ライト | 家具 | 70000 |
| 9 | Java入門書 | 書籍 | 90000 |
| 10 | SQL入門書 | 書籍 | 90000 |
| 11 | Spring Boot本 | 書籍 | 120000 |
このデータには、同じ売上金額の商品があります。
たとえば、ノートPCとスマートフォンはどちらも300000です。
デスクチェアとデスクも同じ200000です。
同点の扱いを学ぶには、ちょうどよいデータです。
ランキングの基本構文
ウィンドウ関数でランキングを出す基本構文は、次の形です。
ランキング関数() OVER (
ORDER BY 並び替えたい列 DESC
)
実際のSQLでは、次のように書きます。
SELECT
product_name,
sales_amount,
ROW_NUMBER() OVER (
ORDER BY sales_amount DESC
) AS ranking
FROM sales;
ROW_NUMBER()がランキング関数です。
OVER句の中で、どの順番で順位を付けるかを指定します。
ORDER BY sales_amount DESCは、売上金額が大きい順という意味です。
| 部分 | 意味 |
|---|---|
| ROW_NUMBER() | 行番号を付ける関数 |
| OVER | どの範囲・順番で関数を使うか指定する |
| ORDER BY sales_amount DESC | 売上金額が高い順に並べて順位を付ける |
| AS ranking | 順位列に名前を付ける |
MySQL公式ドキュメントでは、ウィンドウ関数を使うには関数呼び出しの後にOVER句を含めると説明されています。また、ORDER BYを含めない場合、ROW_NUMBERの番号付けは非決定的になるため、順位を出すときはORDER BYを指定するのが基本です。
ROW_NUMBERでランキングを出す
まず、ROW_NUMBERを使ってランキングを出します。
SELECT
product_name,
category_name,
sales_amount,
ROW_NUMBER() OVER (
ORDER BY sales_amount DESC
) AS row_number_rank
FROM sales;結果のイメージです。
| product_name | category_name | sales_amount | row_number_rank |
|---|---|---|---|
| ノートPC | 家電 | 300000 | 1 |
| スマートフォン | 家電 | 300000 | 2 |
| デスクチェア | 家具 | 200000 | 3 |
| デスク | 家具 | 200000 | 4 |
| 本棚 | 家具 | 150000 | 5 |
| イヤホン | 家電 | 120000 | 6 |
| Spring Boot本 | 書籍 | 120000 | 7 |
| Java入門書 | 書籍 | 90000 | 8 |
| SQL入門書 | 書籍 | 90000 | 9 |
| キーボード | 家電 | 80000 | 10 |
| ライト | 家具 | 70000 | 11 |
ROW_NUMBERは、同じ売上金額でも必ず別々の番号を振ります。
ノートPCとスマートフォンは同じ300000ですが、1位と2位に分かれています。
MySQL公式ドキュメントでも、ROW_NUMBERはパーティション内の現在行の番号を返し、同じ値を持つピアにも異なる行番号を割り当てると説明されています。
ROW_NUMBERは「整理番号」と考える
ROW_NUMBERは、厳密には「同点を考慮した順位」というより、並べた結果に上から順番に番号を振る関数です。
たとえるなら、人気ランキングというより、受付番号に近いです。
同じ点数の人がいても、列に並んだ順番で1番、2番、3番と番号を付けます。
| 特徴 | ROW_NUMBER |
|---|---|
| 同点の扱い | 同点でも別番号 |
| 順位の重複 | しない |
| 番号の飛び | しない |
| 向いている用途 | 上から順に1件ずつ番号を振りたい場合 |
同点でも必ず1件だけ1位にしたい場合は、ROW_NUMBERが向いています。
ただし、同点の順番を安定させるためには、ORDER BYに追加条件を入れることが大切です。
ROW_NUMBERで同点時の順番を安定させる
次のSQLでは、sales_amountだけで並べています。
ROW_NUMBER() OVER (
ORDER BY sales_amount DESC
)同じsales_amountの行がある場合、どちらが先になるかが分かりにくくなることがあります。
そこで、sale_idもORDER BYに追加します。
SELECT
sale_id,
product_name,
sales_amount,
ROW_NUMBER() OVER (
ORDER BY sales_amount DESC, sale_id ASC
) AS row_number_rank
FROM sales;このSQLでは、まず売上金額が高い順に並べます。
同じ売上金額なら、sale_idが小さい順に並べます。
ORDER BY sales_amount DESC, sale_id ASC
ランキングSQLでは、同点時の並び順まで決めておくと結果が安定します。
新人エンジニアは、ROW_NUMBERを使うときは「同点のとき何で並べるか」まで考えてください。
RANKで同点を同じ順位にする
次に、RANKを使います。
RANKは、同じ値の行に同じ順位を付けます。
SELECT
product_name,
category_name,
sales_amount,
RANK() OVER (
ORDER BY sales_amount DESC
) AS rank_no
FROM sales;結果のイメージです。
| product_name | category_name | sales_amount | rank_no |
|---|---|---|---|
| ノートPC | 家電 | 300000 | 1 |
| スマートフォン | 家電 | 300000 | 1 |
| デスクチェア | 家具 | 200000 | 3 |
| デスク | 家具 | 200000 | 3 |
| 本棚 | 家具 | 150000 | 5 |
| イヤホン | 家電 | 120000 | 6 |
| Spring Boot本 | 書籍 | 120000 | 6 |
| Java入門書 | 書籍 | 90000 | 8 |
| SQL入門書 | 書籍 | 90000 | 8 |
| キーボード | 家電 | 80000 | 10 |
| ライト | 家具 | 70000 | 11 |
ノートPCとスマートフォンは同じ300000なので、どちらも1位です。
ただし、その次は2位ではなく3位になります。
これがRANKの特徴です。
同点が2人いると、その人数分だけ次の順位が飛びます。
たとえば、テストで1位が2人いたら、次の人は3位になるイメージです。
RANKは「競技の順位」と考える
RANKは、スポーツ大会やテスト順位に近いです。
同じ点数なら同じ順位。
その次の順位は飛びます。
| 特徴 | RANK |
|---|---|
| 同点の扱い | 同じ順位 |
| 順位の重複 | する |
| 番号の飛び | する |
| 向いている用途 | 同点を同じ順位として扱うランキング |
売上ランキングや成績ランキングで、「同じ点数なら同じ順位にしたい」という場合はRANKが向いています。
DENSE_RANKで順位を飛ばさない
RANKと似ている関数に、DENSE_RANKがあります。
DENSE_RANKも同点を同じ順位にします。
ただし、次の順位が飛びません。
SELECT
product_name,
category_name,
sales_amount,
DENSE_RANK() OVER (
ORDER BY sales_amount DESC
) AS dense_rank_no
FROM sales;結果のイメージです。
| product_name | category_name | sales_amount | dense_rank_no |
|---|---|---|---|
| ノートPC | 家電 | 300000 | 1 |
| スマートフォン | 家電 | 300000 | 1 |
| デスクチェア | 家具 | 200000 | 2 |
| デスク | 家具 | 200000 | 2 |
| 本棚 | 家具 | 150000 | 3 |
| イヤホン | 家電 | 120000 | 4 |
| Spring Boot本 | 書籍 | 120000 | 4 |
| Java入門書 | 書籍 | 90000 | 5 |
| SQL入門書 | 書籍 | 90000 | 5 |
| キーボード | 家電 | 80000 | 6 |
| ライト | 家具 | 70000 | 7 |
ノートPCとスマートフォンは1位です。
次のデスクチェアとデスクは2位です。
順位が飛んでいません。
MySQL公式ドキュメントでは、DENSE_RANKはパーティション内の現在行のランクをギャップなしで返し、同じ値のピアには同じランクを割り当てると説明されています。
ROW_NUMBER、RANK、DENSE_RANKの違い
ここで3つの違いを整理します。
| 関数 | 同点の扱い | 順位の飛び | 例 |
|---|---|---|---|
| ROW_NUMBER | 同点でも別順位 | なし | 1, 2, 3, 4 |
| RANK | 同点は同じ順位 | あり | 1, 1, 3, 3, 5 |
| DENSE_RANK | 同点は同じ順位 | なし | 1, 1, 2, 2, 3 |
この違いは、ランキング機能を作るときにとても重要です。
どれを使うべきかは、業務ルールによって変わります。
| やりたいこと | おすすめ関数 |
|---|---|
| 同点でも必ず1件ずつ順位を付けたい | ROW_NUMBER |
| 同点は同じ順位にし、次の順位は飛ばしたい | RANK |
| 同点は同じ順位にし、次の順位を飛ばしたくない | DENSE_RANK |
新人エンジニアは、まず「同点をどう扱うか」を確認してから関数を選びましょう。
SQLを書く前に、ランキングのルールを決めろ!
PARTITION BYでカテゴリ別ランキングを出す
次に、カテゴリごとのランキングを出します。
カテゴリ別ランキングでは、PARTITION BYを使います。
SELECT
product_name,
category_name,
sales_amount,
RANK() OVER (
PARTITION BY category_name
ORDER BY sales_amount DESC
) AS category_rank
FROM sales;PARTITION BY category_nameは、「カテゴリごとに順位を付ける」という意味です。
家電は家電の中でランキング。
家具は家具の中でランキング。
書籍は書籍の中でランキング。
このように、グループごとの順位を出せます。
MySQL公式ドキュメントでは、OVER句内のPARTITION BYはクエリ行をグループに分割し、ウィンドウ関数の結果は現在行が属するパーティションの行に基づいて計算されると説明されています。
カテゴリ別ランキングの結果イメージ
| product_name | category_name | sales_amount | category_rank |
|---|---|---|---|
| ノートPC | 家電 | 300000 | 1 |
| スマートフォン | 家電 | 300000 | 1 |
| イヤホン | 家電 | 120000 | 3 |
| キーボード | 家電 | 80000 | 4 |
| デスクチェア | 家具 | 200000 | 1 |
| デスク | 家具 | 200000 | 1 |
| 本棚 | 家具 | 150000 | 3 |
| ライト | 家具 | 70000 | 4 |
| Spring Boot本 | 書籍 | 120000 | 1 |
| Java入門書 | 書籍 | 90000 | 2 |
| SQL入門書 | 書籍 | 90000 | 2 |
カテゴリごとに1位が存在しています。
全体ランキングではなく、カテゴリの中での順位です。
たとえるなら、学校全体の成績順位ではなく、クラスごとの順位です。
PARTITION BYを使わない場合と使う場合
PARTITION BYを使わない場合です。
RANK() OVER (
ORDER BY sales_amount DESC
)全体で1つのランキングになります。
PARTITION BYを使う場合です。
RANK() OVER (
PARTITION BY category_name
ORDER BY sales_amount DESC
)category_nameごとにランキングされます。
| 書き方 | ランキングの範囲 |
|---|---|
| PARTITION BYなし | 全体ランキング |
| PARTITION BY category_name | カテゴリ別ランキング |
PARTITION BYは、ランキングの部屋を分けるイメージです。
家電の部屋、家具の部屋、書籍の部屋を作り、それぞれの部屋の中で順位を付けます。
カテゴリごとの上位3件を取得する
ランキングを出したあと、「上位3件だけ欲しい」ということがあります。
たとえば、カテゴリごとの売上上位3件を取得したい場合です。
この場合、ウィンドウ関数で付けた順位を外側のSELECTで絞り込みます。
SELECT
product_name,
category_name,
sales_amount,
category_rank
FROM (
SELECT
product_name,
category_name,
sales_amount,
ROW_NUMBER() OVER (
PARTITION BY category_name
ORDER BY sales_amount DESC, sale_id ASC
) AS category_rank
FROM sales
) AS ranked_sales
WHERE category_rank <= 3
ORDER BY category_name, category_rank;内側のSELECTで、カテゴリごとに順位を付けています。
外側のSELECTで、category_rankが3以下の行だけを取り出しています。
WHERE category_rank <= 3ここでサブクエリを使っている理由は、ウィンドウ関数の実行タイミングにあります。
MySQL公式ドキュメントでは、ウィンドウ関数はSELECTリストとORDER BY句でのみ使用でき、WHERE、GROUP BY、HAVINGの処理後にウィンドウ処理が行われると説明されています。
つまり、同じSELECT内のWHEREで直接category_rankを使って絞り込むことはできません。
一度サブクエリで順位を付けてから、外側でWHEREを使う必要があります。
CTEで上位3件を取得する
MySQL 8では、WITH句、つまりCTEを使って書くこともできます。
CTEは、Common Table Expressionの略です。
一時的な名前付きの検索結果を作るようなイメージです。
WITH ranked_sales AS (
SELECT
product_name,
category_name,
sales_amount,
ROW_NUMBER() OVER (
PARTITION BY category_name
ORDER BY sales_amount DESC, sale_id ASC
) AS category_rank
FROM sales
)
SELECT
product_name,
category_name,
sales_amount,
category_rank
FROM ranked_sales
WHERE category_rank <= 3
ORDER BY category_name, category_rank;サブクエリよりも、処理の段階が見えやすくなります。
まず ranked_sales で順位を付ける
↓
その結果から category_rank が3以下を取り出す
複雑なランキングSQLでは、CTEを使うと読みやすくなることがあります。
ROW_NUMBERで上位3件にする場合の注意
ROW_NUMBERを使って上位3件を取ると、各カテゴリから必ず最大3行だけ取れます。
ROW_NUMBER() OVER (
PARTITION BY category_name
ORDER BY sales_amount DESC, sale_id ASC
)ただし、同点がある場合でも、3件で切られます。
たとえば、3位が同点で複数件ある場合でも、ROW_NUMBERなら3件までです。
同点も含めて上位3位まで出したい場合は、RANKやDENSE_RANKを使う必要があります。
| 目的 | 使う関数 |
|---|---|
| カテゴリごとに必ず3件までにしたい | ROW_NUMBER |
| 同点の3位も含めたい | RANKまたはDENSE_RANK |
ここも業務ルール次第です。
「3件」なのか、「3位まで」なのかを確認しましょう。
似ているようで、結果が変わります。
RANKで上位3位までを取得する
同点を含めて上位3位まで出したい場合は、RANKを使います。
WITH ranked_sales AS (
SELECT
product_name,
category_name,
sales_amount,
RANK() OVER (
PARTITION BY category_name
ORDER BY sales_amount DESC
) AS category_rank
FROM sales
)
SELECT
product_name,
category_name,
sales_amount,
category_rank
FROM ranked_sales
WHERE category_rank <= 3
ORDER BY category_name, category_rank, sales_amount DESC;RANKでは、同点が同じ順位になります。
そのため、3位が複数件あれば、3件を超えて結果が返ることがあります。
これはバグではありません。
「3件まで」ではなく「3位まで」というルールだからです。
ROW_NUMBERとRANKの選び方
上位ランキングでは、ROW_NUMBERとRANKの選び方がとても重要です。
| 要件 | SQL上の考え方 | おすすめ |
|---|---|---|
| 上位10件を表示したい | 件数で切る | ROW_NUMBER |
| 上位10位まで表示したい | 順位で切る | RANKまたはDENSE_RANK |
| 同点でも片方だけ選びたい | 並び順を追加して1件ずつ番号付け | ROW_NUMBER |
| 同点は同順位として全員表示したい | 同点を同じ順位にする | RANK |
ランキング画面を作るときは、SQLを書く前に仕様を確認してください。
「上位3件」なのか。
「上位3位」なのか。
この違いを確認しないと、後で画面の件数が想定と違うと言われることがあります。
ORDER BYはOVERの中と最後の両方に書くことがある
ウィンドウ関数では、ORDER BYが2種類出てくることがあります。
SELECT
product_name,
sales_amount,
RANK() OVER (
ORDER BY sales_amount DESC
) AS rank_no
FROM sales
ORDER BY rank_no, product_name;OVERの中のORDER BYは、順位を決めるための並び順です。
最後のORDER BYは、最終的な表示順です。
| ORDER BYの場所 | 意味 |
|---|---|
| OVER内のORDER BY | 順位計算のための順番 |
| SELECT文最後のORDER BY | 結果表示のための順番 |
この2つを混同しないでください。
OVER内のORDER BYは「採点順」。
最後のORDER BYは「一覧表の表示順」です。
売上合計を出してからランキングする
実務では、明細データをそのままランキングするのではなく、商品ごとに売上を集計してからランキングしたいことがあります。
たとえば、sales_detailsのような売上明細テーブルがある場合です。
CREATE TABLE sales_details (
detail_id INT PRIMARY KEY AUTO_INCREMENT,
product_name VARCHAR(100) NOT NULL,
category_name VARCHAR(100) NOT NULL,
sales_amount INT NOT NULL,
sold_at DATE NOT NULL
);商品ごとの売上合計を出してからランキングするSQLです。
WITH product_sales AS (
SELECT
product_name,
category_name,
SUM(sales_amount) AS total_sales
FROM sales_details
GROUP BY product_name, category_name
),
ranked_product_sales AS (
SELECT
product_name,
category_name,
total_sales,
RANK() OVER (
ORDER BY total_sales DESC
) AS sales_rank
FROM product_sales
)
SELECT
product_name,
category_name,
total_sales,
sales_rank
FROM ranked_product_sales
ORDER BY sales_rank, product_name;このSQLでは、2段階に分けています。
1. product_salesで商品ごとの売上合計を作る 2. ranked_product_salesで売上合計にランキングを付ける
集計してからランキングする。
実務では、この形がよく出てきます。
月別ランキングを出す
月ごとのランキングを出したい場合は、PARTITION BYに年月を使います。
WITH monthly_product_sales AS (
SELECT
DATE_FORMAT(sold_at, '%Y-%m') AS sales_month,
product_name,
SUM(sales_amount) AS total_sales
FROM sales_details
GROUP BY
DATE_FORMAT(sold_at, '%Y-%m'),
product_name
),
ranked_monthly_sales AS (
SELECT
sales_month,
product_name,
total_sales,
RANK() OVER (
PARTITION BY sales_month
ORDER BY total_sales DESC
) AS monthly_rank
FROM monthly_product_sales
)
SELECT
sales_month,
product_name,
total_sales,
monthly_rank
FROM ranked_monthly_sales
ORDER BY sales_month, monthly_rank;このSQLでは、月ごとに売上合計を出し、月ごとのランキングを付けています。
PARTITION BY sales_month
この指定により、2026-01は2026-01の中でランキング、2026-02は2026-02の中でランキングされます。
月ごとのランキング、カテゴリごとのランキング、部署ごとのランキングなど、グループ別ランキングではPARTITION BYが活躍します。
DAOでランキングSQLを使う例
JavaのDAOでランキングSQLを使う場合も、普通のSELECTと同じようにResultSetから値を取り出します。
まず、DTOを作ります。
public class ProductRankingDto {
private String productName;
private String categoryName;
private int salesAmount;
private int ranking;
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public String getCategoryName() {
return categoryName;
}
public void setCategoryName(String categoryName) {
this.categoryName = categoryName;
}
public int getSalesAmount() {
return salesAmount;
}
public void setSalesAmount(int salesAmount) {
this.salesAmount = salesAmount;
}
public int getRanking() {
return ranking;
}
public void setRanking(int ranking) {
this.ranking = ranking;
}
}DAOの例です。
public List<ProductRankingDto> findProductRanking() {
String sql =
"SELECT "
+ " product_name, "
+ " category_name, "
+ " sales_amount, "
+ " RANK() OVER ( "
+ " ORDER BY sales_amount DESC "
+ " ) AS ranking "
+ "FROM sales "
+ "ORDER BY ranking, product_name";
List<ProductRankingDto> rankingList = new ArrayList<>();
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
ProductRankingDto dto = new ProductRankingDto();
dto.setProductName(rs.getString("product_name"));
dto.setCategoryName(rs.getString("category_name"));
dto.setSalesAmount(rs.getInt("sales_amount"));
dto.setRanking(rs.getInt("ranking"));
rankingList.add(dto);
}
return rankingList;
} catch (SQLException e) {
throw new RuntimeException("商品ランキングの取得に失敗しました。", e);
}
}rankingという別名を付けているので、Java側ではrs.getInt("ranking")で取得できます。
RANK() OVER (
ORDER BY sales_amount DESC
) AS rankingSQLで計算したランキング列も、ResultSetでは普通の列のように扱えます。
よくあるミス
MySQL 5.7で使おうとしてしまう
ウィンドウ関数はMySQL 8で使う前提です。
MySQL 5.7ではROW_NUMBERやRANKは基本的に使えません。
エラーになる場合は、まずMySQLのバージョンを確認してください。
SELECT VERSION();研修環境や古い現場では、MySQL 5.7が残っていることがあります。
SQLが正しくても、DBバージョンが対応していなければ動きません。
OVERを書き忘れる
悪い例です。
SELECT
product_name,
RANK() AS ranking
FROM sales;
良い例です。
SELECT
product_name,
RANK() OVER (
ORDER BY sales_amount DESC
) AS ranking
FROM sales;ROW_NUMBER、RANK、DENSE_RANKにはOVER句が必要です。
ORDER BYを書かずにROW_NUMBERを使う
悪い例です。
SELECT
product_name,
ROW_NUMBER() OVER () AS ranking
FROM sales;この書き方でも番号は付く場合があります。
しかし、どの順番で番号が付くのかがランキングとして分かりません。
良い例です。
SELECT
product_name,
ROW_NUMBER() OVER (
ORDER BY sales_amount DESC
) AS ranking
FROM sales;ランキングでは、何を基準に順位を付けるのかを明確にしてください。
WHEREでランキング列を直接絞ろうとする
悪い例です。
SELECT
product_name,
sales_amount,
ROW_NUMBER() OVER (
ORDER BY sales_amount DESC
) AS ranking
FROM sales
WHERE ranking <= 3;この書き方はできません。
rankingはSELECTで作られる列であり、WHEREの時点ではまだ使えません。
良い例です。
SELECT
product_name,
sales_amount,
ranking
FROM (
SELECT
product_name,
sales_amount,
ROW_NUMBER() OVER (
ORDER BY sales_amount DESC
) AS ranking
FROM sales
) AS ranked_sales
WHERE ranking <= 3;ランキングで絞り込みたい場合は、サブクエリやCTEを使いましょう。
同点の扱いを決めずに実装する
ランキング機能では、同点の扱いが重要です。
売上が同じ商品は同順位にするのか
同順位にした場合、次の順位を飛ばすのか
画面には必ず10件だけ表示するのか
10位が複数ある場合は全件表示するのかこのような仕様を決めずにSQLを書くと、後で手戻りになります。
ランキングSQLでは、同点ルールを必ず確認してください。
パフォーマンス上の注意
ウィンドウ関数は便利ですが、大量データに対して使う場合は注意が必要です。
ORDER BYやPARTITION BYに使う列には、インデックスを検討します。
CREATE INDEX idx_sales_category_amount
ON sales (category_name, sales_amount);ただし、インデックスを作れば必ず速くなるわけではありません。
データ量、条件、ORDER BY、GROUP BY、実行計画によって変わります。
実務では、EXPLAINで実行計画を確認します。
EXPLAIN
SELECT
product_name,
category_name,
sales_amount,
RANK() OVER (
PARTITION BY category_name
ORDER BY sales_amount DESC
) AS category_rank
FROM sales;新人エンジニアの段階では、まず正しいSQLを書けることが大切です。
その次に、インデックス、EXPLAIN、実行計画を学ぶとよいです。
新人エンジニアが最初に覚えるべき型
まず、全体ランキングです。
SELECT
列名,
RANK() OVER (
ORDER BY ランキング基準列 DESC
) AS ranking
FROM テーブル名;次に、グループ別ランキングです。
SELECT
列名,
グループ列,
RANK() OVER (
PARTITION BY グループ列
ORDER BY ランキング基準列 DESC
) AS ranking
FROM テーブル名;最後に、上位N件の取得です。
WITH ranked_data AS (
SELECT
列名,
ROW_NUMBER() OVER (
PARTITION BY グループ列
ORDER BY ランキング基準列 DESC
) AS ranking
FROM テーブル名
)
SELECT
*
FROM ranked_data
WHERE ranking <= N;最初はこの3パターンを覚えてください。
全体ランキング。
グループ別ランキング。
上位N件。
この3つが使えると、ランキングSQLの基本はかなり書けます。
まとめ
MySQL 8のウィンドウ関数を使うと、ランキングをSQLだけで分かりやすく出せます。
ROW_NUMBER、RANK、DENSE_RANKは似ていますが、同点の扱いが違います。
| 関数 | 特徴 | 使いどころ |
|---|---|---|
| ROW_NUMBER | 同点でも別々の番号を付ける | 上位10件など件数で切りたい |
| RANK | 同点は同じ順位、次の順位は飛ぶ | 一般的な競技順位に近いランキング |
| DENSE_RANK | 同点は同じ順位、次の順位は飛ばない | 順位番号を詰めたいランキング |
| PARTITION BY | グループごとに順位を付ける | カテゴリ別、部署別、月別ランキング |
| ORDER BY | 順位を決める基準 | 売上順、点数順、日付順など |
一言でまとめるなら、ウィンドウ関数は「行を消さずに、各行へ順位を付けるSQLの機能」です。
新人エンジニアは、まず次の流れを覚えてください。
ランキングの基準を決める
↓
同点の扱いを決める
↓
ROW_NUMBER、RANK、DENSE_RANKから選ぶ
↓
全体ランキングならPARTITION BYなし
↓
カテゴリ別ランキングならPARTITION BYを使う
↓
上位N件で絞るならサブクエリまたはCTEを使う
SQLを書く前に、まず「同点をどう扱うか」「全体ランキングかグループ別ランキングか」「上位N件なのか上位N位なのか」を確認してください。
今後の学習では、ROW_NUMBER、RANK、DENSE_RANK、PARTITION BY、ORDER BY、CTE、サブクエリ、GROUP BY後のランキング、EXPLAINによる実行計画確認を順番に学ぶとよいです。まずは今回のsalesテーブルを使い、全体ランキング、カテゴリ別ランキング、カテゴリ別上位3件を自分で書いてみましょう!