SQL ウィンドウ関数 — LEAD・FIRST_VALUE・WINDOW句の応用

応用ウィンドウ関数実務パターンセッション分析移動平均PostgreSQL/MySQL8.0+/BigQuery対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 6

LEAD() — 次の行の値を先読みして成長率・差分を計算する

LEADORDER BY成長率分析LAGの逆方向版
前提知識

LEAD(expr, offset, default) は、現在の行から offset 行だけ先(未来)の値を返す関数です。デフォルトは1行先。パーティションの最終行など「次の行が存在しない」場合は default の値(省略時は NULL)を返します。

LEAD(revenue)         -- 1行先の revenue(デフォルト)
LEAD(revenue, 2)      -- 2行先の revenue
LEAD(revenue, 1, 0)  -- 1行先、存在しない場合は 0 を返す
LAG との違い:LAG() が「過去(前の行)」を参照するのに対し、LEAD() は「未来(次の行)」を参照します。どちらも「次/前」がどの行かは ORDER BY が決め、月次・週次の前期比や成長率の計算に頻出します。
問題

以下の monthly_revenue テーブルから、各月の売上と「翌月の売上(next_revenue)」、および「翌月との差分(diff = next_revenue − revenue)」を出力してください。翌月が存在しない場合は NULL としてください。

使用テーブル
▸ monthly_revenue
monthrevenue
2026-01100
2026-02120
2026-0390
2026-04150
期待出力
monthrevenuenext_revenuediff
2026-01100120+20
2026-0212090-30
2026-0390150+60
2026-04150NULLNULL
QUESTION 7

FIRST_VALUE() — カテゴリ内トップとの差分を一発で計算する

FIRST_VALUEPARTITION BYカテゴリ比較分析LAST_VALUEの罠あり
前提知識

FIRST_VALUE(expr) は、ウィンドウフレームの最初の行の値を返す関数です。PARTITION BY category ORDER BY score DESC と組み合わせると、「そのカテゴリでスコアが最も高い値(トップ)」を常に取得できます。

FIRST_VALUE(score) OVER(
  PARTITION BY category
  ORDER BY score DESC
) AS top_score  -- カテゴリ内 最高スコア
既定フレームと同点行:ORDER BY を指定した場合のデフォルトフレーム(通常は RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)には先頭行が常に含まれるため、どの行を処理中でも先頭行の値が返されます。同点行があると同じ並び順の値を持つ行がピアとして同じフレームに入りますが、FIRST_VALUE が返す値自体は変わりません。
問題

以下の product_scores テーブルから、各商品のスコアと、「同じカテゴリ内で最高のスコア(top_score)」および「トップとの差(gap = top_score − score)」を出力してください。カテゴリ内でスコアが高い順に並べてください。

使用テーブル
▸ product_scores
productcategoryscore
P1A85
P2A92
P3A78
P4B70
P5B88
期待出力
productcategoryscoretop_scoregap
P2A92920
P1A85927
P3A789214
P5B88880
P4B708818
QUESTION 8

移動平均 — 直近3日間のスライディング・ウィンドウを実装する

ROWS BETWEENAVG + PRECEDING移動平均スライディングウィンドウ
前提知識

移動平均(Moving Average)は、直近N期分のデータの平均を計算する手法で、株価・売上・アクセス数などのトレンド把握に頻用されます。ウィンドウ関数では ROWS BETWEEN でフレームを指定することで実現します。

AVG(amount) OVER(
  ORDER BY sale_date
  ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
)  -- 直近3日(2行前〜現在行)の平均
移動平均と累計平均の違い:フレームが「スライド(滑走)」するため、データが増えても常に直近N件だけを参照します。先頭行から現在行までを平均する累計平均とは対象範囲が根本的に異なります。フレームの先頭が足りない序盤の行では、存在する行だけが対象です。
問題

以下の daily_sales テーブルから、各日の売上と「直近3日間(当日を含む)の売上移動平均(moving_avg_3)」を計算してください。この表は日付の欠損がない1日1行のデータです。日付昇順で出力し、3日未満のデータがある初期の行については、存在する行のみで平均を計算してください。

使用テーブル
▸ daily_sales
sale_dateamount
04-0130
04-0250
04-0340
04-0460
04-0580
期待出力
sale_dateamountmoving_avg_3
04-013030.00
04-025040.00
04-034040.00
04-046050.00
04-058060.00
QUESTION 9

WINDOW句 — 共通ウィンドウ定義でDRY(重複排除)なSQLを書く

WINDOW句DRY原則コード保守性PostgreSQL/MySQL8+/BigQuery
前提知識

同じ PARTITION BY ... ORDER BY ... を複数のウィンドウ関数で繰り返す場合、WINDOW句を使うと名前付きウィンドウを定義して使い回せます(DRY: Don't Repeat Yourself 原則)。

SELECT
  ROW_NUMBER() OVER(w) AS rank,   -- 同じウィンドウ w を参照
  SUM(sales)   OVER(w) AS running,
  LAG(sales)   OVER(w) AS prev
FROM t
WINDOW w AS (PARTITION BY emp_id ORDER BY month); -- 1箇所で定義
WINDOW句の利点:WINDOW句なしで同じクエリを書くと、同一のウィンドウ定義を関数の数だけコピー貼り付けすることになります。名前付きウィンドウなら定義を変更するときも1箇所を直すだけで済みます。WINDOW句は FROMWHEREGROUP BY の後、ORDER BY の前に書きます。
問題

以下の employee_sales テーブルから、各従業員・月ごとに以下の3指標を付与してください。WINDOW句を使って PARTITION BY emp_id, dept ORDER BY month の定義を1箇所にまとめてください。

  1. rank:その社員のその月が「何ヶ月目か」(ROW_NUMBER)
  2. running:その社員のその時点までの累計売上(SUM)
  3. prev_sales:その社員の前月の売上(LAG)。初月は NULL
使用テーブル
▸ employee_sales
emp_iddeptmonthsales
E1Sales01100
E1Sales02120
E2Sales0180
E2Sales0290
E3Tech01150
期待出力
emp_iddeptmonthsalesrankrunningprev_sales
E1Sales011001100NULL
E1Sales021202220100
E2Sales0180180NULL
E2Sales0290217080
E3Tech011501150NULL
QUESTION 10

応用総まとめ — CTE + LAG + SUM でセッション分析を実装する

CTELAG + CASE WHENセッション分析実務最頻出パターン
前提知識

セッション分析は、ユーザーの行動ログから「1回のセッション(連続したアクション)」を識別するプロダクト分析の基本技術です。「前のイベントから30分を超えて経過したら新しいセッション開始」というルールを LAG + CASE WHEN で検出し、SUM の累積でセッション番号を振ります。差がちょうど30分なら同じセッションです。

WITH flagged AS (
  SELECT ...,
    CASE WHEN LAG(ts_col) OVER(...) IS NULL
            OR (ts_col - LAG(ts_col) OVER(...)) > gap_limit
         THEN 1 ELSE 0 END AS is_new_session
  FROM table_name
)
SELECT ...,
  SUM(is_new_session) OVER(PARTITION BY key_col ORDER BY ts_col) AS session_id
FROM flagged;
event_time の単位:event_time は「深夜0時からの経過分数」を表す整数です(例:600 = 10:00、660 = 11:00)。したがって時間差は単純な引き算で求まります。実際のDBでは DATEDIFFEXTRACT(EPOCH FROM ...) 等で時間差を計算します。
問題

以下の user_events テーブルから、ユーザーごとに「前のイベントから30分(event_time の差が30)を超えたら新しいセッション」というルールでセッション番号(session_id)を付与してください。各ユーザーの最初のイベントはセッション1から始めます。

使用テーブル(event_time は深夜0時からの経過分:600=10:00、605=10:05 など)
▸ user_events
user_idevent_timeevent_type
U1600login
U1605click
U1645click
U1650logout
U2660login
U2670click
期待出力
user_idevent_timeevent_typesession_id
U1600login1
U1605click1
U1645click2
U1650logout2
U2660login1
U2670click1