CASE式 × サブクエリ — 全体平均をCROSS JOINで取得してユーザーを3段階に分類する
サブクエリをCASE式の閾値として使うと、データ主導の動的分類が可能になります。1行しか返さないサブクエリをCROSS JOINすることで、全行に同じ集計値(定数)を効率よく付与できます。
SELECT s.user_id, s.total_spent, g.overall_avg, CASE WHEN s.total_spent >= 2 * g.overall_avg THEN 'HIGH_VALUE' WHEN s.total_spent >= g.overall_avg THEN 'NORMAL' ELSE 'LIGHT' END AS segment FROM (...) AS s -- ユーザー別集計(FROM句SQ) CROSS JOIN (...) AS g; -- 全体平均(1行)を全行に付与
orders の completed注文合計(total_spent)を持つユーザーを全体平均基準で HIGH_VALUE / NORMAL / LIGHT に分類してください。取得列は user_id, name, plan, total_spent, overall_avg, segment、total_spent降順。completedなしのユーザーは除外します。
| user_id | name | plan |
|---|---|---|
| 1 | 田中 太郎 | premium |
| 2 | 佐藤 花子 | free |
| 3 | 鈴木 一郎 | premium |
| 4 | 山田 次郎 | standard |
| 5 | 伊藤 三郎 | free |
| order_id | user_id | amount | status |
|---|---|---|---|
| 101 | 1 | 8000 | completed |
| 102 | 1 | 12000 | completed |
| 103 | 2 | 3500 | pending |
| 104 | 3 | 9500 | completed |
| 105 | 3 | 4000 | completed |
| 106 | 4 | 2000 | cancelled |
| 107 | 5 | 4500 | completed |
| 108 | 2 | 3000 | completed |
| user_id | name | plan | total_spent | overall_avg | segment |
|---|---|---|---|---|---|
| 1 | 田中 太郎 | premium | 20000 | 6833 | HIGH_VALUE |
| 3 | 鈴木 一郎 | premium | 13500 | 6833 | NORMAL |
| 5 | 伊藤 三郎 | free | 4500 | 6833 | LIGHT |
| 2 | 佐藤 花子 | free | 3000 | 6833 | LIGHT |
複数EXISTS連結 — AND/NOT EXISTSを組み合わせてターゲット顧客を絞り込む
WHERE 句に EXISTS / NOT EXISTS を AND で連結することで、複数の独立した条件をチェーンできます。各 EXISTS は完全に独立した相関サブクエリとして評価されるため、「Aを持ち、かつBを持ち、かつCを持たない」という複合フィルタを実現できます。
SELECT u.user_id, u.name FROM users u WHERE EXISTS (SELECT 1 FROM orders o1 WHERE o1.user_id = u.user_id AND ...) AND EXISTS (SELECT 1 FROM orders o2 WHERE o2.user_id = u.user_id AND ...) AND NOT EXISTS (SELECT 1 FROM orders o3 WHERE o3.user_id = u.user_id AND ...);
users テーブルから以下の3条件をすべて満たすユーザーを取得してください:
① completedの注文が1件以上ある(EXISTS)
② 2024-05-16以降の注文が1件以上ある(EXISTS)
③ cancelledの注文が1件もない(NOT EXISTS)
取得列は user_id, name, plan、user_id昇順。
| user_id | name | plan |
|---|---|---|
| 1 | 田中 太郎 | premium |
| 2 | 佐藤 花子 | free |
| 3 | 鈴木 一郎 | premium |
| 4 | 山田 次郎 | standard |
| 5 | 伊藤 三郎 | free |
| order_id | user_id | amount | status | ordered_at |
|---|---|---|---|---|
| 101 | 1 | 8000 | completed | 2024-04-10 |
| 102 | 1 | 12000 | completed | 2024-05-20 |
| 103 | 2 | 3500 | pending | 2024-05-10 |
| 104 | 3 | 9500 | completed | 2024-05-15 |
| 105 | 3 | 4000 | completed | 2024-06-01 |
| 106 | 4 | 2000 | cancelled | 2024-05-08 |
| 107 | 5 | 4500 | completed | 2024-04-20 |
| 108 | 2 | 5500 | completed | 2024-06-10 |
| user_id | name | plan |
|---|---|---|
| 1 | 田中 太郎 | premium |
| 2 | 佐藤 花子 | free |
| 3 | 鈴木 一郎 | premium |
Top-N相関サブクエリ — カテゴリ別上位2件をサブクエリで取得する
「カテゴリ別上位N件」の抽出は実務で非常に頻出です。相関サブクエリで「自分より高い行が何件あるか」を数えると、ROW_NUMBER()ウィンドウ関数なしでもTop-N抽出が実現できます。
SELECT p.* FROM products p WHERE ( SELECT COUNT(*) FROM products p2 WHERE p2.category = p.category -- 同じカテゴリ内で AND p2.price > p.price -- 自分より高い商品が ) < 2; -- 2件未満 → 自分は上位2件
products テーブルから、カテゴリ別に price 降順で上位2件の商品を取得してください。取得列は product_id, name, category, price、category 昇順・同一カテゴリ内は price 降順で並べてください。
サブクエリのみで実装し、ウィンドウ関数は使わないこと。
| product_id | name | category | price |
|---|---|---|---|
| 1 | プランA | service | 9800 |
| 2 | プランB | service | 4900 |
| 3 | プランC | service | 2500 |
| 4 | テンプレートX | content | 5500 |
| 5 | テンプレートY | content | 3800 |
| 6 | テンプレートZ | content | 1200 |
| 7 | APIアドオン | option | 3500 |
| 8 | サポート拡張 | option | 2200 |
| 9 | ストレージ追加 | option | 1500 |
| product_id | name | category | price |
|---|---|---|---|
| 4 | テンプレートX | content | 5500 |
| 5 | テンプレートY | content | 3800 |
| 7 | APIアドオン | option | 3500 |
| 8 | サポート拡張 | option | 2200 |
| 1 | プランA | service | 9800 |
| 2 | プランB | service | 4900 |
多段CTE — WITH句を3段階で積み上げてコホート分析を実装する
複数のCTE(WITH句)を連鎖させる多段CTEは、複雑な分析クエリを段階的に分解する実務の標準手法です。各CTEが前のCTEを参照できるため、「ステップ1の結果をステップ2で絞り込み、その結果をステップ3で集計」というパイプラインを直感的に記述できます。
WITH step1 AS ( -- 第1段:基礎集計 SELECT ... FROM source_table ), step2 AS ( -- 第2段:step1を参照して絞り込み SELECT ... FROM step1 WHERE ... ), step3 AS ( -- 第3段:step2をさらに加工 SELECT ... FROM step2 JOIN ... ) SELECT * FROM step3;
以下の3ステップを多段CTEで実装してください:
① user_stats:ユーザー別に completed注文の件数(order_cnt)と合計金額(total_spent)を集計
② active_users:①から order_cnt が 2 以上のユーザーだけを抽出
③ 最終SELECT:②と usersテーブルを JOIN し、user_id, name, plan, order_cnt, total_spent を total_spent 降順で返す
| user_id | name | plan |
|---|---|---|
| 1 | 田中 太郎 | premium |
| 2 | 佐藤 花子 | free |
| 3 | 鈴木 一郎 | premium |
| 4 | 山田 次郎 | standard |
| 5 | 伊藤 三郎 | free |
| order_id | user_id | amount | status |
|---|---|---|---|
| 101 | 1 | 8000 | completed |
| 102 | 1 | 12000 | completed |
| 103 | 2 | 3500 | pending |
| 104 | 3 | 9500 | completed |
| 105 | 3 | 4000 | completed |
| 106 | 4 | 2000 | cancelled |
| 107 | 5 | 4500 | completed |
| 108 | 1 | 6500 | completed |
| user_id | name | plan | order_cnt | total_spent |
|---|---|---|---|---|
| 1 | 田中 太郎 | premium | 3 | 26500 |
| 3 | 鈴木 一郎 | premium | 2 | 13500 |
CTEスコアリング — 複数指標を独立集計してスコアを合算するユーザー評価クエリ
実務では「合計金額・注文件数・活動頻度」など複数の指標を合算したスコアで顧客ランクを決めるケースがよくあります。各指標を独立したCTEで集計し、最終CTEでスコアを合算するパターンは保守性・可読性に優れた設計です。
WITH metric_a AS ( -- ① 指標ごとに独立して集計する SELECT key_col, SUM(num_col) AS total FROM table_a GROUP BY key_col ), metric_b AS ( SELECT key_col, COUNT(*) AS cnt FROM table_b GROUP BY key_col ), scored AS ( -- ② 最終CTEでスコアを合算する SELECT a.key_col, CASE WHEN a.total >= 1000 THEN 3 ELSE 1 END + CASE WHEN b.cnt >= 10 THEN 2 ELSE 0 END AS score FROM metric_a a JOIN metric_b b ON b.key_col = a.key_col ) SELECT * FROM scored;
以下の手順でユーザーの総合スコアを算出し、スコア降順・同スコアは user_id 昇順でランキングを返してください:
① spend_score:ユーザー別の completed合計金額(total_spent)に基づき 10,000以上 → 3点 / 5,000以上 → 2点 / それ以下 → 1点(completedなしは0点)
② freq_score:ユーザー別の completed注文件数(order_cnt)に基づき 3件以上 → 3点 / 2件以上 → 2点 / それ以下 → 1点(completedなしは0点)
③ 最終SELECT:全ユーザーを対象に user_id, name, spend_score, freq_score, total_score(合計) を返す
| user_id | name | plan |
|---|---|---|
| 1 | 田中 太郎 | premium |
| 2 | 佐藤 花子 | free |
| 3 | 鈴木 一郎 | premium |
| 4 | 山田 次郎 | standard |
| 5 | 伊藤 三郎 | free |
| order_id | user_id | amount | status |
|---|---|---|---|
| 101 | 1 | 8000 | completed |
| 102 | 1 | 12000 | completed |
| 103 | 2 | 3500 | pending |
| 104 | 3 | 9500 | completed |
| 105 | 3 | 4000 | completed |
| 106 | 4 | 2000 | cancelled |
| 107 | 5 | 4500 | completed |
| 108 | 1 | 6500 | completed |
| user_id | name | spend_score | freq_score | total_score |
|---|---|---|---|---|
| 1 | 田中 太郎 | 3 | 3 | 6 |
| 3 | 鈴木 一郎 | 3 | 2 | 5 |
| 5 | 伊藤 三郎 | 1 | 1 | 2 |
| 2 | 佐藤 花子 | 0 | 0 | 0 |
| 4 | 山田 次郎 | 0 | 0 | 0 |