SQL 再帰CTE — RBAC・BOM展開・二重再帰の応用

応用WITH RECURSIVEWINDOW FUNCTIONグラフ探索二重再帰PostgreSQL/MySQL8対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 1

階層別給与ランキング — WITH RECURSIVE + RANK() ウィンドウ関数で深度内順位を算出する

WITH RECURSIVEWINDOW FUNCTION組織図RANK / PARTITION BY
前提知識

再帰CTEで各ノードに depth(階層深度)を付与しておけば、その結果を外側クエリでウィンドウ関数に渡すことができます。これにより「同じ階層レベル内でのランキング」や「階層ごとの集計」が一つのクエリで完結します。

WITH RECURSIVE emp_tree AS (
  -- アンカー: ルート社員(depth=0)
  SELECT emp_id, manager_id, salary, 0 AS depth FROM employees
  WHERE manager_id IS NULL
  UNION ALL
  -- 再帰: 部下を depth+1 で追加
  SELECT e.emp_id, e.manager_id, e.salary, et.depth + 1
  FROM employees e JOIN emp_tree et ON e.manager_id = et.emp_id
)
-- 外側クエリでウィンドウ関数を適用(再帰部内では使用不可)
SELECT emp_id, depth, salary,
  RANK() OVER (PARTITION BY depth ORDER BY salary DESC) AS rank_in_level
FROM emp_tree;
重要制約:ウィンドウ関数は再帰部内で使用できません。再帰CTEの SELECT 句に RANK() を書くと構文エラーになります。必ず 外側クエリで適用します。再帰CTEは「depth列の生成」に徹し、ウィンドウ計算は外に出す設計が基本パターンです。
問題

employees テーブルには社員・上司・給与が格納されています。各社員の組織上の深度(depth)と、同じ深度内での給与ランキング(rank_in_levelを求めてください。ランキングは RANK() を使い、深度内で給与が高い順(DESC)とします。取得列は emp_id, emp_name, depth, salary, rank_in_leveldepth 昇順 → rank_in_level 昇順でソートしてください。

使用テーブル
▸ employees
emp_idemp_namemanager_idsalary
1Alice(CEO)NULL1200000
2Bob1850000
3Carol1950000
4Dave2520000
5Eve2680000
6Frank3590000
7Grace3730000
入力データの関係と、再帰処理の停止条件を確認してください。
期待出力
emp_idemp_namedepthsalaryrank_in_level
1Alice(CEO)012000001
3Carol19500001
2Bob18500002
7Grace27300001
5Eve26800002
6Frank25900003
4Dave25200004
QUESTION 2

グラフ経路探索(配列で循環防止) — 訪問済み配列 visited で全経路を安全に列挙する

WITH RECURSIVEグラフ探索配列・ARRAY循環防止
前提知識

再帰CTEはツリー(木構造)だけでなく、循環(サイクル)を持つグラフ(有向グラフ)にも適用できます。ただし循環ルートがあると無限ループになるため、訪問済みノードを配列に記録し、同じノードを再訪しないよう制御する必要があります。

WITH RECURSIVE paths AS (
  -- アンカー: 出発地点, 訪問済み配列の初期化
  SELECT 'A' AS cur, 0 AS cost,
    ARRAY['A'] AS visited, 'A' AS route
  UNION ALL
  -- 再帰: 未訪問のノードへ移動
  SELECT r.to_code, p.cost + r.cost,
    p.visited || r.to_code,            -- || で配列に追加
    p.route || '→' || r.to_code
  FROM paths p JOIN routes r ON r.from_code = p.cur
  WHERE r.to_code != ALL(p.visited)   -- 循環防止
    AND array_length(p.visited, 1) < 4 -- 最大3フライト
)
配列による循環防止の仕組み:visited 列に通過した都市コードを配列で蓄積し、次の候補都市が != ALL(visited)(配列の全要素と異なる)ことを確認してから進みます。PostgreSQL では ARRAY['TYO'] || 'OSA' で配列に要素を追加できます。MySQL 8は配列型がないため、文字列 'TYO,OSA,' + FIND_IN_SET で代替します。
問題

航空路線ネットワーク(有向グラフ)があります。TYO から FUK への全経路を3フライト以内で列挙し、コストが安い順に出力してください。配列 visited を使って同一都市への再訪(循環)を防ぐこと。取得列は route_str, total_cost

使用テーブル
▸ routes(有向グラフ・一部は往復あり)
from_codeto_codecost
TYOOSA13000
TYONGO8000
NGOOSA6000
NGOFUK18000
OSAFUK15000
OSAHIR7000
HIRFUK8000
OSATYO13000
入力データの関係と、再帰処理の停止条件を確認してください。
期待出力
route_strtotal_cost
TYO→NGO→FUK26000
TYO→OSA→FUK28000
TYO→OSA→HIR→FUK28000
TYO→NGO→OSA→FUK29000
QUESTION 3

ロール継承によるパーミッション集計 — ボトムアップ再帰で RBAC の権限チェーンを実装する

WITH RECURSIVEボトムアップRBAC権限管理
前提知識

ロールベースアクセス制御(RBAC)では、ロールが階層構造(親ロールの権限を継承)を持つことがあります。例:viewer → editor → admin → super_admin。ユーザーのロールからボトムアップ再帰で全祖先ロールを辿り、各ロールに付与された権限を集約するパターンは、認証・認可システムの実装で頻出です。

WITH RECURSIVE role_chain AS (
  -- アンカー: 対象ユーザーの直接ロール(ボトムアップの起点)
  SELECT r.role_id, r.role_name, r.parent_role_id, 0 AS chain_level
  FROM users u JOIN roles r ON r.role_id = u.role_id
  WHERE u.user_id = :uid
  UNION ALL
  -- 再帰: parent_role_id を辿って上位ロールへ(ボトムアップ)
  SELECT r.role_id, r.role_name, r.parent_role_id, rc.chain_level + 1
  FROM roles r
  JOIN role_chain rc ON r.role_id = rc.parent_role_id  -- 親ロールへの逆JOIN
)
ボトムアップ基礎との違い:祖先を辿る方向は基礎編のカテゴリパンくずと同じですが、本問では複数のロールが同一の権限を持つ場合の重複除去と、どのロールで付与されたか(audit trail)の記録が追加のポイントになります。
問題

usersuser_id=1(alice) が持つ全パーミッションを、ロール継承チェーンを辿って取得してください。直接ロール・継承ロールの両方のパーミッションを列挙し、取得列は permission_name, granted_by_role, chain_levelchain_level 昇順 → permission_name 昇順でソートしてください。

使用テーブル
▸ roles
role_idrole_nameparent_role_id
1super_adminNULL
2admin1
3editor2
4viewer3
▸ permissions
role_idpermission_name
1system.config
1user.delete
2user.create
2user.edit
3content.edit
3content.publish
4content.read
▸ users
user_idusernamerole_id
1alice3
2bob4
3carol2
入力データの関係と、再帰処理の停止条件を確認してください。
期待出力
permission_namegranted_by_rolechain_level
content.editeditor0
content.publisheditor0
user.createadmin1
user.editadmin1
system.configsuper_admin2
user.deletesuper_admin2
QUESTION 4

BOM展開 × 原価積み上げ — 再帰展開と数量乗算で製品の総原価を算出する

WITH RECURSIVE数量乗算累積BOM/製造集計・GROUP BY
前提知識

製造業の「部品表(BOM: Bill of Materials)」は、製品→サブアセンブリ→部品という木構造です。各レベルで数量が乗算されて積み上がるため、単純な SUM ではなく再帰展開 × 数量乗算を組み合わせる必要があります。

WITH RECURSIVE bom_tree AS (
  -- アンカー: 起点となる製品や部品
  SELECT part_id, part_id AS root_id, 1 AS accumulated_qty
  FROM parts
  UNION ALL
  -- 再帰: 子部品へ展開し数量を乗算
  SELECT b.child_part_id, bt.root_id, bt.accumulated_qty * b.quantity
  FROM bom_tree bt
  JOIN bom b ON b.parent_part_id = bt.part_id
)

設計のポイント:全部品を起点(アンカー)として展開し、BOM 経由で子部品へ再帰します。数量は accumulated_qty * child_quantity で乗算しながら引き継ぎ、葉部品(unit_cost IS NOT NULL)の行だけをコスト計上して最後に集計します。

数量の累積:BOM展開では、親から子へと階層を下るたびに、親の累積数量 * 子の所要量 を計算して引き継ぎます。加算(+)ではなく乗算(*)である点に注意してください。
問題

以下の製品構成を前提に、全部品・サブアセンブリ・製品それぞれの総原価(total_component_cost)を再帰CTEで算出してください。

・集計対象は葉部品(unit_cost IS NOT NULL)のコストのみ。中間アセンブリは自身では原価を持ちません
・数量は親から子への経路全体を乗算して積み上げます(例:完成品Xのネジ使用数 = サブαのネジ数 × サブαの個数)
・出力は part_id, part_name, total_component_cost、part_id 昇順

使用テーブル
▸ parts(NULLはサブアセンブリ/製品)
part_idpart_nameunit_cost
1完成品XNULL
2サブASSY-αNULL
3サブASSY-βNULL
4ネジM35
5ボルトM812
6フレーム200
7基板150
▸ bom(parent→child×quantity)
parent_part_idchild_part_idquantity備考
122完成品X → サブα ×2
131完成品X → サブβ ×1
161完成品X → フレーム ×1
246サブα → ネジ ×6
252サブα → ボルト ×2
344サブβ → ネジ ×4
371サブβ → 基板 ×1
期待出力
part_idpart_nametotal_component_cost
1完成品X478
2サブASSY-α54
3サブASSY-β170
4ネジM35
5ボルトM812
6フレーム200
7基板150
QUESTION 5

部門予算の二重再帰集計 — dept_tree + closure の2つのCTEで配下予算を完全集計する

WITH RECURSIVE閉包テーブル組織/階層複数CTE連鎖
前提知識

複数の再帰CTEを1クエリ内で連鎖させる「二重再帰」パターンを学びます。再帰CTE 1本目でパス文字列・深度を付与した部門ツリー(dept_tree)を展開し、2本目で閉包テーブル(Closure Table)を生成します。

WITH RECURSIVE
  -- ① 1つ目の再帰CTE: 階層ツリーの展開
  tree_cte AS (
    SELECT id, parent_id, name FROM nodes WHERE parent_id IS NULL
    UNION ALL
    SELECT n.id, n.parent_id, n.name
    FROM nodes n JOIN tree_cte t ON n.parent_id = t.id
  ),
  -- ② 2つ目の再帰CTE: 閉包テーブルの生成
  closure_cte AS (
    SELECT id AS ancestor, id AS descendant FROM nodes
    UNION ALL
    SELECT c.ancestor, n.id AS descendant
    FROM closure_cte c JOIN nodes n ON n.parent_id = c.descendant
  )
-- 2つのCTEを組み合わせて集計等を行う
SELECT ...

閉包テーブルとは (ancestor_id, descendant_id) の全ペアを保持するテーブルで、「任意の部門の全配下」を O(1) の JOIN で取得できる設計パターンです。これを経由して各部門の own_budget を SUM することで、direct/indirect を含むtotal_budgetを一括算出します。

二重再帰(連鎖CTE):PostgreSQL や MySQL 8.0以降では、WITH RECURSIVE を一度宣言するだけで、カンマ区切りで複数の再帰CTEを定義できます。後続のCTEは先行するCTEを参照可能ですが、先行するCTEが後続を参照することはできません(前方参照不可)。
問題

以下の部門テーブルを使い、2つの再帰CTEを連鎖させて以下を出力してください。

・dept_tree CTE(再帰①): 深度(depth)とパス文字列(例:本社 > 技術部 > バックエンド)を付与して全部門を展開する
・closure CTE(再帰②): (ancestor_id, descendant_id) の全ペアを生成する閉包テーブルを構築する
・外側クエリで closure を介して各部門の total_budget(own_budget の配下合計)を SUM する
・出力: dept_id, dept_name, depth, path, own_budget, total_budget、depth 昇順 → dept_id 昇順

使用テーブル
▸ departments
dept_iddept_nameparent_idown_budget
1本社NULL5000
2技術部13000
3営業部12000
4バックエンド21500
5フロントエンド21200
6国内営業31800
7海外営業3900
期待出力
dept_iddept_namedepthpathown_budgettotal_budget
1本社0本社500015400
2技術部1本社 > 技術部30005700
3営業部1本社 > 営業部20004700
4バックエンド2本社 > 技術部 > バックエンド15001500
5フロントエンド2本社 > 技術部 > フロントエンド12001200
6国内営業2本社 > 営業部 > 国内営業18001800
7海外営業2本社 > 営業部 > 海外営業900900