OVER() の基本 — 「行を潰さずに」全体の集計値を各行に付与する
ウィンドウ関数は、GROUP BY のように「データをグループ化して集計」しますが、「行をまとめず(潰さず)に、元の行数のまま集計結果を返す」という非常に強力な特徴があります。
SELECT key_col, num_col, SUM(num_col) OVER() AS total_all -- OVER()がウィンドウ関数の合図 FROM table_name;
OVER() をつけると、「結果セット全体をひとつのウィンドウ(窓)として扱い、その集計値を全ての行に同じ値として付与」します。以下の sales テーブル(個別の売上データ)を使い、各担当者の「売上金額」「全体の売上合計」「全体の売上に対する構成比(割合)」を出力してください。
※構成比は amount / 全体の合計 で計算して小数第4位に丸め、金額の高い順に並べること。
| emp_id | emp_name | amount |
|---|---|---|
| E1 | 田中 | 150000 |
| E2 | 佐藤 | 200000 |
| E3 | 鈴木 | 120000 |
| E4 | 高橋 | 180000 |
| emp_name | amount | total_amount | ratio |
|---|---|---|---|
| 佐藤 | 200000 | 650000 | 0.3077 |
| 高橋 | 180000 | 650000 | 0.2769 |
| 田中 | 150000 | 650000 | 0.2308 |
| 鈴木 | 120000 | 650000 | 0.1846 |
SELECT emp_name, amount, SUM(amount) OVER() AS total_amount, -- SUM(amount) OVER(): テーブル全体の amount の合計(650,000)を、各行に同じ値として付与する ROUND(amount * 1.0 / SUM(amount) OVER(), 4) AS ratio -- 構成比を小数第4位に丸める FROM sales ORDER BY amount DESC; /* 実行順序(超重要): 1. FROM sales → 4行を取得 2. (WHERE/GROUP BY があれば実行) → 行を絞り込み 3. Window関数 → 全体を計算し各行に付与 4. SELECT → 構成比を計算して出力 5. ORDER BY → 売上降順に並べ替え */
LEGEND
① FROM
FROM salessales テーブル全体(4行)を読み込みます。| emp_name | amount |
|---|---|
| 田中 | 150000 |
| 佐藤 | 200000 |
| 鈴木 | 120000 |
| 高橋 | 180000 |
OVER() を使えばクエリ一発で取得できます。WHERE amount > 10000 があれば、「1万以上の行だけの合計」が計算されます。| SUM(amount) |
|---|
| 650,000 |
個人の売上情報が消える
| emp_name | amount | SUM(amount) OVER() |
|---|---|---|
| 田中 | 150,000 | 650,000 |
| 佐藤 | 200,000 | 650,000 |
| 鈴木 | 120,000 | 650,000 |
| 高橋 | 180,000 | 650,000 |
構成比の計算もこの行でそのまま行える
WHERE SUM(amount) OVER() > 500000 はエラーになります。WHEREはウィンドウ関数より「前」に評価されるためです。ウィンドウ関数の結果で絞り込みたい場合は、Q8で解説する「サブクエリ(CTE)」を使います。PARTITION BY — 部署ごと・カテゴリごとにウィンドウ(窓)を区切る
OVER() のカッコ内に PARTITION BY 列名を書くと、指定した列の値ごとに「ウィンドウ(集計範囲)」を区切ることができます。GROUP BY に似ていますが、やはり行は潰れません。
SELECT group_col, key_col, num_col, SUM(num_col) OVER(PARTITION BY group_col) AS group_total FROM table_name;
以下の employees テーブルから、各従業員の「名前」「部署」「給与」と、「その人が所属する部署の給与合計」を出力してください。
| emp_name | dept | salary |
|---|---|---|
| 田中 | 営業 | 300000 |
| 佐藤 | 営業 | 280000 |
| 鈴木 | 開発 | 400000 |
| 高橋 | 開発 | 350000 |
| 伊藤 | 人事 | 320000 |
| emp_name | dept | salary | dept_total |
|---|---|---|---|
| 田中 | 営業 | 300000 | 580000 |
| 佐藤 | 営業 | 280000 | 580000 |
| 鈴木 | 開発 | 400000 | 750000 |
| 高橋 | 開発 | 350000 | 750000 |
| 伊藤 | 人事 | 320000 | 320000 |
SELECT emp_name, dept, salary, SUM(salary) OVER(PARTITION BY dept) AS dept_total -- 部署ごとの給与合計を各行に付与 FROM employees ORDER BY CASE dept WHEN '営業' THEN 1 WHEN '開発' THEN 2 WHEN '人事' THEN 3 END, salary DESC, emp_name; /* 実行順序: 1. FROM employees → 5行取得 2. Window関数 → deptごとにパーティションを作成し、各パーティション内でSUMを計算 3. SELECT → 各行に計算した dept_total をアタッチして出力 4. ORDER BY CASE dept WHEN '営業' THEN 1 WHEN '開発' THEN 2 WHEN '人事' THEN 3 END, salary DESC, emp_name → 営業・開発・人事の部署順、部署内は salary 降順、同額は emp_name 昇順 */
LEGEND
① FROM
FROM employeesemployees テーブル全体(5行)を読み込みます。| emp_name | dept | salary |
|---|---|---|
| 田中 | 営業 | 300000 |
| 佐藤 | 営業 | 280000 |
| 鈴木 | 開発 | 400000 |
| 高橋 | 開発 | 350000 |
| 伊藤 | 人事 | 320000 |
PARTITION BY year, month のように複数列を指定すれば、「年月ごとの合計」を各行に付与できます。| dept(パーティションキー) | emp_name | salary | dept_total(パーティション内SUM) |
|---|---|---|---|
| 営業 | 田中 | 300,000 | 580,000 |
| 営業 | 佐藤 | 280,000 | 580,000 |
| 開発 | 鈴木 | 400,000 | 750,000 |
| 開発 | 高橋 | 350,000 | 750,000 |
| 人事 | 伊藤 | 320,000 | 320,000 |
行数は5行のまま変化なし。
SELECT dept, SUM(salary) OVER(PARTITION BY dept) FROM employees GROUP BY dept; のように書くのは誤りです。GROUP BY は行を潰す処理なので、同時に OVER(PARTITION BY) を使うと意図しない結果(またはエラー)になります。単純なグループ合計なら通常の SUM() ... GROUP BY を使いましょう。OVER(ORDER BY) — 日付順に並べて「累計」を計算する
OVER句の中に ORDER BY 列名を指定すると、単なる全体集計ではなく「先頭行から現在の行まで」の範囲(累計)で集計が行われます。
SELECT sort_col, num_col, SUM(num_col) OVER(ORDER BY sort_col ASC) AS running_total FROM table_name;
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(先頭から現在行と同じ並び順値を持つ行まで)という枠組みが適用されるため、累計計算になります。同じ日付の行も現在行と同じフレームに含まれます。以下の daily_sales テーブルから、日付ごとの売上 amount と、「その日までの売上累計(running_total)」を取得してください。
| sale_date | amount |
|---|---|
| 04-01 | 10000 |
| 04-02 | 15000 |
| 04-03 | 12000 |
| 04-04 | 20000 |
| sale_date | amount | running_total |
|---|---|---|
| 04-01 | 10000 | 10000 |
| 04-02 | 15000 | 25000 |
| 04-03 | 12000 | 37000 |
| 04-04 | 20000 | 57000 |
SELECT sale_date, amount, SUM(amount) OVER(ORDER BY sale_date ASC) AS running_total -- 日付順に先頭〜現在行を累計 FROM daily_sales ORDER BY sale_date; /* 実行順序: 1. FROM daily_sales 2. Window関数が sale_date 順に行を評価 3. SELECT 出力 */
LEGEND
① FROM
FROM daily_salesテーブルを読み込みます。| sale_date | amount |
|---|---|
| 04-01 | 10000 |
| 04-02 | 15000 |
| 04-03 | 12000 |
| 04-04 | 20000 |
SUM(amount) OVER(PARTITION BY dept ORDER BY date) と書けば、「部署ごとの売上累計」を算出できます。この複合技は実務ダッシュボード用クエリで必須です。ORDER BY date, id のように一意キーを追加します。| sale_date | amount | 集計フレーム(先頭 〜 現在行) | running_total |
|---|---|---|---|
| 04-01 | 10,000 | 10,000 | 10,000 |
| 04-02 | 15,000 | 10,000 + 15,000 | 25,000 |
| 04-03 | 12,000 | 10,000 + 15,000 + 12,000 | 37,000 |
| 04-04 | 20,000 | 10,000 + 15,000 + 12,000 + 20,000 | 57,000 |
行が下に進むほどフレームが1行ずつ拡張され、全て足し合わせることで累計が得られる。
ORDER BY month として複数の日次データがあると、同じ月のデータはすべてまとめて足されてしまい、意図した「行ごとの累計」になりません。常に一意に決まるようにソートキーを指定してください。ROW_NUMBER() — グループ内で行番号(連番)を振る
ROW_NUMBER() は、指定した順序に従って行に「1, 2, 3...」と一意の連番を振るウィンドウ関数です。引数は取りません。
SELECT group_col, ts_col, ROW_NUMBER() OVER( PARTITION BY group_col -- グループごとにリセット(1から振り直し) ORDER BY ts_col DESC -- 新しい順に振る ) AS rn FROM table_name;
rn=1 の行だけを絞り込むのがSQLの定番・最強パターンです。まずは連番の振り方を完璧にしましょう。以下の logins テーブルには、ユーザーのログイン履歴が入っています。
ユーザー(user_id)ごとに、ログイン日時(login_time)が新しい順(降順)に「1, 2, 3...」と行番号(rn)を振ってください。
| user_id | login_time |
|---|---|
| U1 | 2024-04-01 10:00 |
| U1 | 2024-04-02 12:00 |
| U1 | 2024-04-03 09:00 |
| U2 | 2024-04-01 11:00 |
| U2 | 2024-04-04 15:00 |
| user_id | login_time | rn |
|---|---|---|
| U1 | 2024-04-03 09:00 | 1 |
| U1 | 2024-04-02 12:00 | 2 |
| U1 | 2024-04-01 10:00 | 3 |
| U2 | 2024-04-04 15:00 | 1 |
| U2 | 2024-04-01 11:00 | 2 |
SELECT user_id, login_time, ROW_NUMBER() OVER( PARTITION BY user_id -- user_idごとに独立して番号を振る ORDER BY login_time DESC -- 時間の降順(最新が1になる) ) AS rn FROM logins ORDER BY user_id, rn; /* 実行順序: 1. FROM logins 2. user_id 'U1' のグループを作る 3. user_id 'U2' のグループを作る 4. SELECT 出力 */
LEGEND
① FROM
FROM loginsテーブルを読み込みます。| user_id | login_time |
|---|---|
| U1 | 04-01 10:00 |
| U1 | 04-02 12:00 |
| U1 | 04-03 09:00 |
| U2 | 04-01 11:00 |
| U2 | 04-04 15:00 |
MAX(login_time) GROUP BY user_id でも可能ですが、その場合「ログインしたデバイス(IPアドレス等)などの他の列を同時に取得できない」という致命的な弱点があります。ROW_NUMBER で行番号を振り、後で WHERE rn = 1 で行ごと抽出することでこの問題を完全に解決できます。SELECT user_id, MAX(login_time) FROM logins GROUP BY user_id;
| user_id | MAX(login_time) |
|---|---|
| U1 | 04-03 09:00 |
| U2 | 04-04 15:00 |
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER( PARTITION BY user_id ORDER BY login_time DESC ) AS rn FROM logins ) t WHERE rn = 1;
| user_id | login_time | rn |
|---|---|---|
| U1 | 04-03 09:00 | 1 |
| U2 | 04-04 15:00 | 1 |
ROW_NUMBER() OVER() とだけ書くと、テーブル全体に対して適当な順序で連番が振られてしまいます。「グループごとに」「〜の順で」という意図がある場合は、必ず PARTITION BY と ORDER BY をセットで指定してください。RANK / DENSE_RANK — 順位付けと同順位(タイ)の扱い
順位を付ける関数には3種類あり、同順位(同じ点数など)が発生したときの次の番号の飛び方が異なります。
ROW_NUMBER(): 無条件で 1, 2, 3, 4 と連番を振る(同点でも差がつく)RANK(): 同点は同じ順位になり、次の順位が飛ぶ(1, 1, 3, 4)DENSE_RANK(): 同点は同じ順位になり、次は飛ばずに詰める(1, 1, 2, 3)
SELECT id_col, ROW_NUMBER() OVER (ORDER BY num_col DESC) AS rn, -- 連番 RANK() OVER (ORDER BY num_col DESC) AS rnk, -- 同順位あり・次は飛ぶ DENSE_RANK() OVER (ORDER BY num_col DESC) AS dense_rnk -- 同順位あり・次は詰める FROM table_name;
ORDER BY を与えても、同点の行に番号を振るところだけが異なります。ROW_NUMBER() は同点でも必ず一意の連番を振るため、どちらの行が先になるかは並び順が同値のままだと決まりません。確定させたい場合は ORDER BY の末尾に一意な列を足します。以下の scores テーブル(生徒のテスト点数)を使い、点数が高い順に RANK(), DENSE_RANK(), ROW_NUMBER() の3つの順位を算出し、違いを確認してください。
| student | score |
|---|---|
| Aさん | 95 |
| Bさん | 95 |
| Cさん | 88 |
| Dさん | 88 |
| Eさん | 70 |
| student | score | rnk | dense_rnk | row_num |
|---|---|---|---|---|
| Aさん | 95 | 1 | 1 | 1 |
| Bさん | 95 | 1 | 1 | 2 |
| Cさん | 88 | 3 | 2 | 3 |
| Dさん | 88 | 3 | 2 | 4 |
| Eさん | 70 | 5 | 3 | 5 |
SELECT student, score, RANK() OVER(ORDER BY score DESC) AS rnk, -- 同順位の次を飛ばす(標準的な順位) DENSE_RANK() OVER(ORDER BY score DESC) AS dense_rnk, -- 同順位の次を詰める(飛ばさない) ROW_NUMBER() OVER(ORDER BY score DESC, student) AS row_num -- 同点は student 順で一意にする FROM scores -- 同順位でも強引に一意の連番を振る ORDER BY score DESC, student; /* 実行順序: 1. FROM scores 2. Window関数が score 降順で評価 3. SELECT 出力 */
LEGEND
① FROM
FROM scoresテーブルを読み込みます。| student | score |
|---|---|
| Aさん | 95 |
| Bさん | 95 |
| Cさん | 88 |
| Dさん | 88 |
| Eさん | 70 |
| student | score | RANK() 同順位の次を飛ばす |
DENSE_RANK() 同順位でも詰める |
ROW_NUMBER() 必ず一意の連番 |
|---|---|---|---|---|
| Aさん | 95 | 1 | 1 | 1 |
| Bさん | 95 | 1 ← 同点 | 1 ← 同点 | 2 ← 強制区別 |
| Cさん | 88 | 3 ← 2が欠番! | 2 ← 詰める | 3 |
| Dさん | 88 | 3 ← 同点 | 2 ← 同点 | 4 ← 強制区別 |
| Eさん | 70 | 5 ← 4が欠番! | 3 | 5 |