SQL バッチ処理 — 累計・ピボット・再帰CTEの応用

応用バッチ処理ウィンドウ関数累計・ピボット再帰CTEAPI実務PostgreSQL/BigQuery対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 6

複数CTE連結 — RFMスコアでユーザーをセグメント分類するバッチ

複数CTEWITHRFMセグメント分類
前提知識

複数のCTEをカンマ区切りで列挙すると、後のCTEが前のCTEを参照できるパイプライン構造を作れます。複雑なバッチ処理を「ステップ名付きの処理列」として整理できます。

WITH
step1 AS (
  SELECT user_id, COUNT(*) AS order_count
  FROM orders
  GROUP BY user_id
),
step2 AS (               -- step1 を参照できる
  SELECT *,
    CASE WHEN order_count >= 5 THEN '優良' ELSE '一般' END AS segment
  FROM step1
)
SELECT * FROM step2;
RFMとは:Recency(最終購買日)・Frequency(購買頻度)・Monetary(購買金額)の3指標でユーザーをスコアリングする手法。マーケティングのバッチ処理で非常に頻出です。
問題

以下の orders テーブルから、各ユーザーの R(最終購買からの日数)・F(購買回数)・M(購買金額合計) を計算し、それぞれのスコア(1〜3)を付けて最終セグメントを分類してください。

基準日:2024-05-01。R: 30日以内=3, 60日以内=2, それ以外=1。F: 3回以上=3, 2回=2, 1回=1。M: 100000以上=3, 50000以上=2, それ以外=1。
最終セグメントは合計スコア(total_score)が 8以上で「VIP」、6以上で「優良」、それ以外を「一般」とすること。

使用テーブル
▸ orders
order_iduser_idorder_dateamount
1U012024-04-2050000
2U012024-04-2870000
3U012024-04-3030000
4U022024-03-10120000
5U022024-03-2540000
6U032024-01-1530000
7U042024-04-25200000
8U042024-04-29150000
期待出力
user_idr_scoref_scorem_scoretotal_scoresegment
U013339VIP
U043238VIP
U022237優良
U031113一般
QUESTION 7

DENSE_RANK() — 同率を考慮したランキングと上位グループ抽出

DENSE_RANKウィンドウ関数同率対応ランキングAPI
前提知識

同率スコアがある場合に「3位タイまで全員表示したい」という要件では DENSE_RANK() が適切です。ROW_NUMBERとの違いを押さえましょう。

SELECT player, score,
  ROW_NUMBER()  OVER (ORDER BY score DESC) AS rn,    -- 同率でも強制的に連番
  RANK()        OVER (ORDER BY score DESC) AS rnk,   -- 同率=同順位、次をスキップ
  DENSE_RANK()  OVER (ORDER BY score DESC) AS dr;    -- 同率=同順位、番号は飛ばさない
使い分け:「上位N件(件数固定)」→ ROW_NUMBER。「上位N位タイ全員」→ DENSE_RANK。「ランキング表示でスキップあり」→ RANK。APIの要件に合わせて使い分けてください。
問題

以下の game_scores テーブルから、ゲームごとにDENSE_RANKを使った順位を付け、3位以内の全プレイヤーを返してください。同率は同順位で全員表示すること。

使用テーブル
▸ game_scores
game_idplayer_namescore
G01Alice9500
G01Bob8200
G01Carol8200
G01Dave7800
G01Eve7100
G02Frank6000
G02Grace6000
G02Hank5500
G02Iris4800
期待出力
game_idplayer_namescorerank
G01Alice95001
G01Bob82002
G01Carol82002
G01Dave78003
G02Frank60001
G02Grace60001
G02Hank55002
G02Iris48003
QUESTION 8

LEFT JOIN + COALESCE — 売上ゼロ月を含めた完全な月別レポートを作る

LEFT JOINCOALESCE欠損補完バッチレポート
前提知識

売上がない月のデータは orders テーブルに存在しません。しかし「売上0円の月も含めた月次レポート」が必要な場合があります。LEFT JOIN + COALESCE が王道パターンです。

SELECT
  m.month,
  COALESCE(s.total_sales, 0) AS total_sales  -- NULLなら0に置き換える
FROM       months        m        -- 全月リスト(マスタ)
LEFT JOIN  sales_summary s        -- 売上あり月のみ存在(NULLになる月あり)
        ON m.month = s.month;    -- 月で結合
LEFT JOIN vs INNER JOIN:INNER JOINは両テーブルに一致する行のみ返す(売上ない月が消える)。LEFT JOINは左テーブルの全行を保ちつつ、右テーブルに一致がない場合はNULLを返します。COALESCEでNULLを0に変換するのが売上0補完の定番イディオムです。
問題

2024年1月〜6月の全月について、月別集計レポートを作成してください。売上がない月は total_sales=0、order_count=0 とすること。

使用テーブル
▸ orders(売上データ)
order_idorder_monthamount
12024-0130000
22024-0150000
32024-0380000
42024-0540000
52024-0560000
62024-0690000
▸ month_master(月マスタ)
month
2024-01
2024-02
2024-03
2024-04
2024-05
2024-06
期待出力
monthtotal_salesorder_count
2024-01800002
2024-0200
2024-03800001
2024-0400
2024-051000002
2024-06900001
QUESTION 9

FIRST_VALUE() / LAST_VALUE() — 初回購買と最終購買を同じ行に並べる

FIRST_VALUELAST_VALUE初回/最終購買分析
前提知識

FIRST_VALUE() / LAST_VALUE() は、ウィンドウの最初・最後の行の値を取得します。「各ユーザーの初回購買商品と最終購買商品を同じ行に表示する」などの購買履歴分析に使います。

FIRST_VALUE(product) OVER (
  PARTITION BY user_id          -- ユーザーごとに独立
  ORDER BY     order_date        -- 日付昇順 → 最初の行が初回購買
) AS first_product

LAST_VALUE(product) OVER (
  PARTITION BY user_id
  ORDER BY     order_date
  ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING -- 全行を見る※重要
) AS last_product
LAST_VALUE の落とし穴:ROWS BETWEEN を省略すると「現在行まで」がデフォルトになり、最後の行以外では最新値が取れません。ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING で「全行」を明示してください。
問題

以下の purchase_history テーブルから、各ユーザーごとに初回購買日・初回購買商品・最終購買日・最終購買商品を1行で取得してください。ユーザーごとに1行だけ返すこと。

使用テーブル
▸ purchase_history
user_idorder_dateproduct
U012024-01-10りんご
U012024-02-20バナナ
U012024-04-05みかん
U022024-01-15緑茶
U022024-03-30コーヒー
U032024-02-01
期待出力
user_idfirst_datefirst_productlast_datelast_product
U012024-01-10りんご2024-04-05みかん
U022024-01-15緑茶2024-03-30コーヒー
U032024-02-012024-02-01
QUESTION 10

再帰CTE (RECURSIVE) — 階層カテゴリを全レベル展開するバッチ

RECURSIVE再帰CTE階層構造ツリー展開
前提知識

再帰CTE(WITH RECURSIVE)は自己参照するクエリです。「カテゴリツリー」「組織図」「コメントスレッド」など、階層構造(親子関係)を持つデータを展開する際に使います。

WITH RECURSIVE tree AS (

  -- アンカー部: 最初の行(ルートノード)を取得
  SELECT id, parent_id, name, 0 AS depth
  FROM   categories
  WHERE  parent_id IS NULL          -- 親なし = ルート

  UNION ALL                          -- アンカーと再帰部をつなぐ(重複除去しない)

  -- 再帰部: 前のステップの結果を参照して子を取得
  SELECT c.id, c.parent_id, c.name, t.depth + 1
  FROM       categories c
  INNER JOIN tree       t  -- 前のステップの tree を参照(再帰)
          ON c.parent_id = t.id
)
SELECT * FROM tree;
再帰の終了条件:再帰部で新たな行が生まれなくなった時点で再帰が終了します。循環参照(A→B→A...)があると無限ループになるため、実務では depth の上限チェックを設けることを推奨します。
問題

以下の categories テーブルは「商品カテゴリ」の親子関係を持ちます。再帰CTEを使って全階層を展開し、各カテゴリの depth(深さ)と階層パス(path、例:電化製品 > スマートフォン > Android)を表示してください。

使用テーブル
▸ categories
idparent_idname
1NULL電化製品
21スマートフォン
31パソコン
42Android
52iPhone
63ノートPC
73デスクトップ
期待出力
idnamedepthpath
1電化製品0電化製品
2スマートフォン1電化製品 > スマートフォン
3パソコン1電化製品 > パソコン
4Android2電化製品 > スマートフォン > Android
5iPhone2電化製品 > スマートフォン > iPhone
6ノートPC2電化製品 > パソコン > ノートPC
7デスクトップ2電化製品 > パソコン > デスクトップ