Level-Based Salary Ranking — Calculate Within-Depth Ranks with WITH RECURSIVE + RANK()
Once a recursive CTE assigns each node a depth, the result can be passed to a window function in the outer query. This lets you complete “ranking within the same hierarchy level” and “aggregation by hierarchy level” in a single query.
WITH RECURSIVE emp_tree AS ( -- Anchor: root employee (depth=0) SELECT emp_id, manager_id, salary, 0 AS depth FROM employees WHERE manager_id IS NULL UNION ALL -- Recursive member: add subordinates at 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 ) -- Apply the window function in the outer query (not inside the recursive member) SELECT emp_id, depth, salary, RANK() OVER (PARTITION BY depth ORDER BY salary DESC) AS rank_in_level FROM emp_tree;
RANK() in the recursive CTE's SELECT list causes a syntax error. Always apply it in the outer query. The recursive CTE should focus on “generating the depth column,” leaving the window calculation outside.The employees table stores employees, managers, and salaries. Retrieve each employee's organizational depth (depth) and salary ranking within the same depth (rank_in_level). Use RANK() and rank salaries from highest to lowest (DESC) within each depth. Return emp_id, emp_name, depth, salary, rank_in_level, sorted by depth ascending and then rank_in_level ascending.
| emp_id | emp_name | manager_id | salary |
|---|---|---|---|
| 1 | Alice (CEO) | NULL | 1200000 |
| 2 | Bob | 1 | 850000 |
| 3 | Carol | 1 | 950000 |
| 4 | Dave | 2 | 520000 |
| 5 | Eve | 2 | 680000 |
| 6 | Frank | 3 | 590000 |
| 7 | Grace | 3 | 730000 |
Within depth=2, Grace(730000) has the highest salary → rank_in_level=1
| emp_id | emp_name | depth | salary | rank_in_level |
|---|---|---|---|---|
| 1 | Alice (CEO) | 0 | 1200000 | 1 |
| 3 | Carol | 1 | 950000 | 1 |
| 2 | Bob | 1 | 850000 | 2 |
| 7 | Grace | 2 | 730000 | 1 |
| 5 | Eve | 2 | 680000 | 2 |
| 6 | Frank | 2 | 590000 | 3 |
| 4 | Dave | 2 | 520000 | 4 |
Graph Path Exploration (Cycle Prevention with an Array) — Safely Enumerate All Routes with a Visited Array
Recursive CTEs apply not only to trees but also to graphs (directed graphs) that contain cycles. A cycle can cause an infinite loop, so record visited nodes in an array and prevent the recursion from visiting the same node again.
WITH RECURSIVE paths AS ( -- Anchor: starting point and initialization of the visited array SELECT 'A' AS cur, 0 AS cost, ARRAY['A'] AS visited, 'A' AS route UNION ALL -- Recursive member: move to an unvisited node SELECT r.to_code, p.cost + r.cost, p.visited || r.to_code, -- append to the array with || p.route || '→' || r.to_code FROM paths p JOIN routes r ON r.from_code = p.cur WHERE r.to_code != ALL(p.visited) -- prevent cycles AND array_length(p.visited, 1) < 4 -- maximum 3 flights )
visited column. Proceed only after confirming that the next candidate city is different from every array element with != ALL(visited). In PostgreSQL, ARRAY['TYO'] || 'OSA' appends an element to an array. MySQL 8 has no array type, so use the string 'TYO,OSA,' with FIND_IN_SET instead.Consider a directed airline-route network. Enumerate all routes from TYO to FUK using at most 3 flights, ordered by increasing cost. Use the visited array to prevent revisiting the same city (cycles). Return route_str, total_cost.
| from_code | to_code | cost |
|---|---|---|
| TYO | OSA | 13000 |
| TYO | NGO | 8000 |
| NGO | OSA | 6000 |
| NGO | FUK | 18000 |
| OSA | FUK | 15000 |
| OSA | HIR | 7000 |
| HIR | FUK | 8000 |
| OSA | TYO | 13000 |
| route_str | total_cost |
|---|---|
| TYO→NGO→FUK | 26000 |
| TYO→OSA→FUK | 28000 |
| TYO→OSA→HIR→FUK | 28000 |
| TYO→NGO→OSA→FUK | 29000 |
Permission Aggregation through Role Inheritance — Implement an RBAC Permission Chain with Bottom-Up Recursion
In role-based access control (RBAC), roles can form a hierarchy in which a role inherits permissions from its parent, for example viewer → editor → admin → super_admin. Tracing every ancestor role bottom-up from a user's role and aggregating the permissions assigned to each role is a common pattern in authentication and authorization systems.
WITH RECURSIVE role_chain AS ( -- Anchor: the target user's direct role (bottom-up starting point) 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 -- Recursive member: follow parent_role_id upward (bottom-up) 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 -- reverse join to the parent role )
Retrieve all permissions held by user_id=1 (alice) by following the role-inheritance chain. List permissions from both the direct and inherited roles. Return permission_name, granted_by_role, chain_level, sorted by chain_level ascending and then permission_name ascending.
| role_id | role_name | parent_role_id |
|---|---|---|
| 1 | super_admin | NULL |
| 2 | admin | 1 |
| 3 | editor | 2 |
| 4 | viewer | 3 |
| role_id | permission_name |
|---|---|
| 1 | system.config |
| 1 | user.delete |
| 2 | user.create |
| 2 | user.edit |
| 3 | content.edit |
| 3 | content.publish |
| 4 | content.read |
| user_id | username | role_id |
|---|---|---|
| 1 | alice | 3 |
| 2 | bob | 4 |
| 3 | carol | 2 |
editor does not inherit viewer(4)'s permissions (inheritance moves upward only)
chain_level=0: alice's direct role (editor); chain_level=1: admin; chain_level=2: super_admin
| permission_name | granted_by_role | chain_level |
|---|---|---|
| content.edit | editor | 0 |
| content.publish | editor | 0 |
| user.create | admin | 1 |
| user.edit | admin | 1 |
| system.config | super_admin | 2 |
| user.delete | super_admin | 2 |
BOM Expansion × Cost Rollup — Calculate Total Product Cost with Recursive Expansion and Quantity Multiplication
A manufacturing bill of materials (BOM) is a tree structure of product → subassembly → component. Because quantities multiply as you move through each level, you must combine recursive expansion with quantity multiplication rather than using a simple SUM.
WITH RECURSIVE bom_tree AS ( -- Anchor: the starting products and components SELECT part_id, part_id AS root_id, 1 AS accumulated_qty FROM parts UNION ALL -- Recursive member: expand to child components and multiply the quantity 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 )
Design point: start every part at the anchor and recursively expand to child parts through the BOM. Carry quantity with accumulated_qty * child_quantity, count only leaf-part rows (unit_cost IS NOT NULL), and aggregate them at the end.
parent accumulated quantity * child required quantity. Use multiplication (*), not addition (+).Using the product structure below, calculate total_component_cost for each part, subassembly, and finished product with a recursive CTE.
・Only leaf components (unit_cost IS NOT NULL) contribute cost; intermediate assemblies do not have their own cost
・Multiply quantities across the entire parent-to-child path (for example, the number of screws in Finished Product X = screws per Sub α × number of Sub α units)
・Return part_id, part_name, total_component_cost, sorted by part_id ascending
| part_id | part_name | unit_cost |
|---|---|---|
| 1 | Finished Product X | NULL |
| 2 | SubASSY-α | NULL |
| 3 | SubASSY-β | NULL |
| 4 | M3 Screw | 5 |
| 5 | M8 Bolt | 12 |
| 6 | Frame | 200 |
| 7 | Circuit Board | 150 |
| parent_part_id | child_part_id | quantity | Notes |
|---|---|---|---|
| 1 | 2 | 2 | Finished X → Sub α ×2 |
| 1 | 3 | 1 | Finished X → Sub β ×1 |
| 1 | 6 | 1 | Finished X → Frame ×1 |
| 2 | 4 | 6 | Sub α → Screw ×6 |
| 2 | 5 | 2 | Sub α → Bolt ×2 |
| 3 | 4 | 4 | Sub β → Screw ×4 |
| 3 | 7 | 1 | Sub β → Board ×1 |
| part_id | part_name | total_component_cost |
|---|---|---|
| 1 | Finished Product X | 478 |
| 2 | SubASSY-α | 54 |
| 3 | SubASSY-β | 170 |
| 4 | M3 Screw | 5 |
| 5 | M8 Bolt | 12 |
| 6 | Frame | 200 |
| 7 | Circuit Board | 150 |
Double-Recursive Department Budget Aggregation — Aggregate All Descendant Budgets with dept_tree + closure CTEs
This question demonstrates the “double recursion” pattern: chain multiple recursive CTEs in one query. The first recursive CTE expands a department tree (dept_tree) with path strings and depth; the second generates a Closure Table.
WITH RECURSIVE -- ① First recursive CTE: expand the hierarchy tree 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 ), -- ② Second recursive CTE: generate the closure table 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 ) -- Combine the two CTEs for aggregation and other operations SELECT ...
A Closure Table stores every (ancestor_id, descendant_id) pair, allowing all descendants of any department to be retrieved with an O(1) JOIN. Summing each department's own_budget through this table produces total_budget, including both direct and indirect descendants.
WITH RECURSIVE once, separated by commas. A later CTE may reference an earlier CTE, but an earlier CTE cannot reference a later one (no forward references).Using the department table below, chain two recursive CTEs and output the following:
・dept_tree (recursive CTE 1): expand every department while adding depth and a path string (for example, HQ > Engineering > Backend)
・closure (recursive CTE 2): build a Closure Table containing every (ancestor_id, descendant_id) pair
・In the outer query, use closure to SUM each department's descendant own_budget
・Return dept_id, dept_name, depth, path, own_budget, total_budget, sorted by depth ascending and then dept_id ascending
| dept_id | dept_name | parent_id | own_budget |
|---|---|---|---|
| 1 | HQ | NULL | 5000 |
| 2 | Engineering | 1 | 3000 |
| 3 | Sales | 1 | 2000 |
| 4 | Backend | 2 | 1500 |
| 5 | Frontend | 2 | 1200 |
| 6 | Domestic Sales | 3 | 1800 |
| 7 | International Sales | 3 | 900 |
| dept_id | dept_name | depth | path | own_budget | total_budget |
|---|---|---|---|---|---|
| 1 | HQ | 0 | HQ | 5000 | 15400 |
| 2 | Engineering | 1 | HQ > Engineering | 3000 | 5700 |
| 3 | Sales | 1 | HQ > Sales | 2000 | 4700 |
| 4 | Backend | 2 | HQ > Engineering > Backend | 1500 | 1500 |
| 5 | Frontend | 2 | HQ > Engineering > Frontend | 1200 | 1200 |
| 6 | Domestic Sales | 2 | HQ > Sales > Domestic Sales | 1800 | 1800 |
| 7 | International Sales | 2 | HQ > Sales > International Sales | 900 | 900 |