← 中文资源

简体中文版本

递归 CTE

递归公共表表达式迭代一个工作集直至枯竭,用纯 SQL 表达树与图遍历——物料清单展开、组织架构、依赖链。

阅读英文原文

概念定义

有些查询天然是迭代式的:找某装配件下的全部零件、某经理下的全部下属、可达的全部节点。递归 CTE 给了 SQL 这个循环:种子行、扩展规则、以“迭代产出为空”终止。

工作机制

锚部产出种子集;递归项作用于上一轮输出,结果追加进工作表;递归项返回零行时停止。PostgreSQL 暴露搜索路径与环检测;深度跟踪靠在迭代中携带 level 或 path 列。

使用场景

模式中的树遍历、包与物料图的传递依赖、邻接表模型——应用侧递归会按层级往返数据库的场景。读远多于写时,物化路径或嵌套集是替代方案。

常见误区

UNION ALL 只是更快,从不去重:带环的图用它就是死循环,UNION 的去重才是终止保证。递归项每轮重新求值,其连接列必须有索引。极深的链条会撑大工作集;引擎有迭代上限,遍历中途撞上限是运维错误而非查询结果。

代码示例

-- BOM 展开:每层用量沿路径连乘
WITH RECURSIVE explosion AS (
  SELECT
    b.child_part_id,
    b.parent_part_id,
    b.quantity_per,
    b.quantity_per AS total_quantity,
    b.child_part_id::text AS path,
    1 AS level
  FROM bom_edges b
  WHERE b.parent_part_id = 'BIKE-100'  -- 顶层成品

  UNION ALL

  SELECT
    c.child_part_id,
    c.parent_part_id,
    c.quantity_per,
    e.total_quantity * c.quantity_per,
    e.path || ' > ' || c.child_part_id,
    e.level + 1
  FROM bom_edges c
  JOIN explosion e ON c.parent_part_id = e.child_part_id
  WHERE e.level < 10  -- 深度保险丝
)
SELECT
  child_part_id,
  level,
  total_quantity,
  path
FROM explosion
ORDER BY path;

-- 成本汇总:叶子节件数乘单价回卷到成品
WITH costs AS (
  SELECT
    child_part_id,
    SUM(total_quantity) AS qty_needed
  FROM explosion
  GROUP BY child_part_id
)
SELECT
  c.child_part_id,
  c.qty_needed,
  p.unit_cost_cents,
  c.qty_needed * p.unit_cost_cents AS extended_cost
FROM costs c
JOIN parts p ON p.part_id = c.child_part_id
ORDER BY extended_cost DESC;
-- 递归展开管理链:从 CEO 到每层汇报关系
WITH RECURSIVE org_tree AS (
  SELECT
    employee_id,
    manager_id,
    name,
    1 AS depth,
    ARRAY[employee_id] AS report_path,
    name::text AS chain
  FROM employees
  WHERE manager_id IS NULL  -- 锚点:顶层节点

  UNION ALL

  SELECT
    e.employee_id,
    e.manager_id,
    e.name,
    t.depth + 1,
    t.report_path || e.employee_id,
    t.chain || ' > ' || e.name
  FROM employees e
  JOIN org_tree t ON e.manager_id = t.employee_id
  WHERE NOT t.report_path @> ARRAY[e.employee_id]  -- 防环
)
SELECT
  name,
  depth,
  chain
FROM org_tree
ORDER BY report_path;

-- 反向查询:某人的全部下属(子树)
WITH RECURSIVE subtree AS (
  SELECT employee_id, name, 1 AS depth
  FROM employees WHERE employee_id = 7
  UNION ALL
  SELECT e.employee_id, e.name, s.depth + 1
  FROM employees e JOIN subtree s ON e.manager_id = s.employee_id
)
SELECT count(*) AS total_reports,
       max(depth) AS max_depth
FROM subtree;