SQLで「前月との差を計算したい」「部門ごとの累計を出したい」「3ヶ月移動平均を付けたい」といった要件に直面したとき、分析関数(ウィンドウ関数)を使うと1つのSELECT文で完結できます。集計関数(GROUP BY)が行を集約・削減するのに対し、分析関数は元の行数を保ったまま各行に集計値を付与できるのが最大の特徴です。本記事ではサンプルデータを使いながら、OVER句の仕組みからLAG/LEAD・FIRST_VALUE/LAST_VALUE・NTILE・累計・移動平均まで体系的に解説します。
- 分析関数と集計関数(GROUP BY)の違い
- OVER句・PARTITION BY・ORDER BY・ウィンドウ句の意味と組み合わせ方
- ROW_NUMBER / RANK / DENSE_RANK の使い分け
- LAG / LEAD で前後の行の値を参照する方法
- FIRST_VALUE / LAST_VALUE / NTH_VALUE の使い方と注意点
- NTILE で N 分位に分類する方法
- CUME_DIST / PERCENT_RANK で累積分布を求める方法
- SUM / AVG の分析版で累積合計・移動平均を計算する方法
- 前月比・トップN抽出・重複排除への実務適用パターン
- 分析関数と集計関数の違い
- サンプルデータの準備
- 基本構文:OVER句・PARTITION BY・ORDER BY
- ウィンドウ句(ROWS BETWEEN / RANGE BETWEEN)の仕組み
- ROW_NUMBER / RANK / DENSE_RANK ── 行番号と順位付け
- LAG / LEAD ── 前後の行の値を参照する
- FIRST_VALUE / LAST_VALUE ── ウィンドウ内の最初・最後の値
- NTH_VALUE ── ウィンドウ内のN番目の値
- NTILE ── N分位(四分位など)への分類
- CUME_DIST / PERCENT_RANK ── 累積分布・パーセントランク
- SUM / AVG の分析版 ── 累積合計・移動平均
- 実務パターン:前月比と増減率の計算
- 実務パターン:各部門のトップ N を抽出する
- 実務パターン:重複行を削除して最新レコードだけ残す
- 実務パターン:前年同月比の計算
- 主要な分析関数まとめ
- パフォーマンスの注意点
- まとめ
分析関数と集計関数の違い
両者の最も大きな違いは出力行数です。
| 比較項目 | 集計関数(GROUP BY) | 分析関数(OVER句) |
|---|---|---|
| 出力行数 | グループ数に減る | 元の行数を保つ |
| 集計結果 | グループごとに1行 | 各行に付与される |
| 他列の参照 | GROUP BY列か集計式のみ | 全ての列が使える |
| 前後の行参照 | 不可 | LAG/LEADで可能 |
| 主な用途 | 集計レポートの作成 | 行を保ちながら集計・順位付け |
次のSQLで違いを確認できます。
-- 集計関数(GROUP BY): 部門ごとに1行に集約される
SELECT dept, SUM(sales_amt) AS total_sales
FROM monthly_sales
GROUP BY dept;
-- → 2行(営業1部・営業2部それぞれ1行)
-- 分析関数(OVER句): 全12行を保ったまま各行に合計を付与する
SELECT dept, yr_month, sales_amt,
SUM(sales_amt) OVER (PARTITION BY dept) AS dept_total
FROM monthly_sales;
-- → 12行(全行を保持。dept_totalには部門合計が付く)
サンプルデータの準備
以降の例で使用する月次売上テーブルを作成します。
CREATE TABLE monthly_sales (
dept VARCHAR2(20), -- 部門名
yr_month VARCHAR2(7), -- 年月 (例: 2025-01)
sales_amt NUMBER -- 売上金額
);
INSERT INTO monthly_sales VALUES ('営業1部', '2025-01', 1200000);
INSERT INTO monthly_sales VALUES ('営業1部', '2025-02', 980000);
INSERT INTO monthly_sales VALUES ('営業1部', '2025-03', 1450000);
INSERT INTO monthly_sales VALUES ('営業1部', '2025-04', 1600000);
INSERT INTO monthly_sales VALUES ('営業1部', '2025-05', 1380000);
INSERT INTO monthly_sales VALUES ('営業1部', '2025-06', 1700000);
INSERT INTO monthly_sales VALUES ('営業2部', '2025-01', 900000);
INSERT INTO monthly_sales VALUES ('営業2部', '2025-02', 1100000);
INSERT INTO monthly_sales VALUES ('営業2部', '2025-03', 850000);
INSERT INTO monthly_sales VALUES ('営業2部', '2025-04', 1200000);
INSERT INTO monthly_sales VALUES ('営業2部', '2025-05', 1350000);
INSERT INTO monthly_sales VALUES ('営業2部', '2025-06', 1500000);
COMMIT;
基本構文:OVER句・PARTITION BY・ORDER BY
全ての分析関数は OVER() 句を持ちます。OVER句の中で3つの要素を指定できます。
分析関数名(引数) OVER (
[PARTITION BY パーティション列, ...] -- グループ分けの単位(省略で全行が1グループ)
[ORDER BY ソート列 [ASC|DESC], ...] -- ウィンドウ内のソート順(省略可)
[ROWS | RANGE BETWEEN 開始 AND 終了] -- ウィンドウ(行範囲)の指定(省略可)
)
| 要素 | 役割 | 省略時の挙動 |
|---|---|---|
| PARTITION BY | 分析関数を適用するグループを定義する(GROUP BY に似ているが行は集約されない) | 全行を1グループとして扱う |
| ORDER BY | ウィンドウ内の行の並び順を定義する(LAG/LEADや順位関数に必須) | 順序なし(LAG/LEADは使用不可) |
| ウィンドウ句 | 各行に対して参照する行の範囲(フレーム)を定義する | RANGE UNBOUNDED PRECEDING AND CURRENT ROW |
ウィンドウ句(ROWS BETWEEN / RANGE BETWEEN)の仕組み
ウィンドウ句は分析関数が参照する「行の範囲」を定義します。ROWS(物理的な行数)と RANGE(論理的な値の範囲)の2種類があります。
| ウィンドウ句の記述 | 意味 |
|---|---|
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW |
先頭行から現在行まで(累計に使う定番) |
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW |
2行前から現在行まで(3行移動平均に使う) |
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING |
前後1行を含む3行ウィンドウ |
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING |
パーティション全行(FIRST_VALUE/LAST_VALUEで全体を見るとき) |
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW |
同値の行も含む現在行まで(ORDER BY指定時のデフォルト) |
LAST_VALUE を ORDER BY と組み合わせるとデフォルトのウィンドウがRANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW になるため、結果が「現在行の値」になってしまいます。パーティション全体の最後の値を取得するには、必ず ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING を明示してください。
ROW_NUMBER / RANK / DENSE_RANK ── 行番号と順位付け
行に順位・番号を付ける3関数です。同じ値が存在したときの挙動が異なります。
SELECT
dept,
yr_month,
sales_amt,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY sales_amt DESC) AS row_num,
RANK() OVER (PARTITION BY dept ORDER BY sales_amt DESC) AS rank_num,
DENSE_RANK() OVER (PARTITION BY dept ORDER BY sales_amt DESC) AS dense_rank
FROM monthly_sales
ORDER BY dept, sales_amt DESC;
| 関数 | 同値の扱い | 次の順位 | 用途 |
|---|---|---|---|
| ROW_NUMBER | 同値でも連番を振る(重複なし) | 連続する番号 | 重複排除・ページング |
| RANK | 同値に同じ順位を振る | 同値の数だけ飛ぶ(1,1,3,4…) | 競走の順位(同順あり) |
| DENSE_RANK | 同値に同じ順位を振る | 飛ばさず連続(1,1,2,3…) | 順位のギャップを作りたくない場合 |
RANK/DENSE_RANKのより詳しい使い方は「【Oracle】RANK関数でデータの順位を付ける方法」を参照してください。
LAG / LEAD ── 前後の行の値を参照する
LAGは指定した行数前の値、LEADは指定した行数後の値を返します。前月比や変化量の計算に欠かせない関数です。
LAG (列名 [, 遡る行数 [, デフォルト値]]) OVER (PARTITION BY ... ORDER BY ...) LEAD(列名 [, 進む行数 [, デフォルト値]]) OVER (PARTITION BY ... ORDER BY ...) -- 行数のデフォルト: 1(直前/直後) -- デフォルト値のデフォルト: NULL(範囲外のときに返す値)
SELECT
dept,
yr_month,
sales_amt,
LAG(sales_amt, 1, 0) OVER (PARTITION BY dept ORDER BY yr_month) AS prev_month,
LEAD(sales_amt, 1, 0) OVER (PARTITION BY dept ORDER BY yr_month) AS next_month,
sales_amt
- LAG(sales_amt, 1) OVER (PARTITION BY dept ORDER BY yr_month) AS mom_diff
FROM monthly_sales
ORDER BY dept, yr_month;
| DEPT | YR_MONTH | SALES_AMT | PREV_MONTH | MOM_DIFF |
|---|---|---|---|---|
| 営業1部 | 2025-01 | 1,200,000 | 0 | (null) |
| 営業1部 | 2025-02 | 980,000 | 1,200,000 | -220,000 |
| 営業1部 | 2025-03 | 1,450,000 | 980,000 | 470,000 |
| … | … | … | … | … |
PARTITION BY を省略すると、全行が1グループになります。「営業1部の1月の前月」として「営業2部の12月(前年度末など)」が参照されてしまうため、部門をまたいで計算したくない場合は
PARTITION BY dept を必ず付けてください。
FIRST_VALUE / LAST_VALUE ── ウィンドウ内の最初・最後の値
FIRST_VALUE はウィンドウ内の先頭行の値、LAST_VALUE は末尾行の値を返します。「この部門の初月の売上」「最新月の売上」を各行に付けるときに使います。
SELECT
dept,
yr_month,
sales_amt,
FIRST_VALUE(sales_amt) OVER (
PARTITION BY dept ORDER BY yr_month
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS first_month_sales,
LAST_VALUE(sales_amt) OVER (
PARTITION BY dept ORDER BY yr_month
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING -- 全行を見るため明示必須
) AS last_month_sales
FROM monthly_sales
ORDER BY dept, yr_month;
結果のイメージです(営業1部の場合)。
| DEPT | YR_MONTH | SALES_AMT | FIRST_MONTH_SALES | LAST_MONTH_SALES |
|---|---|---|---|---|
| 営業1部 | 2025-01 | 1,200,000 | 1,200,000 | 1,700,000 |
| 営業1部 | 2025-03 | 1,450,000 | 1,200,000 | 1,700,000 |
| 営業1部 | 2025-06 | 1,700,000 | 1,200,000 | 1,700,000 |
NTH_VALUE ── ウィンドウ内のN番目の値
NTH_VALUE(列名, N) はウィンドウ内でN番目の値を返します。「部門の中で売上2位の金額」を各行に付けるような場面で使えます。
SELECT
dept,
yr_month,
sales_amt,
NTH_VALUE(sales_amt, 2) OVER (
PARTITION BY dept ORDER BY sales_amt DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS second_highest
FROM monthly_sales
ORDER BY dept, yr_month;
-- 営業1部の second_highest は全行で 1,600,000(2位の売上)が付く
NTILE ── N分位(四分位など)への分類
NTILE(N) はパーティション内の行をN等分し、各行に1〜Nの分位番号を付けます。売上を「下位・中位・上位」に3分類したり、四分位(N=4)で外れ値を検出したりするのに使います。
SELECT
dept,
yr_month,
sales_amt,
NTILE(3) OVER (PARTITION BY dept ORDER BY sales_amt) AS tile
FROM monthly_sales
ORDER BY dept, tile, sales_amt;
-- tile=1: 下位1/3(売上が低い月)
-- tile=2: 中位1/3
-- tile=3: 上位1/3(売上が高い月)
行数がNで割り切れないとき、余りの行は先頭のグループから順に1行ずつ追加されます。例えば7行をNTILE(3)で分けると「3, 2, 2」行になります。
CUME_DIST / PERCENT_RANK ── 累積分布・パーセントランク
2つの関数はともに0〜1の値を返し、その値がパーティション内でどの位置にあるかを表します。
| 関数 | 計算式 | 値の範囲 | 同値の扱い |
|---|---|---|---|
| CUME_DIST | (現在以下の行数)/ 全行数 | 0超〜1(最大値は必ず1) | 同値グループの最大値を返す |
| PERCENT_RANK | (順位 – 1)/(全行数 – 1) | 0〜1(最小値は必ず0) | 同値グループの最小値を返す |
SELECT
dept,
yr_month,
sales_amt,
ROUND(CUME_DIST() OVER (PARTITION BY dept ORDER BY sales_amt), 2) AS cum_dist,
ROUND(PERCENT_RANK() OVER (PARTITION BY dept ORDER BY sales_amt), 2) AS pct_rank
FROM monthly_sales
ORDER BY dept, sales_amt;
例えば CUME_DIST = 0.50 なら「この月の売上は部門内の下位50%以内」を意味します。CUME_DIST >= 0.80 を条件にすれば上位20%の月を抽出できます。
SUM / AVG の分析版 ── 累積合計・移動平均
集計関数(SUM・AVG・COUNT・MIN・MAX)もOVER句と組み合わせて分析関数として使えます。ウィンドウ句と組み合わせることで累積合計や移動平均を簡単に計算できます。
累積合計(ランニングトータル)
SELECT
dept,
yr_month,
sales_amt,
SUM(sales_amt) OVER (
PARTITION BY dept
ORDER BY yr_month
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW -- 先頭から現在行まで
) AS cumulative_sales
FROM monthly_sales
ORDER BY dept, yr_month;
| DEPT | YR_MONTH | SALES_AMT | CUMULATIVE_SALES |
|---|---|---|---|
| 営業1部 | 2025-01 | 1,200,000 | 1,200,000 |
| 営業1部 | 2025-02 | 980,000 | 2,180,000 |
| 営業1部 | 2025-03 | 1,450,000 | 3,630,000 |
| 営業1部 | 2025-04 | 1,600,000 | 5,230,000 |
| 営業1部 | 2025-05 | 1,380,000 | 6,610,000 |
| 営業1部 | 2025-06 | 1,700,000 | 8,310,000 |
移動平均(ローリング平均)
SELECT
dept,
yr_month,
sales_amt,
ROUND(
AVG(sales_amt) OVER (
PARTITION BY dept
ORDER BY yr_month
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW -- 2ヶ月前〜現在の3行
)
) AS moving_avg_3m
FROM monthly_sales
ORDER BY dept, yr_month;
| DEPT | YR_MONTH | SALES_AMT | MOVING_AVG_3M | 備考 |
|---|---|---|---|---|
| 営業1部 | 2025-01 | 1,200,000 | 1,200,000 | 1ヶ月のみの平均 |
| 営業1部 | 2025-02 | 980,000 | 1,090,000 | 2ヶ月の平均 |
| 営業1部 | 2025-03 | 1,450,000 | 1,210,000 | 3ヶ月の移動平均(以降も3行) |
| 営業1部 | 2025-04 | 1,600,000 | 1,343,333 | 2月〜4月の平均 |
実務パターン:前月比と増減率の計算
LAG を使って前月比(%)を計算する実用パターンです。ダッシュボードやレポートで頻繁に使います。
SELECT
dept,
yr_month,
sales_amt,
LAG(sales_amt) OVER (PARTITION BY dept ORDER BY yr_month) AS prev_sales,
ROUND(
(sales_amt - LAG(sales_amt) OVER (PARTITION BY dept ORDER BY yr_month))
/ LAG(sales_amt) OVER (PARTITION BY dept ORDER BY yr_month) * 100,
1
) AS mom_change_pct -- 前月比増減率(%)
FROM monthly_sales
ORDER BY dept, yr_month;
実務パターン:各部門のトップ N を抽出する
ROW_NUMBER をサブクエリで使い、各グループ(部門)のトップNだけを取り出す定番パターンです。
SELECT dept, yr_month, sales_amt, rn AS rank_in_dept
FROM (
SELECT
dept, yr_month, sales_amt,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY sales_amt DESC) AS rn
FROM monthly_sales
)
WHERE rn <= 3
ORDER BY dept, rn;
同率がある場合、
ROW_NUMBER は厳密に上位N行だけを返しますが、RANK は同率の行を全て含むためN行を超えることがあります。「必ずN行」にしたい場合は ROW_NUMBER、「同率は全員含める」場合は RANK を選んでください。
実務パターン:重複行を削除して最新レコードだけ残す
ROW_NUMBER を使うと、同一キーの中で特定の1行だけを残す「重複排除」が簡潔に書けます。
-- 重複がある仮定のテーブルから、同一(dept, yr_month)で最新のROWIDを持つ行のみ残す
DELETE FROM monthly_sales
WHERE ROWID NOT IN (
SELECT MAX(ROWID)
FROM monthly_sales
GROUP BY dept, yr_month
);
-- ROW_NUMBER を使った重複確認(削除前に内容を確認したい場合)
SELECT dept, yr_month, sales_amt, ROWID
FROM (
SELECT dept, yr_month, sales_amt, ROWID,
ROW_NUMBER() OVER (
PARTITION BY dept, yr_month
ORDER BY ROWID DESC
) AS rn
FROM monthly_sales
)
WHERE rn > 1; -- rn > 1 が重複行(最初の1件以外)
実務パターン:前年同月比の計算
LAG の第2引数に12を指定すると、月次データで12行前(前年同月)の値を参照できます。
SELECT
dept,
yr_month,
sales_amt,
LAG(sales_amt, 12) OVER (PARTITION BY dept ORDER BY yr_month) AS same_month_prev_yr,
ROUND(
(sales_amt - LAG(sales_amt, 12) OVER (PARTITION BY dept ORDER BY yr_month))
/ LAG(sales_amt, 12) OVER (PARTITION BY dept ORDER BY yr_month) * 100,
1
) AS yoy_change_pct -- 前年同月比増減率(%)
FROM monthly_sales
ORDER BY dept, yr_month;
-- 2026年以降のデータが追加されると、12ヶ月前の値が自動的に参照される
主要な分析関数まとめ
| 関数 | 主な用途 | ORDER BY | ウィンドウ句 |
|---|---|---|---|
| ROW_NUMBER() | 連番・ページング・重複排除 | 必須 | 不可 |
| RANK() | 順位付け(同率あり・飛びあり) | 必須 | 不可 |
| DENSE_RANK() | 順位付け(同率あり・飛びなし) | 必須 | 不可 |
| LAG(col, n) | n行前の値を参照 | 必須 | 不可 |
| LEAD(col, n) | n行後の値を参照 | 必須 | 不可 |
| FIRST_VALUE(col) | ウィンドウ先頭の値 | 任意 | 推奨 |
| LAST_VALUE(col) | ウィンドウ末尾の値(要ウィンドウ句明示) | 任意 | 必須 |
| NTH_VALUE(col, n) | ウィンドウのn番目の値 | 任意 | 推奨 |
| NTILE(n) | n分位への分類 | 必須 | 不可 |
| CUME_DIST() | 累積分布(0超〜1) | 必須 | 不可 |
| PERCENT_RANK() | パーセントランク(0〜1) | 必須 | 不可 |
| SUM(col) OVER(…) | 累積合計・グループ合計 | 任意 | 有効 |
| AVG(col) OVER(…) | 移動平均・グループ平均 | 任意 | 有効 |
| COUNT(*) OVER(…) | 累積件数・グループ件数 | 任意 | 有効 |
パフォーマンスの注意点
分析関数はデータ量が多くなるとメモリ・CPU コストが高まる場合があります。以下の点に気をつけてください。
- ORDER BY 列にインデックスを付ける:ウィンドウのソートが高速化されます
- PARTITION BY の粒度を適切に選ぶ:パーティションが大きすぎると1グループ内でのソートコストが増大します
- 同じ OVER 句を複数回書く場合はインラインビューに切り出す:同じウィンドウ定義を何度も記述すると評価が重複することがあります
- 実行計画で WINDOW SORT を確認する:
EXPLAIN PLANでWINDOW SORT操作が表示されます。コストが高い場合はPARTITION BYやORDER BY列の見直しを検討してください
まとめ
Oracleの分析関数は、OVER句の PARTITION BY・ORDER BY・ウィンドウ句を組み合わせることで、行を集約せずに多彩な集計・順位付け・前後参照を1つのSELECT文で実現できます。
- 集計関数(GROUP BY)は行を削減するが、分析関数(OVER句)は行数を保ったまま集計値を付与する
- LAG/LEAD:前後の行を参照。前月比・前年同月比の計算に必須
- FIRST_VALUE/LAST_VALUE:ウィンドウの先頭・末尾を取得。LAST_VALUEはウィンドウ句の明示が必要
- NTILE:N分位での分類に使う。行数がNで割り切れないと先頭グループに余りが振られる
- CUME_DIST/PERCENT_RANK:0〜1のスコアで相対的な位置を表す
- SUM/AVG の分析版:累積合計はウィンドウ句
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW、移動平均はROWS BETWEEN N PRECEDING AND CURRENT ROW - ROW_NUMBER:トップN抽出や重複排除のサブクエリで活躍する

