複数CTE連結 — RFMスコアでユーザーをセグメント分類するバッチ
複数の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;
以下の 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以上で「優良」、それ以外を「一般」とすること。
| order_id | user_id | order_date | amount |
|---|---|---|---|
| 1 | U01 | 2024-04-20 | 50000 |
| 2 | U01 | 2024-04-28 | 70000 |
| 3 | U01 | 2024-04-30 | 30000 |
| 4 | U02 | 2024-03-10 | 120000 |
| 5 | U02 | 2024-03-25 | 40000 |
| 6 | U03 | 2024-01-15 | 30000 |
| 7 | U04 | 2024-04-25 | 200000 |
| 8 | U04 | 2024-04-29 | 150000 |
| user_id | r_score | f_score | m_score | total_score | segment |
|---|---|---|---|---|---|
| U01 | 3 | 3 | 3 | 9 | VIP |
| U04 | 3 | 2 | 3 | 8 | VIP |
| U02 | 2 | 2 | 3 | 7 | 優良 |
| U03 | 1 | 1 | 1 | 3 | 一般 |
DENSE_RANK() — 同率を考慮したランキングと上位グループ抽出
同率スコアがある場合に「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; -- 同率=同順位、番号は飛ばさない
以下の game_scores テーブルから、ゲームごとにDENSE_RANKを使った順位を付け、3位以内の全プレイヤーを返してください。同率は同順位で全員表示すること。
| game_id | player_name | score |
|---|---|---|
| G01 | Alice | 9500 |
| G01 | Bob | 8200 |
| G01 | Carol | 8200 |
| G01 | Dave | 7800 |
| G01 | Eve | 7100 |
| G02 | Frank | 6000 |
| G02 | Grace | 6000 |
| G02 | Hank | 5500 |
| G02 | Iris | 4800 |
| game_id | player_name | score | rank |
|---|---|---|---|
| G01 | Alice | 9500 | 1 |
| G01 | Bob | 8200 | 2 |
| G01 | Carol | 8200 | 2 |
| G01 | Dave | 7800 | 3 |
| G02 | Frank | 6000 | 1 |
| G02 | Grace | 6000 | 1 |
| G02 | Hank | 5500 | 2 |
| G02 | Iris | 4800 | 3 |
LEFT JOIN + COALESCE — 売上ゼロ月を含めた完全な月別レポートを作る
売上がない月のデータは 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; -- 月で結合
2024年1月〜6月の全月について、月別集計レポートを作成してください。売上がない月は total_sales=0、order_count=0 とすること。
| order_id | order_month | amount |
|---|---|---|
| 1 | 2024-01 | 30000 |
| 2 | 2024-01 | 50000 |
| 3 | 2024-03 | 80000 |
| 4 | 2024-05 | 40000 |
| 5 | 2024-05 | 60000 |
| 6 | 2024-06 | 90000 |
| month |
|---|
| 2024-01 |
| 2024-02 |
| 2024-03 |
| 2024-04 |
| 2024-05 |
| 2024-06 |
| month | total_sales | order_count |
|---|---|---|
| 2024-01 | 80000 | 2 |
| 2024-02 | 0 | 0 |
| 2024-03 | 80000 | 1 |
| 2024-04 | 0 | 0 |
| 2024-05 | 100000 | 2 |
| 2024-06 | 90000 | 1 |
FIRST_VALUE() / LAST_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
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING で「全行」を明示してください。以下の purchase_history テーブルから、各ユーザーごとに初回購買日・初回購買商品・最終購買日・最終購買商品を1行で取得してください。ユーザーごとに1行だけ返すこと。
| user_id | order_date | product |
|---|---|---|
| U01 | 2024-01-10 | りんご |
| U01 | 2024-02-20 | バナナ |
| U01 | 2024-04-05 | みかん |
| U02 | 2024-01-15 | 緑茶 |
| U02 | 2024-03-30 | コーヒー |
| U03 | 2024-02-01 | 水 |
| user_id | first_date | first_product | last_date | last_product |
|---|---|---|---|---|
| U01 | 2024-01-10 | りんご | 2024-04-05 | みかん |
| U02 | 2024-01-15 | 緑茶 | 2024-03-30 | コーヒー |
| U03 | 2024-02-01 | 水 | 2024-02-01 | 水 |
再帰CTE (RECURSIVE) — 階層カテゴリを全レベル展開するバッチ
再帰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;
以下の categories テーブルは「商品カテゴリ」の親子関係を持ちます。再帰CTEを使って全階層を展開し、各カテゴリの depth(深さ)と階層パス(path、例:電化製品 > スマートフォン > Android)を表示してください。
| id | parent_id | name |
|---|---|---|
| 1 | NULL | 電化製品 |
| 2 | 1 | スマートフォン |
| 3 | 1 | パソコン |
| 4 | 2 | Android |
| 5 | 2 | iPhone |
| 6 | 3 | ノートPC |
| 7 | 3 | デスクトップ |
| id | name | depth | path |
|---|---|---|---|
| 1 | 電化製品 | 0 | 電化製品 |
| 2 | スマートフォン | 1 | 電化製品 > スマートフォン |
| 3 | パソコン | 1 | 電化製品 > パソコン |
| 4 | Android | 2 | 電化製品 > スマートフォン > Android |
| 5 | iPhone | 2 | 電化製品 > スマートフォン > iPhone |
| 6 | ノートPC | 2 | 電化製品 > パソコン > ノートPC |
| 7 | デスクトップ | 2 | 電化製品 > パソコン > デスクトップ |