CTE + CROSS JOIN + COALESCE — 欠損月を0埋めして完全マトリックスを作る
実データには「売上がなかった月・商品の組み合わせ」が欠損します。ダッシュボード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で置換)
以下の sales テーブルと products テーブルから、2024-01〜2024-03 の全月 × 全商品の売上マトリックスを作成してください。
売上がない組み合わせは 0 で埋めること。月一覧は CTE で生成してください。
| sale_month | product_id | amount |
|---|---|---|
| 2024-01 | P01 | 30000 |
| 2024-01 | P02 | 45000 |
| 2024-02 | P01 | 50000 |
| 2024-03 | P02 | 20000 |
| 2024-03 | P03 | 35000 |
| product_id | product_name |
|---|---|
| P01 | りんご |
| P02 | バナナ |
| P03 | みかん |
| sale_month | product_id | product_name | total_sales |
|---|---|---|---|
| 2024-01 | P01 | りんご | 30000 |
| 2024-01 | P02 | バナナ | 45000 |
| 2024-01 | P03 | みかん | 0 |
| 2024-02 | P01 | りんご | 50000 |
| 2024-02 | P02 | バナナ | 0 |
| 2024-02 | P03 | みかん | 0 |
| 2024-03 | P01 | りんご | 0 |
| 2024-03 | P02 | バナナ | 20000 |
| 2024-03 | P03 | みかん | 35000 |
CTEでロジック分解 — 複雑な条件フィルタをステップに分けて可読性を上げる
実務では「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;
COUNT(*) >= 2 のような集計関数を条件にする場合は必ず HAVING を使います。WHEREはグループ化前(行単位)、HAVINGはグループ化後(集計単位)です。以下の orders と users テーブルから、次の3つの条件をすべて満たすユーザーを抽出するAPIを、CTEを使ってステップに分解して作成してください。
条件①: 合計購入回数が2回以上
条件②: 合計購入額が50,000円以上
条件③: 最終購入日が 2024-03-01 以降
| user_id | user_name |
|---|---|
| U01 | Alice |
| U02 | Bob |
| U03 | Carol |
| U04 | Dave |
| U05 | Eve |
| order_id | user_id | amount | order_date |
|---|---|---|---|
| 1 | U01 | 20000 | 2024-01-15 |
| 2 | U01 | 35000 | 2024-03-10 |
| 3 | U02 | 60000 | 2024-02-20 |
| 4 | U03 | 15000 | 2024-01-05 |
| 5 | U03 | 25000 | 2024-03-22 |
| 6 | U04 | 80000 | 2024-03-01 |
| 7 | U04 | 10000 | 2024-04-05 |
| 8 | U05 | 12000 | 2024-02-14 |
| user_id | user_name | order_count | total_amount | last_order_date |
|---|---|---|---|---|
| U01 | Alice | 2 | 55000 | 2024-03-10 |
| U04 | Dave | 2 | 90000 | 2024-04-05 |
CTE + SUM OVER — 累計売上(ランニングトータル)を計算する
SUM() OVER (ORDER BY ...) はウィンドウ関数で、現在行までの合計(累計)を計算します。月ごとの売上推移だけでなく「年間目標に対して今どこにいるか」を返す進捗APIに使います。
SUM(sales) OVER ( ORDER BY order_month -- 月順に並べてから累計を計算 ROWS BETWEEN UNBOUNDED PRECEDING -- ROWS BETWEEN: 集計対象の範囲を指定 AND CURRENT ROW -- 「先頭行から現在行まで」= 累計 ) AS cumulative_sales
UNBOUNDED は「制限なし(端まで)」という意味です。以下の monthly_sales テーブルから、月ごとの売上・累計売上・年間目標(800,000円)に対する達成率(%)を計算するAPIを作成してください。
CTEで月次集計を行い、外側のSELECTで累計と達成率を計算してください。
| order_month | sales |
|---|---|
| 2024-01 | 95000 |
| 2024-02 | 120000 |
| 2024-03 | 108000 |
| 2024-04 | 145000 |
| 2024-05 | 132000 |
| 2024-06 | 160000 |
| order_month | monthly_sales | cumulative_sales | achievement_rate |
|---|---|---|---|
| 2024-01 | 95000 | 95000 | 11.9 |
| 2024-02 | 120000 | 215000 | 26.9 |
| 2024-03 | 108000 | 323000 | 40.4 |
| 2024-04 | 145000 | 468000 | 58.5 |
| 2024-05 | 132000 | 600000 | 75.0 |
| 2024-06 | 160000 | 760000 | 95.0 |
CTE + LEAD — 次回購入日と購入間隔を計算してチャーン予測に使う
LEAD(col, n) は LAG の逆で、現在行から n 行後の値を返します。「次の購入はいつか」「購入から次回購入まで何日かかったか」を計算するのに使います。
LEAD(order_date, 1) OVER ( PARTITION BY user_id -- ユーザーごとに独立して「次の行」を参照 ORDER BY order_date -- 日付順に並べた次の行の order_date を返す ) -- 最後の購入行(次がない行)は NULL になる
date2 - date1 で日数差を INTEGER で取得できます。BigQuery では DATE_DIFF(date2, date1, DAY) を使います。NULLが含まれる日付差はNULLになります(NULL伝播)。以下の purchase_log テーブルから、各ユーザー・各購入ごとの次回購入日と購入間隔(日数)を計算してください。
最後の購入(次回購入なし)は next_date を NULL、days_to_next も NULL で出力してください。
| log_id | user_id | order_date |
|---|---|---|
| 1 | U01 | 2024-01-10 |
| 2 | U01 | 2024-02-15 |
| 3 | U01 | 2024-04-01 |
| 4 | U02 | 2024-01-20 |
| 5 | U02 | 2024-03-05 |
| 6 | U03 | 2024-02-28 |
| user_id | order_date | next_date | days_to_next |
|---|---|---|---|
| U01 | 2024-01-10 | 2024-02-15 | 36 |
| U01 | 2024-02-15 | 2024-04-01 | 46 |
| U01 | 2024-04-01 | NULL | NULL |
| U02 | 2024-01-20 | 2024-03-05 | 45 |
| U02 | 2024-03-05 | NULL | NULL |
| U03 | 2024-02-28 | NULL | NULL |
複数CTE + CASE WHEN — RFMスコアでユーザーをセグメント分類する
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
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以上=一般, それ以外=休眠
| order_id | user_id | amount | order_date |
|---|---|---|---|
| 1 | U01 | 30000 | 2024-01-10 |
| 2 | U01 | 50000 | 2024-02-20 |
| 3 | U01 | 40000 | 2024-03-25 |
| 4 | U02 | 80000 | 2024-03-15 |
| 5 | U02 | 60000 | 2024-03-28 |
| 6 | U03 | 120000 | 2023-12-01 |
| 7 | U04 | 15000 | 2024-03-30 |
| 8 | U04 | 20000 | 2024-03-31 |
| 9 | U04 | 18000 | 2024-04-01 |
| user_id | recency_days | frequency | monetary | r_score | f_score | m_score | total_score | segment |
|---|---|---|---|---|---|---|---|---|
| U01 | 7 | 3 | 120000 | 3 | 3 | 3 | 9 | VIP |
| U02 | 4 | 2 | 140000 | 3 | 2 | 3 | 8 | VIP |
| U04 | 0 | 3 | 53000 | 3 | 3 | 2 | 8 | VIP |
| U03 | 122 | 1 | 120000 | 1 | 1 | 3 | 5 | 一般 |