はじめに
久々にSQL Serverを使うことがありましたので、よく使うウィンドウ関数についてまとめようと思います。
お知らせ(採用情報)
最後にお知らせとなりますが、AppTime では一緒に働くメンバーを募集しております。
詳しくは採用情報ページをご確認ください。
みなさまからのご応募をお待ちしております。
1. ウィンドウ関数とは
ウィンドウ関数(Window Function / 分析関数)とは、行を集約せずに、指定した「窓(ウィンドウ)」の範囲内で計算を行う関数群です。
通常の集約関数(GROUP BY + SUM など)は複数行を1行にまとめますが、ウィンドウ関数は元の行数を保ったまま各行に計算結果を付与できます。
| 方式 | 行数 | 概要 |
|---|---|---|
| 通常の集約(GROUP BY) | 減る | 指定キーで折り畳まれる |
| ウィンドウ関数 | 保たれる | 各行に結果を付与 |
SQL Server では SQL Server 2005 以降でウィンドウ関数がサポートされています。本記事では SQL Server 2012以降の構文を前提とします。
2. 基本構文(OVER 句)
ウィンドウ関数はすべて OVER() 句と組み合わせて使います。
関数名() OVER (
[PARTITION BY 列名, ...] -- グループを分ける(省略可)
[ORDER BY 列名 [ASC|DESC]] -- 行の並び順を定義
)
| 句 | 役割 | 省略 |
|---|---|---|
PARTITION BY |
計算の単位となるグループを定義(GROUP BY と似ているが行を折り畳まない) | 可(全行が 1 ウィンドウになる) |
ORDER BY |
ウィンドウ内の行の順序を定義。順位系関数では必須 | ROW_NUMBER 等では不可 |
3. ROW_NUMBER — 連番を振る
ウィンドウ内の各行に 重複なし・飛び番なし の連番(1, 2, 3, …)を振ります。同じORDER BY値の行があっても、必ず異なる番号が付きます(どちらが先になるかは不定)。
サンプルデータ
-- 社員の売上テーブル
SELECT * FROM Sales;
| EmpName | Dept | Sales |
|---|---|---|
| Alice | 営業1課 | 900 |
| Bob | 営業1課 | 750 |
| Carol | 営業1課 | 750 |
| Dave | 営業2課 | 800 |
| Eve | 営業2課 | 600 |
使用例
SELECT
EmpName,
Dept,
Sales,
ROW_NUMBER() OVER (
PARTITION BY Dept
ORDER BY Sales DESC
) AS RowNum
FROM Sales;
結果
| EmpName | Dept | Sales | RowNum |
|---|---|---|---|
| Alice | 営業1課 | 900 | 1 |
| Bob | 営業1課 | 750 | 2 |
| Carol | 営業1課 | 750 | 3 |
| Dave | 営業2課 | 800 | 1 |
| Eve | 営業2課 | 600 | 2 |
BobとCarolのSalesは同じ750ですが、ROW_NUMBERは必ず異なる番号を付けます。どちらが2になるかは ORDER BY に追加の列を指定しない限り不定です。順位付けを正確にしたい場合は RANK / DENSE_RANKを検討してください。
典型的な使い方:各グループの上位 N 件を取得
-- 各部署で売上トップ 1 名を取得
SELECT * FROM (
SELECT
EmpName, Dept, Sales,
ROW_NUMBER() OVER (
PARTITION BY Dept ORDER BY Sales DESC
) AS rn
FROM Sales
) AS t
WHERE rn = 1;
4. RANK — 同率を考慮したランキング
ウィンドウ内で同じ ORDER BY 値の行には同じ順位を付けます。同率が存在する場合、次の順位はその件数分だけ飛び番になります(例:1, 2, 2, 4)。
使用例
SELECT
EmpName, Dept, Sales,
RANK() OVER (
PARTITION BY Dept
ORDER BY Sales DESC
) AS Rank
FROM Sales;
結果
| EmpName | Dept | Sales | Rank |
|---|---|---|---|
| Alice | 営業1課 | 900 | 1 |
| Bob | 営業1課 | 750 | 2 |
| Carol | 営業1課 | 750 | 2 |
| John | 営業1課 | 600 | 4 |
| Dave | 営業2課 | 800 | 1 |
| Eve | 営業2課 | 600 | 2 |
Bob・Carol は同率2位なので、3位は存在せず次は4位になります(本例では3人なので4位は現れませんが、行が増えれば飛び番が発生します)。
5. DENSE_RANK — 連番を保ったランキング
RANK と同様に同率には同じ順位を付けますが、次の順位は飛び番にならない連番(1, 2, 2, 3)になります。「何番目に高い値か」を知りたいときに適しています。
使用例
SELECT
EmpName, Dept, Sales,
DENSE_RANK() OVER (
PARTITION BY Dept
ORDER BY Sales DESC
) AS DenseRank
FROM Sales;
結果
| EmpName | Dept | Sales | DenseRank |
|---|---|---|---|
| Alice | 営業1課 | 900 | 1 |
| Bob | 営業1課 | 750 | 2 |
| Carol | 営業1課 | 750 | 2 |
| John | 営業1課 | 600 | 3 |
| Dave | 営業2課 | 800 | 1 |
| Eve | 営業2課 | 600 | 2 |
「上位Nランク以内の行を取得したい、かつ同率は全員含めたい」場合はDENSE_RANKが最適です。例:WHERE DenseRank <= 3 で上位3ランクを漏れなく取得できます。
6. 3 関数の比較
| 関数 | 同率の扱い | 飛び番 | 主なユースケース |
|---|---|---|---|
ROW_NUMBER |
必ず異なる番号 | なし | 重複排除・ページング・上位N件(1件限定) |
RANK |
同じ順位 | あり | 競技ランキング(同率2位が2人なら次は4位) |
DENSE_RANK |
同じ順位 | なし | グループ内の何番目か・上位Nランク全員取得 |
同じデータに適用した場合の挙動の違い
| Sales | ROW_NUMBER | RANK | DENSE_RANK |
|---|---|---|---|
| 900 | 1 | 1 | 1 |
| 750 | 2 | 2 | 2 |
| 750 | 3 | 2 | 2 |
| 600 | 4 | 4 ← 飛び番 | 3 ← 連番 |
7. 実践的なユースケース
① 重複レコードの削除(ROW_NUMBER)
同一キーで重複しているレコードから最新の1件だけを残す定番パターンです。
-- 重複を除いた最新レコードのみ残す
WITH cte AS (
SELECT
*,
ROW_NUMBER() OVER (
PARTITION BY CustomerId
ORDER BY UpdatedAt DESC
) AS rn
FROM Orders
)
DELETE FROM cte WHERE rn > 1;
② ページング(ROW_NUMBER)
-- 1ページ10件、3ページ目を取得
SELECT * FROM (
SELECT
*,
ROW_NUMBER() OVER (ORDER BY CreatedAt DESC) AS rn
FROM Products
) AS t
WHERE rn BETWEEN 21 AND 30;
-- SQL Server 2012 以降は OFFSET-FETCH が推奨
SELECT * FROM Products
ORDER BY CreatedAt DESC
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
③ グループ内の上位Nランクを全員取得(DENSE_RANK)
-- 各部署で売上上位2ランク以内の全社員を取得
SELECT EmpName, Dept, Sales, dr
FROM (
SELECT
EmpName, Dept, Sales,
DENSE_RANK() OVER (
PARTITION BY Dept
ORDER BY Sales DESC
) AS dr
FROM Sales
) AS t
WHERE dr <= 2;
8. 注意点・パフォーマンス
⚠ WHERE 句でウィンドウ関数を直接使えない
ウィンドウ関数はSELECTリストで評価されるため、WHERE / HAVING では使えません。サブクエリかCTEでラップする必要があります。
-- NG(エラー)
SELECT *, ROW_NUMBER() OVER (ORDER BY Sales) AS rn
FROM Sales WHERE rn = 1; -- ❌ rn は未定義
-- OK(CTE や派生テーブルでラップ)
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (ORDER BY Sales) AS rn
FROM Sales
) t WHERE rn = 1; -- ✅
⚠ ORDER BY の列が同値のとき結果が不定(ROW_NUMBER)
同じ値が複数ある場合、どの行が何番になるかは実行のたびに変わる可能性があります。決定的にしたいときは ORDER BY に一意な列(主キーなど)を追加してください。
-- ✅ Sales が同値でも主キーで決定的に並ぶ
ROW_NUMBER() OVER (ORDER BY Sales DESC, EmpId ASC)
💡 インデックスを活用してパフォーマンスを向上させる
ウィンドウ関数は PARTITION BY・ORDER BY の列にインデックスがあると大幅に速くなります。特に大量データで ORDER BY に使うカラムが複合インデックスの先頭にあるとソートコストが削減されます。
CREATE INDEX IX_Sales_Dept_Sales ON Sales (Dept, Sales DESC);
💡 PARTITION BY を省略すると全行が 1 ウィンドウ
PARTITION BY を指定しない場合、テーブル全体が1つのウィンドウになります。意図通りかどうか確認してください。
9. まとめ
| 関数 | これを使う場面 |
|---|---|
ROW_NUMBER |
重複排除・ページング・グループ内 1 件だけ欲しいとき |
RANK |
スポーツや競技のように「同率2位の次は4位」にしたいとき |
DENSE_RANK |
「上位Nランク以内を全員」「値の順位番号が欲しい」とき |