PostgreSQL 中的层级查询 Oracle CONNECT BY 替代方案

PostgreSQL 中的层级查询:CONNECT BY 替代方案


一、关键说明

PostgreSQL 不支持 Oracle 的 CONNECT BY 语法

层级递归查询在 PostgreSQL 中通过 WITH RECURSIVE(递归 CTE) 实现,这也是 SQL 标准(SQL:1999)的写法,被 MySQL 8.0+、SQL Server、DB2、Oracle 12c+ 等所有主流数据库支持。


二、Oracle CONNECT BY 与 PG WITH RECURSIVE 对照

Oracle 写法

sql 复制代码
SELECT employee_id, name, manager_id, LEVEL
FROM employees
START WITH manager_id IS NULL
CONNECT BY PRIOR employee_id = manager_id
ORDER SIBLINGS BY name;

PostgreSQL 等价写法

sql 复制代码
WITH RECURSIVE emp_tree AS (
    -- 锚定成员(START WITH 部分)
    SELECT employee_id, name, manager_id, 1 AS level
    FROM employees
    WHERE manager_id IS NULL

    UNION ALL

    -- 递归成员(CONNECT BY 部分)
    SELECT e.employee_id, e.name, e.manager_id, t.level + 1
    FROM employees e
    JOIN emp_tree t ON e.manager_id = t.employee_id
)
SELECT * FROM emp_tree
ORDER BY level, name;

三、WITH RECURSIVE 语法结构

sql 复制代码
WITH RECURSIVE 别名 AS (
    -- 1. 锚定查询(初始集合,相当于 START WITH)
    SELECT ... FROM ... WHERE 起点条件

    UNION ALL    -- 或 UNION

    -- 2. 递归查询(相当于 CONNECT BY)
    SELECT ... FROM 表 JOIN 别名 ON 关联条件
    WHERE 终止条件
)
SELECT ... FROM 别名;

关键点

  1. 必须有 RECURSIVE 关键字
  2. 锚定查询和递归查询用 UNION ALL 连接
  3. 递归查询中必须引用 CTE 自己(这就是递归点)
  4. 必须有终止条件,否则死循环

四、实战案例

准备测试数据

sql 复制代码
CREATE TABLE employees (
    id          INT PRIMARY KEY,
    name        VARCHAR(50),
    manager_id  INT,
    salary      NUMERIC(10,2),
    department  VARCHAR(50)
);

INSERT INTO employees VALUES
(1,  'CEO张总',     NULL, 50000, '总裁办'),
(2,  'CTO李总',     1,    40000, '技术部'),
(3,  'CFO王总',     1,    40000, '财务部'),
(4,  '研发总监刘',  2,    30000, '技术部'),
(5,  '运维总监陈',  2,    30000, '技术部'),
(6,  '工程师赵',    4,    20000, '技术部'),
(7,  '工程师钱',    4,    20000, '技术部'),
(8,  '工程师孙',    5,    18000, '技术部'),
(9,  '会计周',      3,    15000, '财务部');

数据形成的层级:

复制代码
CEO张总
├── CTO李总
│   ├── 研发总监刘
│   │   ├── 工程师赵
│   │   └── 工程师钱
│   └── 运维总监陈
│       └── 工程师孙
└── CFO王总
    └── 会计周

案例 1:自顶向下查询(树形展示)⭐

sql 复制代码
WITH RECURSIVE org_tree AS (
    -- 锚定:从 CEO 开始
    SELECT id, name, manager_id, 1 AS level,
           name::TEXT AS path
    FROM employees
    WHERE manager_id IS NULL

    UNION ALL

    -- 递归:找下属
    SELECT e.id, e.name, e.manager_id, t.level + 1,
           t.path || ' -> ' || e.name
    FROM employees e
    JOIN org_tree t ON e.manager_id = t.id
)
SELECT
    LPAD(' ', (level-1)*4) || name AS hierarchy,
    level,
    path
FROM org_tree
ORDER BY path;

输出

复制代码
hierarchy                    | level | path
-----------------------------+-------+---------------------------------------------
CEO张总                      |     1 | CEO张总
    CFO王总                  |     2 | CEO张总 -> CFO王总
        会计周               |     3 | CEO张总 -> CFO王总 -> 会计周
    CTO李总                  |     2 | CEO张总 -> CTO李总
        研发总监刘           |     3 | CEO张总 -> CTO李总 -> 研发总监刘
            工程师赵         |     4 | CEO张总 -> CTO李总 -> 研发总监刘 -> 工程师赵
            工程师钱         |     4 | CEO张总 -> CTO李总 -> 研发总监刘 -> 工程师钱
        运维总监陈           |     3 | CEO张总 -> CTO李总 -> 运维总监陈
            工程师孙         |     4 | CEO张总 -> CTO李总 -> 运维总监陈 -> 工程师孙

案例 2:自底向上查询(找所有上级)

需求:查询"工程师赵"的所有上级链路

sql 复制代码
WITH RECURSIVE manager_chain AS (
    -- 锚定:从赵开始
    SELECT id, name, manager_id, 1 AS level
    FROM employees
    WHERE name = '工程师赵'

    UNION ALL

    -- 递归:向上找经理
    SELECT e.id, e.name, e.manager_id, m.level + 1
    FROM employees e
    JOIN manager_chain m ON e.id = m.manager_id
)
SELECT * FROM manager_chain ORDER BY level;

输出

复制代码
id | name        | manager_id | level
---+-------------+------------+------
6  | 工程师赵    | 4          | 1
4  | 研发总监刘  | 2          | 2
2  | CTO李总     | 1          | 3
1  | CEO张总     | NULL       | 4

案例 3:查询某节点下的所有后代

需求:查询 "CTO李总" 下面所有员工

sql 复制代码
WITH RECURSIVE subordinates AS (
    SELECT id, name, manager_id, 1 AS level
    FROM employees
    WHERE name = 'CTO李总'

    UNION ALL

    SELECT e.id, e.name, e.manager_id, s.level + 1
    FROM employees e
    JOIN subordinates s ON e.manager_id = s.id
)
SELECT * FROM subordinates WHERE level > 1   -- 排除起始节点
ORDER BY level, id;

案例 4:计算每个节点的子树员工数 + 总薪资

sql 复制代码
WITH RECURSIVE subtree AS (
    -- 每个员工作为根
    SELECT id AS root_id, id, name, salary
    FROM employees

    UNION ALL

    -- 递归找该员工下属
    SELECT s.root_id, e.id, e.name, e.salary
    FROM employees e
    JOIN subtree s ON e.manager_id = s.id
)
SELECT
    e.id,
    e.name,
    COUNT(*) - 1                    AS subordinate_count,   -- 减去自己
    SUM(s.salary)                   AS total_salary
FROM employees e
JOIN subtree s ON s.root_id = e.id
GROUP BY e.id, e.name
ORDER BY total_salary DESC;

案例 5:限制递归深度(防止过深)

sql 复制代码
WITH RECURSIVE org_tree AS (
    SELECT id, name, manager_id, 1 AS level
    FROM employees
    WHERE manager_id IS NULL

    UNION ALL

    SELECT e.id, e.name, e.manager_id, t.level + 1
    FROM employees e
    JOIN org_tree t ON e.manager_id = t.id
    WHERE t.level < 3            -- 只递归到第 3 层
)
SELECT * FROM org_tree;

案例 6:检测并防止循环(关键!)

如果数据中存在循环引用(A 的经理是 B,B 的经理是 A),上面的递归会死循环。需要主动检测:

sql 复制代码
WITH RECURSIVE org_tree AS (
    SELECT id, name, manager_id, 1 AS level,
           ARRAY[id] AS visited            -- 用数组记录已访问节点
    FROM employees
    WHERE manager_id IS NULL

    UNION ALL

    SELECT e.id, e.name, e.manager_id, t.level + 1,
           t.visited || e.id
    FROM employees e
    JOIN org_tree t ON e.manager_id = t.id
    WHERE NOT (e.id = ANY(t.visited))      -- 已访问过则停止
)
SELECT * FROM org_tree;

案例 7:PG 14+ 内置循环检测语法 ⭐

PostgreSQL 14 开始支持 SEARCHCYCLE 子句,更优雅:

sql 复制代码
WITH RECURSIVE org_tree AS (
    SELECT id, name, manager_id
    FROM employees
    WHERE manager_id IS NULL

    UNION ALL

    SELECT e.id, e.name, e.manager_id
    FROM employees e
    JOIN org_tree t ON e.manager_id = t.id
)
SEARCH DEPTH FIRST BY id SET ordercol           -- 深度优先,生成排序列
CYCLE id SET is_cycle USING path                 -- 自动检测循环
SELECT * FROM org_tree
ORDER BY ordercol;

输出会自动包含:

  • ordercol:层次遍历的排序键
  • is_cycle:是否检测到循环
  • path:访问路径数组

五、Oracle 伪列对应关系

Oracle CONNECT BY PostgreSQL 等价实现
LEVEL 递归中手动维护 level + 1
CONNECT_BY_ROOT col 递归时携带 root_col 字段
SYS_CONNECT_BY_PATH(col, '/') 拼接 `path
CONNECT_BY_ISLEAF 通过 NOT EXISTS 判断
CONNECT_BY_ISCYCLE PG 14+ 用 CYCLE 子句
ORDER SIBLINGS BY PG 14+ 用 SEARCH 子句

等价改写示例

sql 复制代码
-- Oracle
SELECT name, LEVEL,
       CONNECT_BY_ROOT name AS root_name,
       SYS_CONNECT_BY_PATH(name, '/') AS path
FROM employees
START WITH manager_id IS NULL
CONNECT BY PRIOR id = manager_id;

-- PostgreSQL 等价
WITH RECURSIVE tree AS (
    SELECT id, name, manager_id, 1 AS level,
           name AS root_name,
           '/' || name AS path
    FROM employees
    WHERE manager_id IS NULL

    UNION ALL

    SELECT e.id, e.name, e.manager_id, t.level + 1,
           t.root_name,                             -- ROOT 透传
           t.path || '/' || e.name                  -- PATH 累加
    FROM employees e
    JOIN tree t ON e.manager_id = t.id
)
SELECT name, level, root_name, path FROM tree;

六、判断叶子节点(CONNECT_BY_ISLEAF)

sql 复制代码
WITH RECURSIVE tree AS (
    SELECT id, name, manager_id, 1 AS level
    FROM employees WHERE manager_id IS NULL

    UNION ALL

    SELECT e.id, e.name, e.manager_id, t.level + 1
    FROM employees e
    JOIN tree t ON e.manager_id = t.id
)
SELECT
    t.*,
    CASE WHEN EXISTS (SELECT 1 FROM employees WHERE manager_id = t.id)
         THEN 0 ELSE 1
    END AS is_leaf
FROM tree t;

七、其他常见层级查询场景

场景 1:物料清单(BOM 展开)

sql 复制代码
-- 表结构: parts(id, name), bom(parent_id, child_id, qty)
WITH RECURSIVE bom_tree AS (
    SELECT b.parent_id, b.child_id, p.name, b.qty,
           1 AS level, b.qty AS total_qty
    FROM bom b
    JOIN parts p ON b.child_id = p.id
    WHERE b.parent_id = 100   -- 起始产品

    UNION ALL

    SELECT b.parent_id, b.child_id, p.name, b.qty,
           t.level + 1, t.total_qty * b.qty       -- 累计数量
    FROM bom b
    JOIN parts p ON b.child_id = p.id
    JOIN bom_tree t ON b.parent_id = t.child_id
)
SELECT * FROM bom_tree ORDER BY level;

场景 2:地区行政区划(省→市→区→街道)

sql 复制代码
WITH RECURSIVE region_tree AS (
    SELECT id, name, parent_id, 1 AS level, name::TEXT AS full_path
    FROM regions WHERE parent_id IS NULL    -- 国家级

    UNION ALL

    SELECT r.id, r.name, r.parent_id, t.level + 1,
           t.full_path || '/' || r.name
    FROM regions r
    JOIN region_tree t ON r.parent_id = t.id
)
SELECT id, full_path, level FROM region_tree
WHERE level = 4;                            -- 只看街道级

场景 3:评论树(多级回复)

sql 复制代码
WITH RECURSIVE comment_tree AS (
    SELECT id, content, parent_id, user_id, 1 AS depth,
           ARRAY[id] AS sort_path
    FROM comments WHERE post_id = 100 AND parent_id IS NULL

    UNION ALL

    SELECT c.id, c.content, c.parent_id, c.user_id, t.depth + 1,
           t.sort_path || c.id
    FROM comments c
    JOIN comment_tree t ON c.parent_id = t.id
)
SELECT REPEAT('  ', depth-1) || content AS display, depth
FROM comment_tree
ORDER BY sort_path;       -- 按层级遍历顺序展示

场景 4:图遍历(最短路径示意)

sql 复制代码
-- 找到从节点 A 到节点 B 的所有路径
WITH RECURSIVE paths AS (
    SELECT from_node, to_node, ARRAY[from_node] AS path, 1 AS hops
    FROM edges WHERE from_node = 'A'

    UNION ALL

    SELECT p.from_node, e.to_node, p.path || e.to_node, p.hops + 1
    FROM edges e
    JOIN paths p ON e.from_node = p.to_node
    WHERE NOT (e.to_node = ANY(p.path))      -- 防止环
      AND p.hops < 10                         -- 限制最大跳数
)
SELECT * FROM paths
WHERE to_node = 'B'
ORDER BY hops LIMIT 1;                        -- 最短路径

八、性能优化建议

1. 给递归字段建索引

sql 复制代码
-- 自顶向下:索引 manager_id
CREATE INDEX idx_emp_manager ON employees(manager_id);

-- 自底向上:索引主键已自带

2. 限制递归深度

sql 复制代码
WHERE t.level < 10        -- 防止数据问题导致深度爆炸

3. 在递归内尽早过滤

sql 复制代码
-- ❌ 慢:把所有数据递归出来再过滤
WITH RECURSIVE tree AS (...)
SELECT * FROM tree WHERE department = '技术部';

-- ✅ 快:在递归过程中就过滤
WITH RECURSIVE tree AS (
    SELECT ... FROM employees
    WHERE manager_id IS NULL AND department = '技术部'

    UNION ALL

    SELECT ... FROM employees e JOIN tree t ON ...
    WHERE e.department = '技术部'
)
SELECT * FROM tree;

4. 用 EXPLAIN 验证

sql 复制代码
EXPLAIN ANALYZE
WITH RECURSIVE tree AS (...)
SELECT * FROM tree;

期望看到 WorkTable ScanRecursive Union,确认走的是递归。


九、PG 与 Oracle 层级查询对比总结

特性 Oracle CONNECT BY PostgreSQL WITH RECURSIVE
语法简洁性 ⭐⭐⭐⭐⭐ 最简洁 ⭐⭐⭐ 较冗长
灵活性 ⭐⭐⭐ 局限于树形 ⭐⭐⭐⭐⭐ 可处理图、复杂逻辑
标准化 Oracle 私有 SQL 标准(SQL:1999)
跨库迁移 容易(MySQL/PG/SQL Server 通用)
循环检测 NOCYCLE 关键字 PG 14+ CYCLE 子句
排序 ORDER SIBLINGS BY PG 14+ SEARCH 子句
性能 优化器特化处理 取决于索引和写法

一句话总结

PostgreSQL 没有 CONNECT BY,但 WITH RECURSIVE 完全能实现并超越它的能力。 核心结构是"锚定查询 + UNION ALL + 递归查询"。自顶向下找下属用 JOIN ON e.manager_id = t.id,自底向上找上级用 JOIN ON e.id = t.manager_id 。PG 14+ 还能用 SEARCHCYCLE 子句优雅处理排序和循环检测。这个语法是 SQL 标准,写一次走遍 PG / MySQL / SQL Server / Oracle 12c+。

如果你有具体的层级数据场景(组织架构、菜单树、BOM 等),可以贴出表结构,我帮你写出最优的 WITH RECURSIVE 查询。

相关推荐
万事大吉CC2 小时前
【3】深入剖析 Django 之 MTV:路径引用与资源加载机制
数据库·django·sqlite
Hical_W2 小时前
用 Hical + MySQL 5 分钟搭建 CRUD API(C++20 协程版)
数据库·mysql·c++20
AIMath~2 小时前
agent上下文和模型的上下文区别
数据库
与遨游于天地2 小时前
分布式锁从Redis到Redisson的演进
数据库·redis·分布式
山峰哥3 小时前
SQL性能提升20倍的秘密:这些优化技巧让DBA都惊叹
开发语言·数据库·sql·编辑器·深度优先·宽度优先
HuDie3403 小时前
prompt模版
数据库·prompt
梦想画家4 小时前
PostgreSQL 图计算双雄:Apache AGE 与 pgGraphBLAS 的融合实战指南
数据库·postgresql·图算法
逻辑驱动的ken4 小时前
Java高频面试考点场景题23
java·开发语言·数据库·面试·职场和发展·哈希算法