相関サブクエリ — 各ユーザーの「最新注文」だけを取得する
相関サブクエリ(Correlated Subquery)とは、外側のクエリの列を内側のサブクエリが参照するサブクエリです。外側の行ごとにサブクエリが評価されるため、「この行と同じ条件で絞ったときの最大値・最小値」という処理に向いています。
SELECT * FROM orders o1 WHERE o1.ordered_at = ( SELECT MAX(o2.ordered_at) -- 内側のクエリが外側の o1.user_id を参照 → 相関サブクエリ FROM orders o2 WHERE o2.user_id = o1.user_id -- ← ここが「相関」の核心。外側の行ごとに評価される );
「各ユーザーの直近ログイン」「各商品の最後の価格変更」など、グループ内の最新行取得は API・バッチ処理で最頻出のパターンの一つです。
ROW_NUMBER() ウィンドウ関数を使った書き方が高速な場合があります。ただし可読性が高いため少量データや素直な要件には適しています。orders テーブルから、各ユーザー(user_id)の最新注文(ordered_at が最大)の行を取得してください。同一ユーザーの複数注文が存在する場合、最新の1行だけ返すこと。結果は user_id 昇順で並べてください。
| order_id | user_id | amount | ordered_at |
|---|---|---|---|
| 1001 | U01 | 3200 | 2024-03-10 |
| 1002 | U01 | 1500 | 2024-05-20 |
| 1003 | U02 | 8000 | 2024-04-01 |
| 1004 | U02 | 2100 | 2024-06-15 |
| 1005 | U03 | 500 | 2024-02-28 |
| 1006 | U01 | 4700 | 2024-06-01 |
| order_id | user_id | amount | ordered_at |
|---|---|---|---|
| 1006 | U01 | 4700 | 2024-06-01 |
| 1004 | U02 | 2100 | 2024-06-15 |
| 1005 | U03 | 500 | 2024-02-28 |
WITH句(CTE) — 月次売上レポートを段階的に組み立てる
WITH句(CTE:Common Table Expression)は、クエリの中で一時的な名前付き結果セットを定義できる構文です。複雑なサブクエリをネストして書く代わりに、処理を「段階的なブロック」として切り出せるため、可読性と保守性が大幅に向上します。
WITH cte_name1 AS ( -- 第1ブロック:最初の一時テーブルを定義 SELECT ... FROM some_table ), cte_name2 AS ( -- 第2ブロック:cte_name1 を参照できる SELECT ... FROM cte_name1 ) SELECT * FROM cte_name2; -- 最後のSELECTが実際の出力
MATERIALIZEDオプションで一度だけ計算させられる)、③再帰クエリを書くとき。sales テーブルから、月別・商品カテゴリ別の売上合計と月次全体平均に対する比率(%)を取得してください。CTE を使って「月別カテゴリ集計」→「月別合計」→「比率計算」の3段階で組み立てること。結果は月・カテゴリ昇順で並べてください。
| sale_id | category | amount | sold_at |
|---|---|---|---|
| 1 | food | 3000 | 2024-05-03 |
| 2 | drink | 1500 | 2024-05-10 |
| 3 | food | 2000 | 2024-05-20 |
| 4 | drink | 2500 | 2024-06-05 |
| 5 | food | 4000 | 2024-06-12 |
| 6 | drink | 1000 | 2024-06-20 |
| month | category | cat_total | month_total | ratio_pct |
|---|---|---|---|---|
| 2024-05 | drink | 1500 | 6500 | 23.08 |
| 2024-05 | food | 5000 | 6500 | 76.92 |
| 2024-06 | drink | 3500 | 7500 | 46.67 |
| 2024-06 | food | 4000 | 7500 | 53.33 |
CASE WHEN + 条件付き集計 — 在庫状況のピボット集計と FILTER 句
CASE WHEN は SQL の条件分岐式です。集計関数の内部に組み込むことで、「特定条件を満たす行だけを集計する」いわゆる 条件付き集計(ピボット集計) が実現できます。PostgreSQL では FILTER (WHERE ...) という専用の構文も使えます。
-- CASE WHEN を使った条件付き集計(標準SQL) SUM(CASE WHEN status = 'active' THEN amount ELSE 0 END) AS active_sum -- FILTER 句を使った条件付き集計(PostgreSQL) SUM(amount) FILTER (WHERE status = 'active') AS active_sum
inventory テーブルから、各倉庫(warehouse)ごとに「在庫あり(quantity > 0)の商品数」「在庫切れ(quantity = 0)の商品数」「全商品数」を1行にまとめたピボット集計を取得してください。
| item_id | warehouse | quantity |
|---|---|---|
| A01 | 東京 | 50 |
| A02 | 東京 | 0 |
| A03 | 東京 | 20 |
| B01 | 大阪 | 0 |
| B02 | 大阪 | 0 |
| B03 | 大阪 | 15 |
| C01 | 福岡 | 100 |
| C02 | 福岡 | 30 |
| warehouse | in_stock | out_of_stock | total_items |
|---|---|---|---|
| 東京 | 2 | 1 | 3 |
| 大阪 | 1 | 2 | 3 |
| 福岡 | 2 | 0 | 2 |
LAG / LEAD — 前月比・前行比の計算でトレンドを可視化する
LAG(列, n) はウィンドウ関数で、現在行から n 行前の値を返します。LEAD(列, n) は逆に n 行後の値を返します。GROUP BY して行を集約することなく「前の行との差分」を同じ行に付与できるのが最大の特徴です。
LAG(col, 1, 0) OVER ( -- col の 1行前の値。前行がない場合はデフォルト 0 PARTITION BY group_col -- グループ内で独立した順序を保つ ORDER BY sort_col -- この順序で「前後」を定義する(必須) ) LEAD(col, 1) OVER ( -- col の 1行後の値(次の行) ORDER BY sort_col )
OVER 句内の ORDER BY が必須です。順序が定まらないと「前の行」が何なのか決まりません。monthly_sales テーブルから、各商品(product_id)の月別売上と前月売上・前月比(%)を取得してください。前月データがない場合(最初の月)は前月売上は NULL、前月比は NULL で構いません。結果は product_id・month の昇順で並べてください。
| product_id | month | revenue |
|---|---|---|
| P01 | 2024-03 | 10000 |
| P01 | 2024-04 | 12000 |
| P01 | 2024-05 | 9000 |
| P02 | 2024-03 | 5000 |
| P02 | 2024-04 | 7500 |
| P02 | 2024-05 | 7500 |
| product_id | month | revenue | prev_revenue | mom_pct |
|---|---|---|---|---|
| P01 | 2024-03 | 10000 | NULL | NULL |
| P01 | 2024-04 | 12000 | 10000 | +20.00 |
| P01 | 2024-05 | 9000 | 12000 | -25.00 |
| P02 | 2024-03 | 5000 | NULL | NULL |
| P02 | 2024-04 | 7500 | 5000 | +50.00 |
| P02 | 2024-05 | 7500 | 7500 | 0.00 |
再帰CTE(WITH RECURSIVE) — カテゴリ階層の全子孫を取得する
WITH RECURSIVE は、CTE が自分自身を参照する「再帰クエリ」を書くための構文です。階層データ(カテゴリツリー・組織図・部品表)を再帰的にたどるときに使います。
WITH RECURSIVE cte AS ( -- ① アンカー部(再帰の起点。最初に1回だけ実行) SELECT id, parent_id, name, 0 AS depth FROM categories WHERE id = 1 -- 起点となる行を1件選ぶ UNION ALL -- アンカー部と再帰部をつなぐ(重複排除なしで高速) -- ② 再帰部(cte = 前の反復の結果を参照。結果が0行になるまで繰り返す) SELECT c.id, c.parent_id, c.name, r.depth + 1 FROM categories c JOIN cte r ON c.parent_id = r.id -- 前の反復の id を親IDとして持つ行を取得 ) SELECT * FROM cte;
depth < 10 などの上限条件か、path 列を使ったループ検知を必ず入れましょう。categories テーブルは parent_id で自己参照する階層構造です。category_id = 1 を起点として、その全ての子孫カテゴリ(直接の子だけでなく孫・曾孫も含む)を depth(階層深さ)付きで取得してください。深さは起点を 0 とします。
| category_id | parent_id | name |
|---|---|---|
| 1 | NULL | 食品 |
| 2 | 1 | 飲料 |
| 3 | 1 | 菓子 |
| 4 | 2 | コーヒー |
| 5 | 2 | ジュース |
| 6 | 3 | チョコレート |
| 7 | 10 | 雑貨(別ツリー) |
| category_id | parent_id | name | depth |
|---|---|---|---|
| 1 | NULL | 食品 | 0 |
| 2 | 1 | 飲料 | 1 |
| 3 | 1 | 菓子 | 1 |
| 4 | 2 | コーヒー | 2 |
| 5 | 2 | ジュース | 2 |
| 6 | 3 | チョコレート | 2 |