はじめに
SQLを学んでいると、こんな場面に出くわしませんか?
- 「部署ごとの合計を出したいけど、元のデータも残したい...」
- 「売上ランキングを作りたいけど、GROUP BYだと行がまとまっちゃう...」
- 「前日との差分を計算したい...」
こんな悩みを解決してくれるのが ウィンドウ関数 です!
この記事では、PostgreSQLのウィンドウ関数について、図解を交えながら初心者向けにわかりやすく解説します。
対象読者
- SQLの基本(SELECT, WHERE, GROUP BY)は理解している
-
PARTITION BYやOVERを見たことはあるけど、よくわからない - ウィンドウ関数を実務で使えるようになりたい
目次
ウィンドウ関数とは?
ウィンドウ関数とは、テーブルの各行に対して、関連する行のグループ(=ウィンドウ)を使って計算を行う関数です。
ポイントは 「行を集約せずに、計算結果を各行に追加できる」 ことです。
イメージ図
┌─────────────────────────────────────────────────────┐
│ 通常の集約関数(SUM + GROUP BY) │
│ │
│ 行1 ─┐ │
│ 行2 ─┼─→ まとめて1行に集約 → 結果: 1行 │
│ 行3 ─┘ │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ ウィンドウ関数(SUM + OVER) │
│ │
│ 行1 → 行1 + 計算結果 │
│ 行2 → 行2 + 計算結果 → 結果: 3行(元のまま!) │
│ 行3 → 行3 + 計算結果 │
└─────────────────────────────────────────────────────┘
GROUP BY との違い
実際のデータで比較してみましょう。
サンプルデータ
CREATE TABLE sales (
id SERIAL PRIMARY KEY,
department VARCHAR(50),
employee VARCHAR(50),
amount INTEGER
);
INSERT INTO sales (department, employee, amount) VALUES
('営業', '田中', 100),
('営業', '佐藤', 150),
('営業', '山田', 200),
('開発', '鈴木', 180),
('開発', '高橋', 120);
| id | department | employee | amount |
|---|---|---|---|
| 1 | 営業 | 田中 | 100 |
| 2 | 営業 | 佐藤 | 150 |
| 3 | 営業 | 山田 | 200 |
| 4 | 開発 | 鈴木 | 180 |
| 5 | 開発 | 高橋 | 120 |
GROUP BY の場合
SELECT
department,
SUM(amount) AS total
FROM sales
GROUP BY department;
結果:2行に集約される
| department | total |
|---|---|
| 営業 | 450 |
| 開発 | 300 |
ウィンドウ関数の場合
SELECT
department,
employee,
amount,
SUM(amount) OVER (PARTITION BY department) AS dept_total
FROM sales;
結果:5行のまま、各行に部署合計が追加される
| department | employee | amount | dept_total |
|---|---|---|---|
| 営業 | 田中 | 100 | 450 |
| 営業 | 佐藤 | 150 | 450 |
| 営業 | 山田 | 200 | 450 |
| 開発 | 鈴木 | 180 | 300 |
| 開発 | 高橋 | 120 | 300 |
ウィンドウ関数を使うと「個人の売上」と「部署の合計」を同時に表示できます!
基本構文を理解しよう
構文
関数名() OVER (
[PARTITION BY カラム名]
[ORDER BY カラム名]
[フレーム句]
)
各要素の説明
| 要素 | 必須 | 説明 |
|---|---|---|
OVER() |
✅ | ウィンドウ関数であることを示す。空でもOK |
PARTITION BY |
❌ | データをグループ分けする(省略時は全行が1グループ) |
ORDER BY |
❌ | ウィンドウ内での順序を指定 |
| フレーム句 | ❌ | 計算対象の範囲を細かく指定(後述) |
PARTITION BY のイメージ
PARTITION BY department を指定すると...
┌──────────────────┐ ┌──────────────────┐
│ 営業グループ │ │ 開発グループ │
│ ├─ 田中: 100 │ │ ├─ 鈴木: 180 │
│ ├─ 佐藤: 150 │ │ └─ 高橋: 120 │
│ └─ 山田: 200 │ │ │
│ │ │ │
│ → 合計: 450 │ │ → 合計: 300 │
└──────────────────┘ └──────────────────┘
各グループ(パーティション)ごとに独立して計算される
ORDER BY のイメージ
ORDER BY を指定すると、累計のような計算ができます。
SUM(amount) OVER (ORDER BY id)
id=1: 100 → 累計: 100
id=2: 150 → 累計: 100 + 150 = 250
id=3: 200 → 累計: 100 + 150 + 200 = 450
id=4: 180 → 累計: 100 + 150 + 200 + 180 = 630
id=5: 120 → 累計: 100 + 150 + 200 + 180 + 120 = 750
PARTITION BY と ORDER BY は組み合わせて使うことも多いです!
実践!よく使うウィンドウ関数7選
1. ROW_NUMBER() - 連番を振る
各行に一意の連番を振ります。
SELECT
department,
employee,
amount,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY amount DESC) AS rank
FROM sales;
| department | employee | amount | rank |
|---|---|---|---|
| 営業 | 山田 | 200 | 1 |
| 営業 | 佐藤 | 150 | 2 |
| 営業 | 田中 | 100 | 3 |
| 開発 | 鈴木 | 180 | 1 |
| 開発 | 高橋 | 120 | 2 |
ユースケース: 部署ごとの売上トップ3を取得する など
2. RANK() - 順位を付ける(同率あり・ギャップあり)
同じ値には同じ順位を付け、次の順位は飛ばします。
-- 同じ金額のデータを追加して試す
SELECT
employee,
amount,
RANK() OVER (ORDER BY amount DESC) AS rank
FROM sales;
山田: 200 → 1位
鈴木: 180 → 2位
佐藤: 150 → 3位
高橋: 120 → 4位
田中: 100 → 5位
もし佐藤と高橋が同じ150だったら...
山田: 200 → 1位
鈴木: 180 → 2位
佐藤: 150 → 3位 ← 同率
高橋: 150 → 3位 ← 同率
田中: 100 → 5位 ← 4位が飛ばされる!
3. DENSE_RANK() - 順位を付ける(同率あり・ギャップなし)
同じ値には同じ順位を付けますが、次の順位は飛ばしません。
山田: 200 → 1位
鈴木: 180 → 2位
佐藤: 150 → 3位 ← 同率
高橋: 150 → 3位 ← 同率
田中: 100 → 4位 ← 連続している!
4. LAG() - 前の行の値を取得
SELECT
employee,
amount,
LAG(amount, 1) OVER (ORDER BY id) AS prev_amount,
amount - LAG(amount, 1) OVER (ORDER BY id) AS diff
FROM sales;
| employee | amount | prev_amount | diff |
|---|---|---|---|
| 田中 | 100 | NULL | NULL |
| 佐藤 | 150 | 100 | 50 |
| 山田 | 200 | 150 | 50 |
| 鈴木 | 180 | 200 | -20 |
| 高橋 | 120 | 180 | -60 |
ユースケース: 前日比、前月比の計算
LAG / LEAD の引数を詳しく解説
LAG() と LEAD() は最大3つの引数を取ります。
LAG(カラム名, オフセット, デフォルト値) OVER (...)
LEAD(カラム名, オフセット, デフォルト値) OVER (...)
| 引数 | 必須 | 説明 |
|---|---|---|
| カラム名 | ✅ | 取得したいカラム |
| オフセット | ❌ | 何行前/後か(省略時は 1) |
| デフォルト値 | ❌ | 該当行がない場合の値(省略時は NULL) |
オフセットを変えた例:
SELECT
id,
employee,
amount,
LAG(amount, 1) OVER (ORDER BY id) AS "1行前",
LAG(amount, 2) OVER (ORDER BY id) AS "2行前",
LAG(amount, 3) OVER (ORDER BY id) AS "3行前"
FROM sales;
| id | employee | amount | 1行前 | 2行前 | 3行前 |
|---|---|---|---|---|---|
| 1 | 田中 | 100 | NULL | NULL | NULL |
| 2 | 佐藤 | 150 | 100 | NULL | NULL |
| 3 | 山田 | 200 | 150 | 100 | NULL |
| 4 | 鈴木 | 180 | 200 | 150 | 100 |
| 5 | 高橋 | 120 | 180 | 200 | 150 |
デフォルト値を指定する例:
前の行がない場合(最初の行など)は NULL になりますが、第3引数でデフォルト値を指定できます。
SELECT
id,
amount,
LAG(amount, 1, 0) OVER (ORDER BY id) AS prev_amount
FROM sales;
| id | amount | prev_amount |
|---|---|---|
| 1 | 100 | 0 |
| 2 | 150 | 100 |
| 3 | 200 | 150 |
デフォルト値を指定すると NULL の代わりにその値が入るので、計算時のエラーを防げます!
省略した場合:
第2引数を省略すると、自動的に 1 が使われます。
-- この2つは同じ意味
LAG(amount) OVER (ORDER BY id)
LAG(amount, 1) OVER (ORDER BY id)
1行前/後を取りたいだけなら省略してもOKです。
5. LEAD() - 次の行の値を取得
LAG() が「前の行」なら、LEAD() は「次の行」を取得します。引数の使い方は LAG() と同じです。
SELECT
employee,
amount,
LEAD(amount, 1) OVER (ORDER BY id) AS next_amount
FROM sales;
| employee | amount | next_amount |
|---|---|---|
| 田中 | 100 | 150 |
| 佐藤 | 150 | 200 |
| 山田 | 200 | 180 |
| 鈴木 | 180 | 120 |
| 高橋 | 120 | NULL |
6. FIRST_VALUE() / LAST_VALUE() - 最初・最後の値
SELECT
department,
employee,
amount,
FIRST_VALUE(employee) OVER (
PARTITION BY department
ORDER BY amount DESC
) AS top_performer
FROM sales;
| department | employee | amount | top_performer |
|---|---|---|---|
| 営業 | 山田 | 200 | 山田 |
| 営業 | 佐藤 | 150 | 山田 |
| 営業 | 田中 | 100 | 山田 |
| 開発 | 鈴木 | 180 | 鈴木 |
| 開発 | 高橋 | 120 | 鈴木 |
7. SUM() / AVG() / COUNT() をウィンドウ関数として使う
通常の集約関数も OVER を付けるとウィンドウ関数になります!
SELECT
department,
employee,
amount,
SUM(amount) OVER (PARTITION BY department) AS dept_total,
AVG(amount) OVER (PARTITION BY department) AS dept_avg,
COUNT(*) OVER (PARTITION BY department) AS dept_count,
-- 累計
SUM(amount) OVER (ORDER BY id) AS running_total
FROM sales;
実務でよくある使用例
ユースケース1: 部署ごとの売上トップ3を取得
WITH ranked AS (
SELECT
department,
employee,
amount,
ROW_NUMBER() OVER (
PARTITION BY department
ORDER BY amount DESC
) AS rank
FROM sales
)
SELECT * FROM ranked WHERE rank <= 3;
ROW_NUMBER() を使って順位を付け、CTEやサブクエリでフィルタリングするのが定番パターンです。
ユースケース2: 前月比を計算する
SELECT
month,
revenue,
LAG(revenue, 1) OVER (ORDER BY month) AS prev_month,
ROUND(
(revenue - LAG(revenue, 1) OVER (ORDER BY month)) * 100.0
/ LAG(revenue, 1) OVER (ORDER BY month),
1
) AS growth_rate
FROM monthly_sales;
ユースケース3: 累計売上と達成率
SELECT
date,
amount,
SUM(amount) OVER (ORDER BY date) AS cumulative,
ROUND(
SUM(amount) OVER (ORDER BY date) * 100.0 / 1000000,
1
) AS achievement_rate
FROM daily_sales;
ユースケース4: 移動平均(直近3件の平均)
SELECT
date,
amount,
AVG(amount) OVER (
ORDER BY date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS moving_avg_3
FROM daily_sales;
ROWS BETWEEN はフレーム句と呼ばれ、計算対象の範囲を細かく指定できます。
よくあるエラーと対処法
エラー1: OVER() を付け忘れる
-- ❌ エラー
SELECT ROW_NUMBER() FROM sales;
-- ✅ 正しい
SELECT ROW_NUMBER() OVER () FROM sales;
エラー2: ウィンドウ関数を WHERE で使おうとする
-- ❌ エラー
SELECT * FROM sales
WHERE ROW_NUMBER() OVER (ORDER BY amount DESC) <= 3;
-- ✅ 正しい(サブクエリやCTEを使う)
WITH ranked AS (
SELECT *, ROW_NUMBER() OVER (ORDER BY amount DESC) AS rn
FROM sales
)
SELECT * FROM ranked WHERE rn <= 3;
ウィンドウ関数は SELECT と ORDER BY でしか使えません!
WHERE や HAVING では使えないので、CTEやサブクエリで囲みましょう。
まとめ
ウィンドウ関数のポイント
- 行を集約せずに計算結果を追加できる
OVER()がウィンドウ関数の目印PARTITION BYでグループ分け、ORDER BYで順序指定- WHERE では使えないのでCTE/サブクエリを活用
よく使う関数チートシート
| 関数 | 用途 |
|---|---|
ROW_NUMBER() |
連番を振る |
RANK() |
順位(同率でギャップあり) |
DENSE_RANK() |
順位(同率でギャップなし) |
LAG(col, n) |
n行前の値 |
LEAD(col, n) |
n行後の値 |
FIRST_VALUE() |
最初の値 |
SUM/AVG/COUNT() OVER() |
集約しない集計 |
おすすめの学習方法
- まずは
ROW_NUMBER()とPARTITION BYを使いこなす - 次に
LAG()で前後比較をマスター - 実務で「これウィンドウ関数で書けるかも?」と意識する
参考リンク
最後まで読んでいただきありがとうございました!