はじめに
SQL の集計でよく出てくる
- GROUP BY
- OVER (PARTITION BY ...)(ウィンドウ関数)
ですが、
- なんとなく使っている
- 動くから OK にしている
というケースも多いのではないでしょうか。
本記事では
「どちらを使うべきか」
ではなく
「どこで選択を誤ると設計が壊れるか」
という視点で整理します。
OVER(ウィンドウ関数)とは?
ウィンドウ関数は、
1行ずつのデータを保持したまま集計や順位、累計などの情報を追加できる
SQL 構文です。
GROUP BY のように行をまとめるのではなく、
行を消さずに「文脈」を付与するイメージです。
入力テーブル:employees
| employee_id | department_id | salary |
|---|---|---|
| 1 | A | 3000 |
| 2 | A | 4000 |
| 3 | B | 3500 |
| 4 | B | 4500 |
SQLクエリ例
SELECT
employee_id,
department_id,
salary,
AVG(salary) OVER (PARTITION BY department_id) AS dept_avg
FROM employees;
出力テーブル
| employee_id | department_id | salary | dept_avg |
|---|---|---|---|
| 1 | A | 3000 | 3500 |
| 2 | A | 4000 | 3500 |
| 3 | B | 3500 | 4000 |
| 4 | B | 4500 | 4000 |
- 各社員の行はそのまま残る
- 同じ部門の平均給与を dept_avg 列として追加
ポイント:
- 行の責務を保持したまま集計情報を付与できる
- 順位付け(RANK)、累計(SUM OVER)、平均との差(AVG OVER など)も可能
注意点
- WHERE句では直接使えない(サブクエリやCTEが必要)
- 大量データや複雑なパーティションでは計算コストが増える可能性
GROUP BY のアンチパターン
アンチパターン①:行を残したいのに GROUP BY する
やりがちな場面
- 社員ごとの売上や給与レポートを作っているつもりで、部門単位でまとめてしまう
- 後から「社員ごとの情報も必要」となり、再JOINなどで対応しなければならない
入力テーブル:employees
| employee_id | department_id | salary |
|---|---|---|
| 1 | A | 3000 |
| 2 | A | 4000 |
| 3 | B | 3500 |
| 4 | B | 4500 |
SQLクエリ
SELECT
employee_id,
department_id,
AVG(salary)
FROM employees
GROUP BY department_id;
返却されるテーブル
| employee_id | department_id | avg_salary |
|---|---|---|
| ? | A | 3500 |
| ? | B | 4000 |
問題点
- employee_id が意味を失っている
- 社員単位の情報が消える
- 「行の責務」が壊れている
兆候
- 後続で「社員ごとの〇〇」を扱いたくなる
- 再 JOIN が発生する
👉 元の行に意味があるなら OVER を検討すべき
アンチパターン②:GROUP BY → 再 JOIN 前提の設計
やりがちな場面
- 月次レポートで部門ごとの平均給与を出すつもりでGROUP BY
- 後で個別社員ランキングや累計給与も必要になり、クエリが冗長化
入力テーブル:employees
| employee_id | department_id | salary |
|---|---|---|
| 1 | A | 3000 |
| 2 | A | 4000 |
| 3 | B | 3500 |
| 4 | B | 4500 |
SQLクエリ
WITH dept_avg AS (
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
)
SELECT e.*, d.avg_salary
FROM employees e
JOIN dept_avg d
ON e.department_id = d.department_id;
返却されるテーブル
| employee_id | department_id | salary | avg_salary |
|---|---|---|---|
| 1 | A | 3000 | 3500 |
| 2 | A | 4000 | 3500 |
| 3 | B | 3500 | 4000 |
| 4 | B | 4500 | 4000 |
問題点
- クエリが冗長
- 中間集計の責務が不明瞭
- 「集計を付与したいだけ」なのに行を消している
👉 最初から OVER で書けば 1 クエリで完結
アンチパターン③:将来拡張を考えず GROUP BY 固定
やりがちな場面
- 月次レポートで部門ごとの平均給与だけを計算するつもりでGROUP BYを使用
- 後から「個別社員のランキング」「平均との差」「累計給与」などが必要になったとき、既存クエリを全面的に書き換える必要が出る
入力テーブル:employees
| employee_id | department_id | salary |
|---|---|---|
| 1 | A | 3000 |
| 2 | A | 4000 |
| 3 | B | 3500 |
| 4 | B | 4500 |
最初に書いたGROUP BYクエリ(平均だけ)
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id;
返却されるテーブル
| department_id | avg_salary |
|---|---|
| A | 3500 |
| B | 4000 |
後からランキングを追加したい場合
WITH dept_avg AS (
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
)
SELECT e.employee_id, e.department_id, e.salary,
RANK() OVER (PARTITION BY e.department_id ORDER BY e.salary DESC) AS salary_rank
FROM employees e
JOIN dept_avg d
ON e.department_id = d.department_id;
返却されるテーブル
| employee_id | department_id | salary | salary_rank |
|---|---|---|---|
| 2 | A | 4000 | 1 |
| 1 | A | 3000 | 2 |
| 4 | B | 4500 | 1 |
| 3 | B | 3500 | 2 |
- 今は平均だけ
- 次は順位
- その次は平均との差・累計
この状態で GROUP BY を選ぶと
後から全面書き換えになりがち。
👉 行に文脈を付けたいなら OVER の方が拡張しやすい
OVER(ウィンドウ関数)のアンチパターン
アンチパターン①:集計表なのに OVER + DISTINCT
やりがちな場面
- 社員ごとの給与レポートを作るつもりが、部門平均だけを計算してしまう
- 後で社員ごとの情報も必要になり、再JOINやDISTINCTで無理やり行を減らす
SELECT DISTINCT
department_id,
AVG(salary) OVER (PARTITION BY department_id)
FROM employees;
問題点
- 意味的には集計表
- DISTINCT で無理やり行を減らしている
- 可読性が低い
👉 集計結果だけなら素直に GROUP BY
アンチパターン②:WHERE で使えると思っている
やりがちな場面
- 部門ごとの平均給与を算出して社員レコードに付与したい
- 最初は平均だけで良いと思ったが、後で「平均を超えた社員だけ抽出」などの条件が必要になり、書けなくて戸惑う
SELECT *
FROM employees
WHERE salary > AVG(salary) OVER (PARTITION BY department_id);
❌ これは書けない
理由
- WHERE は SELECT より前
- ウィンドウ関数は SELECT フェーズ
👉 サブクエリや CTE が必要
👉 理解不足のサインになりやすい
アンチパターン③:行が不要なのに OVER を多用
やりがちな場面
- 月次レポートや集計表で「部門ごとの合計や平均だけが必要」なのに、社員ごとの行に対して複数のウィンドウ関数を計算してしまう
- 後で最終結果が1行だけで十分なのに、余計な計算コストが発生していることに気づ
AVG(salary) OVER (PARTITION BY department_id)
MAX(salary) OVER (PARTITION BY department_id)
MIN(salary) OVER (PARTITION BY department_id)
問題点
- 計算コストが増える
- 意図が読み取りづらい
👉 最終結果が1行なら GROUP BY
設計判断のアクティビティ図(思考フロー)
開始
↓
「最終成果物は “集計された行” か?」
├─ 集計された行
│ ↓
│ GROUP BY
│
└─ 元データの行を保持したい
↓
「1行1行に業務的な意味があるか?」
├─ ある(社員・注文・ログなど)
│ ↓
│ OVER(ウィンドウ関数)
│
└─ ない(ただの中間データ)
↓
「ランキング・累計・平均との差など
順序や比較が関係するか?」
├─ する
│ ↓
│ OVER
│
└─ しない
↓
「集計結果だけを使うか?」
├─ はい → GROUP BY
└─ いいえ → OVER
アンチパターンから分かる対応関係
| 状態 | 見直すべき選択 |
|---|---|
| GROUP BY → 再 JOIN | OVER |
| DISTINCT + OVER | GROUP BY |
| 行単位比較で GROUP BY | OVER |
| 集計表なのに OVER | GROUP BY |
レビューで使える一言フレーズ
- 行を消すなら GROUP BY
- 行を残すなら OVER
- JOIN で戻すなら最初から OVER
- DISTINCT が出てきたら設計を疑う
まとめ
GROUP BY
→ 結果を作るための構文(行を減らす)
OVER(ウィンドウ関数)
→ 文脈を付与するための構文(行を残す)
どちらが高機能なのではなく、
行の責務をどう扱いたいかで選ぶのが
設計として自然です。