SQL CTE・WITH句 — 複数CTE・累計・RFMの応用

応用CTE複数CTE・再帰CTE累計 / LEADRFMAPI実務PostgreSQL/BigQuery対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 6

CTE + CROSS JOIN + COALESCE — 欠損月を0埋めして完全マトリックスを作る

CTECROSS JOINCOALESCELEFT JOIN0埋めAPI
前提知識

実データには「売上がなかった月・商品の組み合わせ」が欠損します。ダッシュボードAPIでは全月×全商品の完全マトリックス(売上なしは0)が必要なことが多く、CROSS JOIN + LEFT JOIN + COALESCE で作ります。

-- CROSS JOIN: 2テーブルの全組み合わせ(直積)を生成
SELECT m.month, p.product_id
FROM   months   m
CROSS JOIN products p
-- months 3行 × products 4行 = 12行の全組み合わせ

-- COALESCE: 最初のNULLでない値を返す関数
COALESCE(sales, 0)   -- sales が NULL なら 0 を返す(売上なしを0で置換)
なぜ LEFT JOIN が必要か:CROSS JOINで「全組み合わせ」を作り、実績テーブルを LEFT JOIN すると「売上がなかった組み合わせ」の売上列が NULL になります。その NULL を COALESCE で 0 に変換して完成です。
問題

以下の sales テーブルと products テーブルから、2024-01〜2024-03 の全月 × 全商品の売上マトリックスを作成してください。

売上がない組み合わせは 0 で埋めること。月一覧は CTE で生成してください。

使用テーブル
▸ sales
sale_monthproduct_idamount
2024-01P0130000
2024-01P0245000
2024-02P0150000
2024-03P0220000
2024-03P0335000
▸ products
product_idproduct_name
P01りんご
P02バナナ
P03みかん
期待出力
sale_monthproduct_idproduct_nametotal_sales
2024-01P01りんご30000
2024-01P02バナナ45000
2024-01P03みかん0
2024-02P01りんご50000
2024-02P02バナナ0
2024-02P03みかん0
2024-03P01りんご0
2024-03P02バナナ20000
2024-03P03みかん35000
QUESTION 7

CTEでロジック分解 — 複雑な条件フィルタをステップに分けて可読性を上げる

複数CTEHAVING集計フィルタセグメントAPICTE
前提知識

実務では「2回以上購入し、合計購入額が50,000円以上で、最終購入日が特定日以降」といった複合条件のAPIが求められます。これを1クエリに詰め込むと保守困難になります。CTEでステップを分解すると各条件を独立してテスト・修正できます。

WITH filtered AS (               -- ① 行単位の絞り込みは WHERE
  SELECT *
  FROM   table_name
  WHERE  date_col >= DATE '2024-01-01'
), grouped AS (                    -- ② グループ単位の絞り込みは HAVING
  SELECT key_col,
         COUNT(*)      AS cnt,
         SUM(num_col)  AS total
  FROM   filtered
  GROUP BY key_col
  HAVING COUNT(*) >= 2
)
SELECT * FROM grouped;
HAVING:GROUP BY後のグループに対してフィルタをかける句。COUNT(*) >= 2 のような集計関数を条件にする場合は必ず HAVING を使います。WHEREはグループ化前(行単位)、HAVINGはグループ化後(集計単位)です。
問題

以下の ordersusers テーブルから、次の3つの条件をすべて満たすユーザーを抽出するAPIを、CTEを使ってステップに分解して作成してください。

条件①: 合計購入回数が2回以上
条件②: 合計購入額が50,000円以上
条件③: 最終購入日が 2024-03-01 以降

使用テーブル
▸ users
user_iduser_name
U01Alice
U02Bob
U03Carol
U04Dave
U05Eve
▸ orders
order_iduser_idamountorder_date
1U01200002024-01-15
2U01350002024-03-10
3U02600002024-02-20
4U03150002024-01-05
5U03250002024-03-22
6U04800002024-03-01
7U04100002024-04-05
8U05120002024-02-14
期待出力
user_iduser_nameorder_counttotal_amountlast_order_date
U01Alice2550002024-03-10
U04Dave2900002024-04-05
QUESTION 8

CTE + SUM OVER — 累計売上(ランニングトータル)を計算する

SUM OVERCTE累計ROWS BETWEEN進捗API
前提知識

SUM() OVER (ORDER BY ...) はウィンドウ関数で、現在行までの合計(累計)を計算します。月ごとの売上推移だけでなく「年間目標に対して今どこにいるか」を返す進捗APIに使います。

SUM(sales) OVER (
  ORDER BY order_month              -- 月順に並べてから累計を計算
  ROWS BETWEEN UNBOUNDED PRECEDING  -- ROWS BETWEEN: 集計対象の範囲を指定
    AND CURRENT ROW                  -- 「先頭行から現在行まで」= 累計
) AS cumulative_sales
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW:「先頭行から現在行まで」を意味するフレーム指定です。ORDER BY だけでも累計は計算されますが、明示的に書くと意図が明確になります。UNBOUNDED は「制限なし(端まで)」という意味です。
問題

以下の monthly_sales テーブルから、月ごとの売上・累計売上・年間目標(800,000円)に対する達成率(%)を計算するAPIを作成してください。

CTEで月次集計を行い、外側のSELECTで累計と達成率を計算してください。

使用テーブル
▸ monthly_sales
order_monthsales
2024-0195000
2024-02120000
2024-03108000
2024-04145000
2024-05132000
2024-06160000
期待出力
order_monthmonthly_salescumulative_salesachievement_rate
2024-01950009500011.9
2024-0212000021500026.9
2024-0310800032300040.4
2024-0414500046800058.5
2024-0513200060000075.0
2024-0616000076000095.0
QUESTION 9

CTE + LEAD — 次回購入日と購入間隔を計算してチャーン予測に使う

LEADCTE購入間隔チャーン分析LTV API
前提知識

LEAD(col, n) は LAG の逆で、現在行から n 行後の値を返します。「次の購入はいつか」「購入から次回購入まで何日かかったか」を計算するのに使います。

LEAD(order_date, 1) OVER (
  PARTITION BY user_id    -- ユーザーごとに独立して「次の行」を参照
  ORDER BY     order_date  -- 日付順に並べた次の行の order_date を返す
)
-- 最後の購入行(次がない行)は NULL になる
日付の差分計算:PostgreSQL では date2 - date1 で日数差を INTEGER で取得できます。BigQuery では DATE_DIFF(date2, date1, DAY) を使います。NULLが含まれる日付差はNULLになります(NULL伝播)。
問題

以下の purchase_log テーブルから、各ユーザー・各購入ごとの次回購入日と購入間隔(日数)を計算してください。

最後の購入(次回購入なし)は next_date を NULL、days_to_next も NULL で出力してください。

使用テーブル
▸ purchase_log
log_iduser_idorder_date
1U012024-01-10
2U012024-02-15
3U012024-04-01
4U022024-01-20
5U022024-03-05
6U032024-02-28
期待出力
user_idorder_datenext_datedays_to_next
U012024-01-102024-02-1536
U012024-02-152024-04-0146
U012024-04-01NULLNULL
U022024-01-202024-03-0545
U022024-03-05NULLNULL
U032024-02-28NULLNULL
QUESTION 10

複数CTE + CASE WHEN — RFMスコアでユーザーをセグメント分類する

複数CTECASE WHENRFM分析DATEDIFFセグメントAPI
前提知識

RFM分析は顧客をRecency(最終購入からの経過日数)・Frequency(購入回数)・Monetary(購入額)の3軸で評価し、セグメント分類するマーケティング手法です。複数CTEでステップを分けることで、各スコアの計算ロジックを独立させられます。

-- CASE WHEN: 条件分岐で値を変換する(if〜else ifのイメージ)
CASE
  WHEN days_since_activity < 14 THEN 5
  WHEN days_since_activity < 60 THEN 3
  ELSE                                1
END
基準日の設定:Recency は「今日から最終購入日までの日数」ですが、クエリの中では基準日を固定することが多いです(データが過去のものなので、CURRENT_DATE を使うとテスト時に結果が変わるため)。実務ではパラメータで受け取ります。
問題

以下の orders テーブルから、RFMスコア(各1〜3点)を計算してユーザーをセグメント(VIP / 一般 / 休眠)に分類するAPIを、複数CTEで作成してください。

基準日は 2024-04-01 とします。スコアリング基準:
R(Recency): <30日=3, <90日=2, それ以外=1
F(Frequency): ≥3回=3, ≥2回=2, それ以外=1
M(Monetary): ≥100000=3, ≥50000=2, それ以外=1
セグメント: R+F+M が 8以上=VIP, 5以上=一般, それ以外=休眠

使用テーブル
▸ orders
order_iduser_idamountorder_date
1U01300002024-01-10
2U01500002024-02-20
3U01400002024-03-25
4U02800002024-03-15
5U02600002024-03-28
6U031200002023-12-01
7U04150002024-03-30
8U04200002024-03-31
9U04180002024-04-01
期待出力
user_idrecency_daysfrequencymonetaryr_scoref_scorem_scoretotal_scoresegment
U01731200003339VIP
U02421400003238VIP
U0403530003328VIP
U0312211200001135一般