SQL ウィンドウ関数 — PERCENT_RANK・LAG+LEADの応用

応用ウィンドウ関数NTILE / PERCENT_RANKCTELAG + LEADパーティション別移動平均PostgreSQL/MySQL8.0+/BigQuery対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 6

NTILE() — 購入額を4分位に分割してユーザーをセグメント化する

NTILE番号付け関数セグメンテーション
前提知識

NTILE(N) は全行を 最大 N 個のサイズ差が1行以内のグループ(バケット)に分割し、各行に 1〜N の範囲でグループ番号を割り当てます。
ROW_NUMBER / RANK / DENSE_RANK と同じ「番号付け系」関数の仲間です。

NTILE(4) OVER(ORDER BY total_purchase DESC)
-- 購入額を高い順に並べて4等分
-- 1=最高額グループ(上位25%)
-- 4=最低額グループ(下位25%)
割り切れない場合の動作:行数が N で割り切れないとき、余りの行は 先頭のグループから1行ずつ余分に割り当てられます。例:6行÷4グループ → tier1,tier2 に2行ずつ、tier3,tier4 に1行ずつ。
問題

以下の customers テーブルを使用し、購入総額(total_purchase)の高い順に全ユーザーを 4段階のティア (tier) に分類してください。
tier 1 が最高額、tier 4 が最低額グループとなるよう降順で分割すること。

使用テーブル
▸ customers
user_idnametotal_purchase
U1田中48000
U2鈴木12000
U3佐藤95000
U4高橋33000
U5伊藤71000
U6渡辺8000
期待出力
user_idnametotal_purchasetier
U3佐藤950001
U5伊藤710001
U1田中480002
U4高橋330002
U2鈴木120003
U6渡辺80004
QUESTION 7

PERCENT_RANK() — スコアが全体の何%に位置するかを計算する

PERCENT_RANK相対順位パーセンタイル
前提知識

PERCENT_RANK() は各行の「全体の中での相対的な位置」を 0.0〜1.0 の小数で返します。
複数行なら最小値の行は 0.0(0パーセンタイル)、最大値の行は 1.0(100パーセンタイル)になります。1行だけの場合は 0.0 です。

PERCENT_RANK() OVER(ORDER BY score)
-- 計算式: (rank - 1) / (total_rows - 1)
-- 例: 5行中3位 → (3-1)/(5-1) = 0.5 = 50パーセンタイル
CUME_DIST との違い:CUME_DIST()(累積分布)は「自分以下の行が全体の何割か」を返し、0 より大きく 1 以下の値になります。PERCENT_RANK は順位を (rank - 1) / (total_rows - 1) で正規化して 0 始まりになります。両者は同点がなくても値が異なり、同点(タイ)が生じると順位の扱いによる差がさらに重要になります。
問題

以下の exam_scores テーブルを使用し、各社員のテストスコアが全体の中で何%に位置するか(パーセンタイル)を求めてください。
出力は小数点以下1桁(%換算)で表示し、スコアの昇順で出力すること。

使用テーブル
▸ exam_scores
employee_idnamescore
E1田中70
E2鈴木90
E3佐藤60
E4高橋85
E5伊藤75
期待出力
namescorepercentile
佐藤600.0
田中7025.0
伊藤7550.0
高橋8575.0
鈴木90100.0
QUESTION 8

WITH句(CTE)— ウィンドウ関数をモジュール化して累計・構成比を求める

CTE / WITH句SUM OVER累計・構成比可読性向上
前提知識

ウィンドウ関数を含む長いクエリをサブクエリで書くと、ネストが深くなり読みづらくなります。WITH 句(CTE: Common Table Expression)を使うと、「まずウィンドウ関数で中間テーブルを作り、次にそれを使って計算する」という2ステップを上から下へ自然に読めるモダンな書き方になります。

WITH cte_name AS (
  -- ① ウィンドウ関数で中間テーブルを作成
  SELECT ..., SUM(col) OVER(...) AS running_total
  FROM tbl
)
-- ② 中間テーブルを参照して追加計算
SELECT ..., running_total / total * 100 AS pct
FROM cte_name;
OVER() の意味(引数なし):SUM(revenue) OVER() のように OVER 内に何も書かないと、全行を対象にした集計が計算されます(= テーブル全体の合計)。これを利用して「全体合計」と「累計」を1つのCTE内で同時に計算できます。
問題

以下の monthly_revenue テーブルを使用し、WITH 句を活用して月ごとに以下の3つの値を出力してください。
1. running_total — 1月からの累計売上
2. monthly_pct — その月の売上が全体に占める割合(%、小数点1桁)

使用テーブル
▸ monthly_revenue
monthrevenue
01月200
02月250
03月180
04月320
05月290
期待出力
monthrevenuerunning_totalmonthly_pct
01月20020016.1
02月25045020.2
03月18063014.5
04月32095025.8
05月290124023.4
QUESTION 9

LAG() + LEAD() 同時活用 — 前後の値を比較して「ピーク日」を自動検出する

LAG + LEAD前後比較CASE WHEN異常検知
前提知識

LAG(前の行)と LEAD(次の行)を 同時に 使うことで、「前後の値との3点比較」が可能になります。これはデータのピーク(極大値)・谷(極小値)検出、急変検知など、時系列分析で頻出のパターンです。

LAG(amount)  OVER(ORDER BY date)  AS prev_amount  -- 1行前の値
LEAD(amount) OVER(ORDER BY date)  AS next_amount  -- 1行後の値

-- CASE WHEN で3点比較 → ピーク判定
CASE
  WHEN amount > prev_amount AND amount > next_amount THEN 'PEAK'
END
先頭行・末尾行の扱い:先頭行の LAG は NULL、末尾行の LEAD は NULL を返します。NULL との比較(> NULL)は UNKNOWN となり、CASE WHEN では真として扱われないため、先頭・末尾行は自動的にピーク判定から除外されます。これは意図通りの動作です。
問題

以下の daily_sales テーブルを使用し、各日付の「前日の売上 prev_amount」「翌日の売上 next_amount」を横に並べ、さらに前後両方の日より売上が高い日に 'PEAK' を、それ以外は空文字 ''is_peak カラムとして出力してください。

使用テーブル
▸ daily_sales
dateamount
04-0180
04-02160
04-03110
04-04190
04-05130
04-06175
期待出力
dateamountprev_amountnext_amountis_peak
04-0180NULL160
04-0216080110PEAK
04-03110160190
04-04190110130PEAK
04-05130190175
04-06175130NULL
QUESTION 10

PARTITION BY × フレーム指定 — 店舗ごとに独立した移動平均を計算する

PARTITION BYROWS BETWEENグループ別移動平均実務最頻出
前提知識

PARTITION BY(グループ分割)と ROWS BETWEEN(フレーム指定)を組み合わせることで、「各グループの中だけで動くウィンドウ」を実現できます。これはウィンドウ関数の中でも最も実務的なパターンのひとつです。

AVG(amount) OVER(
  PARTITION BY store_id       -- 店舗単位でウィンドウを区切る
  ORDER BY date               -- 各パーティション内で日付順に並べる
  ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
  -- 直前1行 + 現在行 = 2日間平均
)
PARTITION BY の「リセット」効果:PARTITION BY を指定すると、パーティションが切り替わるたびにフレームが 必ずリセット(先頭から再開)されます。別の店舗の行を誤って巻き込む「クロスパーティション汚染」が自動的に防がれます。
問題

以下の store_daily_sales テーブルを使用し、店舗(store_id)ごとに独立した「直近2日間の移動平均(moving_avg_2d)」を計算してください。
つまり、店舗が切り替わった時点でウィンドウが必ずリセットされる必要があります。

使用テーブル
▸ store_daily_sales
store_iddateamount
S104-01100
S104-02140
S104-03120
S104-04160
S204-01200
S204-02180
S204-03220
S204-04190
期待出力
store_iddateamountmoving_avg_2d
S104-01100100.0
S104-02140120.0
S104-03120130.0
S104-04160140.0
S204-01200200.0
S204-02180190.0
S204-03220200.0
S204-04190205.0