Window 関数

集計しながら各行も保つ魔法のような機能。ランキング、累積、前後比較、移動平均、グループ内 N 件など。 SQL の表現力が一気に広がる。

普通の集計との違い

GROUP BY:                 Window:
+-----+-------+           +-----+-------+--------+
| user| total |           | id  | views | rank   |
+-----+-------+           +-----+-------+--------+
| 1   | 500   |           | 1   | 100   | 3      |
| 2   | 800   |           | 2   | 300   | 2      |
+-----+-------+           | 3   | 500   | 1      |
                          +-----+-------+--------+

GROUP BY は行数が減る。Window は行数を保ったまま列を追加。
      

基本構文

window_function() OVER (
  PARTITION BY ...   -- グループ分け(GROUP BY 相当)
  ORDER BY ...       -- 順序
  ROWS / RANGE ...   -- 範囲指定
)

主な関数

ランキング系

位置系

集計系(OVER 付き)

ROW_NUMBER の典型

-- 各ユーザの最新投稿だけ(DISTINCT ON の代替)
SELECT * FROM (
  SELECT *,
    ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
  FROM posts
) t
WHERE rn = 1;

各カテゴリの上位 3 件

SELECT * FROM (
  SELECT *,
    ROW_NUMBER() OVER (PARTITION BY category ORDER BY views DESC) AS rn
  FROM posts
) t
WHERE rn <= 3;

RANK / DENSE_RANK

SELECT
  name, score,
  RANK()       OVER (ORDER BY score DESC) AS rank,
  DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank
FROM scoreboard;
score   RANK   DENSE
100     1      1
90      2      2
90      2      2
80      4      3       ← RANK は 2,2 の次が 4、DENSE は 3
      

LAG / LEAD(前後比較)

-- 前日からの増減
SELECT
  date,
  views,
  LAG(views) OVER (ORDER BY date) AS prev,
  views - LAG(views) OVER (ORDER BY date) AS diff
FROM daily_stats;

第 2 引数で N 行前 + デフォルト値

LAG(views, 1, 0) OVER (...)    -- 1 行前、無ければ 0
LEAD(views, 7, 0) OVER (...)   -- 7 行後

累積

SELECT
  date,
  amount,
  SUM(amount) OVER (ORDER BY date) AS cumulative
FROM revenue;

カテゴリ別累積

SUM(amount) OVER (PARTITION BY category ORDER BY date)

移動平均(ROWS)

-- 過去 7 日(自分含む)の平均
SELECT
  date, sales,
  AVG(sales) OVER (
    ORDER BY date
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
  ) AS ma7
FROM daily;

ROWS の指定例

ROWS vs RANGE

RANGE の例

-- 過去 7 日(同日複数行も含む)
SUM(sales) OVER (
  ORDER BY date
  RANGE BETWEEN INTERVAL '6 days' PRECEDING AND CURRENT ROW
)

FIRST_VALUE / LAST_VALUE

SELECT
  user_id, post_id, created_at,
  FIRST_VALUE(post_id) OVER (PARTITION BY user_id ORDER BY created_at) AS first_post,
  LAST_VALUE(post_id) OVER (
    PARTITION BY user_id
    ORDER BY created_at
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  ) AS last_post
FROM posts;

LAST_VALUE はデフォルトの範囲が「現在まで」なので、UNBOUNDED FOLLOWING を明示しないと最後にならない。

NTILE(N 分位)

SELECT
  user_id, total_spent,
  NTILE(4) OVER (ORDER BY total_spent DESC) AS quartile
FROM customer_stats;
-- 上位 25% / 50% / 75% / 100% に分割

WINDOW 句で名前付け

同じ Window 定義を何度も書かずに済む:

SELECT
  user_id, created_at,
  ROW_NUMBER() OVER w AS rn,
  RANK()       OVER w AS rank,
  LAG(views)   OVER w AS prev_views
FROM posts
WINDOW w AS (PARTITION BY user_id ORDER BY created_at);

典型パターン集

1. グループ内の最新 1 件

WITH ranked AS (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
  FROM events
)
SELECT * FROM ranked WHERE rn = 1;

2. 重複の検出と削除

-- email 重複の重複側だけを削除
DELETE FROM users WHERE id IN (
  SELECT id FROM (
    SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn
    FROM users
  ) t WHERE rn > 1
);

3. 連続日数(ストリーク)

-- 連続ログイン日数を求める常套手段
SELECT user_id, COUNT(*) AS streak
FROM (
  SELECT
    user_id,
    login_date,
    login_date - (ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date))::INT AS grp
  FROM logins
) t
GROUP BY user_id, grp
ORDER BY streak DESC;

4. 前回からの経過時間

SELECT
  user_id, action_at,
  action_at - LAG(action_at) OVER (PARTITION BY user_id ORDER BY action_at) AS gap
FROM actions;

5. 累計に対する比率

SELECT
  category, name, sales,
  sales * 1.0 / SUM(sales) OVER (PARTITION BY category) AS pct_in_cat
FROM products;

6. 上位 3 件 + 「その他」

WITH ranked AS (
  SELECT category, sum(amount) AS total,
    ROW_NUMBER() OVER (ORDER BY sum(amount) DESC) AS rn
  FROM orders
  GROUP BY category
)
SELECT
  CASE WHEN rn <= 3 THEN category ELSE 'Other' END AS category,
  sum(total) AS total
FROM ranked
GROUP BY 1;

7. 中央値

SELECT
  category,
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY price) AS median
FROM products
GROUP BY category;

パフォーマンス

WAAPI 的な「ふるい」

Window 関数は、行を選別するときに WHERE で使えない。サブクエリ / CTE で囲んで rn = 1 のパターンが定番。

失敗パターン

症状対処
WHERE で Window 関数が使えないサブクエリ / CTE に包む
LAST_VALUE が現在行の値しか返さないROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
RANK と DENSE_RANK の混乱同点の扱いを明示的に
遅いPARTITION + ORDER のインデックス
Window 関数の威力

GROUP BY だけでは「行が消える」のに対し、Window は残しながら計算できる。 これを覚えると SQL 一発で解ける問題が爆増する。レポート / ダッシュボード / リアルタイム分析の鍵。