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
Expected output (depth ascending → rank_in_level ascending):
| 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 |
Within depth=2, salaries rank from 1 to 4. If salaries are tied, RANK() skips the next rank (for example, a tie at rank 2 is followed by rank 4).
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
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 |
Expected output (total_cost ascending):
| route_str | total_cost |
|---|---|
| TYO→NGO→FUK | 26000 |
| TYO→OSA→FUK | 28000 |
| TYO→OSA→HIR→FUK | 28000 |
| TYO→NGO→OSA→FUK | 29000 |
The reverse OSA→TYO route and routes with 4 or more flights are excluded. Ties at 28000 may appear in any order, such as route_str order.
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
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
Expected output (chain_level ascending → permission_name ascending):
| 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 |
alice (editor) does not have the permissions of viewer (the child of the chain_level=0 role). Inheritance moves upward to parent roles only.
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
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 )
parent accumulated quantity * child required quantity. Use multiplication (*), not addition (+).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.
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 |
Expected output (part_id ascending):
| 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 |
Finished Product X = Frame(200) + Sub α×2 (12 screws=60, 4 bolts=48) + Sub β×1 (4 screws=20, board=150) = 478
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
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 ...
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).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.
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 |
Expected output (depth ascending → dept_id ascending):
| 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 |
HQ = 5000+3000+2000+1500+1200+1800+900 = 15400 / Engineering = 3000+1500+1200 = 5700
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free