SQL バッチ処理 — 相関サブクエリ・LAG/LEADの応用

応用バッチ処理相関サブクエリ・CTE条件付き集計LAG / LEAD再帰CTEPostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 1

相関サブクエリ — 各ユーザーの「最新注文」だけを取得する

相関サブクエリMAX最新レコード注文API
前提知識

相関サブクエリ(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 昇順で並べてください。

使用テーブル
▸ orders
order_iduser_idamountordered_at
1001U0132002024-03-10
1002U0115002024-05-20
1003U0280002024-04-01
1004U0221002024-06-15
1005U035002024-02-28
1006U0147002024-06-01
期待出力
order_iduser_idamountordered_at
1006U0147002024-06-01
1004U0221002024-06-15
1005U035002024-02-28
QUESTION 2

WITH句(CTE) — 月次売上レポートを段階的に組み立てる

WITH / CTESUM / AVG売上レポートバッチ集計
前提知識

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段階で組み立てること。結果は月・カテゴリ昇順で並べてください。

使用テーブル
▸ sales
sale_idcategoryamountsold_at
1food30002024-05-03
2drink15002024-05-10
3food20002024-05-20
4drink25002024-06-05
5food40002024-06-12
6drink10002024-06-20
期待出力
monthcategorycat_totalmonth_totalratio_pct
2024-05drink1500650023.08
2024-05food5000650076.92
2024-06drink3500750046.67
2024-06food4000750053.33
QUESTION 3

CASE WHEN + 条件付き集計 — 在庫状況のピボット集計と FILTER 句

CASE WHENSUM / FILTERピボット集計在庫API
前提知識

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
ピボット集計とは:行方向のデータを列方向に展開して、1行で複数のカテゴリの集計値を並べて見せること。Excelのピボットテーブルをイメージしてください。「行をカテゴリ別に分けてGROUP BY」ではなく「1行に複数カテゴリの合計を横並び」にする点が特徴です。
問題

inventory テーブルから、各倉庫(warehouse)ごとに「在庫あり(quantity > 0)の商品数」「在庫切れ(quantity = 0)の商品数」「全商品数」を1行にまとめたピボット集計を取得してください。

使用テーブル
▸ inventory
item_idwarehousequantity
A01東京50
A02東京0
A03東京20
B01大阪0
B02大阪0
B03大阪15
C01福岡100
C02福岡30
期待出力
warehousein_stockout_of_stocktotal_items
東京213
大阪123
福岡202
QUESTION 4

LAG / LEAD — 前月比・前行比の計算でトレンドを可視化する

LAG / LEADOVER PARTITION BY前月比売上分析
前提知識

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
)
ORDER BY は必須:ウィンドウ関数で LAG/LEAD を使うときは OVER 句内の ORDER BY が必須です。順序が定まらないと「前の行」が何なのか決まりません。
問題

monthly_sales テーブルから、各商品(product_id)の月別売上と前月売上・前月比(%)を取得してください。前月データがない場合(最初の月)は前月売上は NULL、前月比は NULL で構いません。結果は product_id・month の昇順で並べてください。

使用テーブル
▸ monthly_sales
product_idmonthrevenue
P012024-0310000
P012024-0412000
P012024-059000
P022024-035000
P022024-047500
P022024-057500
期待出力
product_idmonthrevenueprev_revenuemom_pct
P012024-0310000NULLNULL
P012024-041200010000+20.00
P012024-05900012000-25.00
P022024-035000NULLNULL
P022024-0475005000+50.00
P022024-05750075000.00
QUESTION 5

再帰CTE(WITH RECURSIVE) — カテゴリ階層の全子孫を取得する

WITH RECURSIVEUNION ALL階層データカテゴリAPI
前提知識

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;
無限ループ注意:データに循環参照(A→B→A)があると再帰が止まらなくなります。depth < 10 などの上限条件か、path 列を使ったループ検知を必ず入れましょう。
問題

categories テーブルは parent_id で自己参照する階層構造です。category_id = 1 を起点として、その全ての子孫カテゴリ(直接の子だけでなく孫・曾孫も含む)を depth(階層深さ)付きで取得してください。深さは起点を 0 とします。

使用テーブル
▸ categories
category_idparent_idname
1NULL食品
21飲料
31菓子
42コーヒー
52ジュース
63チョコレート
710雑貨(別ツリー)
期待出力
category_idparent_idnamedepth
1NULL食品0
21飲料1
31菓子1
42コーヒー2
52ジュース2
63チョコレート2