#06 SQLをちゃんと理解する

ウィンドウ関数

ウィンドウ関数とは

ウィンドウ関数? OVER()句を使って、グループ化せずに集計・順位付けを行う関数。ROW_NUMBER()・RANK()・LAG()・SUM() OVER() など。GROUP BYと違い、元の行を保持したまま計算できる。 は「行を集約せずに集計する」関数です。GROUP BYでは複数行がひとつの行に集約されますが、ウィンドウ関数では元の行を保ちながら集計結果を各行に付加できます。

GROUP BY との違い

-- GROUP BYの場合: 行が集約される
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id;
-- 結果は部門数分の行になる

-- ウィンドウ関数の場合: 行は集約されない
SELECT
    name,
    department_id,
    salary,
    AVG(salary) OVER (PARTITION BY department_id) AS dept_avg_salary
FROM employees;
-- 元の全員分の行に部門平均が付加される

OVER句の構造

ウィンドウ関数の OVER 句の動作 OVER句の構文 関数() OVER ( PARTITION BY 列 ORDER BY 列 ROWS BETWEEN ... AND ... ) PARTITION BY ウィンドウを分割する単位 GROUP BY に似ているが 行は集約されない ORDER BY(OVER内) ウィンドウ内の並び順 累積集計では必須 (フレームが変わる) ROWS/RANGE BETWEEN フレームの範囲 UNBOUNDED PRECEDING ~ CURRENT ROW など 例: 部門内給与ランキング(行が集約されない) name dept_id salary dept平均(OVER) 部門内RANK(OVER) 田中 1 600,000 540,000 1 鈴木 1 480,000 540,000 2 佐藤 2 500,000 475,000 1 山田 2 450,000 475,000 2 → 全4行が保たれたまま、部門ごとの平均とランクが各行に付加される(GROUP BYとの最大の違い)
図1: ウィンドウ関数のOVER句の構造と各部の役割

OVER句は次の3つのオプションで構成されます。

関数名() OVER (
    PARTITION BY 列名        -- ウィンドウをどの単位で分割するか
    ORDER BY 列名            -- ウィンドウ内の並び順
    ROWS BETWEEN ... AND ... -- フレームの範囲
)

PARTITION BY

GROUP BY に似ていますが、行を集約せずグループを分割します。省略すると全行が1つのウィンドウになります。

-- 各部門内での給与ランキング
SELECT
    name,
    department_id,
    salary,
    RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS dept_rank
FROM employees;

ORDER BY(OVER内)

ウィンドウ内の行の並び順を決めます。累積集計(SUM、COUNT)では ORDER BY によってフレームが変わります。

ROWS BETWEEN / RANGE BETWEEN

フレーム(計算対象の範囲)を指定します。

-- よく使うフレーム指定
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW  -- 先頭から現在行まで(累積)
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW          -- 直前2行から現在行まで(移動平均)
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING  -- 現在行から末尾まで

ROW_NUMBER・RANK・DENSE_RANK

ROW_NUMBER / RANK / LAG / LEAD 比較 name score ROW_NUMBER RANK DENSE_RANK LAG(score,1) LEAD(score,1) A 95 1 1 1 NULL 90 B 90 2 2 2 95 90 C 90 3 2 ← 同じ 2 ← 同じ 90 85 D 85 4 4 ← スキップ 3 ← 連続 90 NULL ROW_NUMBER() 同点でも必ず連番 重複なし・ユニークな連番 → 上位N件取得によく使う RANK() 同点は同順位、次をスキップ B,Cが2位 → 次は4位 → スポーツのランキングなど DENSE_RANK() 同点は同順位、スキップなし B,Cが2位 → 次は3位 → 売上ランクなど LAG() / LEAD() LAG(col, N, default) → N行前の値を参照 LEAD(col, N, default) → N行後の値を参照 行が存在しない場合 → デフォルト値(省略時NULL) 主な活用例 • 前月比の計算(LAGで前月売上を参照) • 株価の前日差(LAG(close, 1)) • 次のイベントまでの日数(LEAD)
図2: ROW_NUMBER・RANK・DENSE_RANKと同点の扱い方
SELECT
    name,
    score,
    ROW_NUMBER() OVER (ORDER BY score DESC) AS row_num,
    RANK()       OVER (ORDER BY score DESC) AS rank_num,
    DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank
FROM scores;
namescoreROW_NUMBERRANKDENSE_RANK
A95111
B90222
C90322
D85443
  • ROW_NUMBER: 同点でも連番を振る(重複なし)
  • RANK: 同点は同じ順位、次の順位はスキップ(4位へ飛ぶ)
  • DENSE_RANK: 同点は同じ順位、次の順位はスキップしない(3位が続く)

LAG・LEAD(前後の行を参照)

LAG() は前の行の値、LEAD() は後ろの行の値を参照します。前月比や前日比の計算に便利です。

SELECT
    sale_month,
    total_sales,
    LAG(total_sales, 1) OVER (ORDER BY sale_month) AS prev_month_sales,
    total_sales - LAG(total_sales, 1) OVER (ORDER BY sale_month) AS diff,
    ROUND(
        100.0 * total_sales / LAG(total_sales, 1) OVER (ORDER BY sale_month) - 100,
        1
    ) AS growth_rate_pct
FROM monthly_sales;
| sale_month | total_sales | prev_month | diff  | growth_rate |
|------------|-------------|------------|-------|-------------|
| 2025-01    | 100000      | NULL       | NULL  | NULL        |
| 2025-02    | 120000      | 100000     | 20000 | 20.0        |
| 2025-03    | 110000      | 120000     | -10000| -8.3        |

LAG(列, N, デフォルト値) の形でN行前を参照でき、前の行が存在しない場合のデフォルト値も指定できます。


SUM() OVER()(累積合計)

SELECT
    sale_date,
    daily_sales,
    SUM(daily_sales) OVER (
        ORDER BY sale_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS cumulative_sales
FROM daily_sales;
| sale_date  | daily_sales | cumulative_sales |
|------------|-------------|-----------------|
| 2025-01-01 | 10000       | 10000           |
| 2025-01-02 | 15000       | 25000           |
| 2025-01-03 | 8000        | 33000           |

ORDER BY のみ指定した場合も(フレームのデフォルトが RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW なので)同様の結果になりますが、明示的に ROWS BETWEEN を指定する方が意図が明確です。


移動平均

-- 3日移動平均
SELECT
    sale_date,
    daily_sales,
    AVG(daily_sales) OVER (
        ORDER BY sale_date
        ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
    ) AS moving_avg_3d
FROM daily_sales;

実用例

各部門での上位3名を抽出

WITH ranked AS (
    SELECT
        name,
        department_id,
        salary,
        ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rn
    FROM employees
)
SELECT name, department_id, salary
FROM ranked
WHERE rn <= 3;

ウィンドウ関数はSELECTで評価されるため、WHERE句では直接フィルタできません。CTEやサブクエリで一度結果を作ってからフィルタする必要があります。

連続する日付のデータ欠損を検出

-- LAGで前の日付との差を計算し、1日より大きければデータ欠損
SELECT
    sale_date,
    LAG(sale_date) OVER (ORDER BY sale_date) AS prev_date,
    DATEDIFF(sale_date, LAG(sale_date) OVER (ORDER BY sale_date)) AS days_gap
FROM daily_sales
HAVING days_gap > 1;  -- MySQL では HAVING で集計後フィルタ可

まとめ

  • ウィンドウ関数は行を集約せずに集計結果を各行に付加できる
  • OVER(PARTITION BY ... ORDER BY ...) でウィンドウを定義する
  • ROW_NUMBER: 連番、RANK: スキップあり順位、DENSE_RANK: スキップなし順位
  • LAG/LEAD: 前後の行を参照して差分・比率を計算できる
  • SUM OVER + ROWS BETWEEN で累積合計や移動平均を計算できる
  • ウィンドウ関数の結果はWHEREで直接フィルタできない(CTEを使う)

次回は NULL? SQLにおける「値が存在しない」を表す特殊な状態。NULLは値ではないため、`= NULL` では比較できず、`IS NULL` / `IS NOT NULL` を使う。NULLを含む演算は基本的にNULLになる。 の正しい扱い方と3値論理について解説します。