Oracle 层次查询(CONNECT BY)完全指南:从入门到精通

树状结构数据怎么查?一文掌握 Oracle 最强大的递归查询利器

📌 前言

在日常数据库开发中,我们经常遇到树状结构 的数据------组织架构、菜单树、物料清单(BOM)、地区层级等。这类数据通常使用一张自关联表存储(如员工表中通过 manager_id 指向上级)。那么问题来了:如何高效地查询某个节点下的所有子孙?又如何反向查找所有祖先?

Oracle 数据库提供了独有的 CONNECT BY 层次查询语法,可以轻松实现上述需求。本文将通过完整的可运行代码 + 逐段原理解析,带你彻底掌握 Oracle 层次查询的使用方法。

✅ 文末附有所有示例的 SQL 脚本,可直接复制到 Oracle 数据库中运行验证。


一、准备工作:建表并插入测试数据

我们先创建一张经典的 emp(员工)表,并插入一个三层树结构的数据。

sql 复制代码
-- 清理环境(测试用,生产环境慎用)
DROP TABLE emp PURGE;

-- 创建员工表
CREATE TABLE emp (
    emp_id     NUMBER PRIMARY KEY,
    emp_name   VARCHAR2(50),
    mgr_id     NUMBER,   -- 上级经理的 emp_id
    CONSTRAINT emp_mgr_fk FOREIGN KEY (mgr_id) REFERENCES emp(emp_id)
);

-- 插入数据(形成树状结构)
INSERT INTO emp VALUES (100, '大老板', NULL);  -- 根节点
INSERT INTO emp VALUES (101, '经理A', 100);
INSERT INTO emp VALUES (102, '经理B', 100);
INSERT INTO emp VALUES (201, '员工A1', 101);
INSERT INTO emp VALUES (202, '员工A2', 101);
INSERT INTO emp VALUES (203, '员工B1', 102);

COMMIT;

-- 查看原始数据
SELECT * FROM emp ORDER BY emp_id;

此时的数据树形结构如下:

复制代码
大老板 (100)
├── 经理A (101)
│   ├── 员工A1 (201)
│   └── 员工A2 (202)
└── 经理B (102)
    └── 员工B1 (203)

二、核心语法与基础查询

2.1 基本语法结构

sql 复制代码
SELECT 列, LEVEL, CONNECT_BY_ISLEAF, ...
FROM 表
START WITH 起始条件          -- 指定根节点
CONNECT BY [NOCYCLE] 连接条件 -- 定义父子关系方向
[ORDER SIBLINGS BY 列];      -- 对兄弟节点排序

关键元素说明:

  • START WITH : 查询的起点(可以多行,也可以根节点为 NULL 等)
  • CONNECT BY : 定义递归的父子关系,PRIOR 的位置决定遍历方向
  • LEVEL : 伪列,根节点为 1,每下一层 +1
  • CONNECT_BY_ISLEAF : 叶子节点为 1,否则为 0
  • NOCYCLE : 遇到循环引用时不会报错,继续执行
  • ORDER SIBLINGS BY : 在同一父节点下进行排序,不影响层次结构

2.2 向下查找(找所有子孙)

需求:查询经理A(emp_id=101)及其所有下属员工。

sql 复制代码
SELECT emp_id, emp_name, mgr_id, LEVEL
FROM emp
START WITH emp_id = 101
CONNECT BY PRIOR emp_id = mgr_id;

输出结果:

EMP_ID EMP_NAME MGR_ID LEVEL
101 经理A 100 1
201 员工A1 101 2
202 员工A2 101 2

原理解析:

  • START WITH emp_id = 101 → 将经理A作为根节点(LEVEL=1)
  • CONNECT BY PRIOR emp_id = mgr_id
    • PRIOR emp_id 表示上一行(当前父节点)的 emp_id
    • 寻找所有 mgr_id 等于该值的行 → 即找到子节点
  • 递归执行:对每个新找到的子节点,继续找它的子节点(本例中无更深数据)

💡 口诀PRIOR 在等号左边,表示从父向下找子。

2.3 向上查找(找所有祖先)

需求:查询员工A1(emp_id=201)及其所有上级领导。

sql 复制代码
SELECT emp_id, emp_name, mgr_id, LEVEL
FROM emp
START WITH emp_id = 201
CONNECT BY emp_id = PRIOR mgr_id;

输出结果:

EMP_ID EMP_NAME MGR_ID LEVEL
201 员工A1 101 1
101 经理A 100 2
100 大老板 NULL 3

原理解析:

  • START WITH 起点是员工A1
  • CONNECT BY emp_id = PRIOR mgr_id
    • PRIOR mgr_id 表示上一行的 mgr_id(当前节点的上级编号)
    • 寻找 emp_id 等于该值的行 → 即找到父节点
  • 递归向上,直到找不到上级(mgr_id 为 NULL)

💡 口诀PRIOR 在等号右边,表示从子向上找父。


三、进阶技巧与实用函数

3.1 显示层级缩进(LPAD)

让输出结果更直观地体现树状层级。

sql 复制代码
SELECT LPAD(' ', 2 * (LEVEL - 1)) || emp_name AS tree_name,
       emp_id, mgr_id, LEVEL
FROM emp
START WITH mgr_id IS NULL
CONNECT BY PRIOR emp_id = mgr_id;

结果示意:

复制代码
TREE_NAME        EMP_ID  MGR_ID  LEVEL
大老板            100     NULL      1
  经理A          101      100       2
    员工A1       201      101       3
    员工A2       202      101       3
  经理B          102      100       2
    员工B1       203      102       3

3.2 显示完整路径(SYS_CONNECT_BY_PATH)

将从根节点到当前节点的路径拼接成一个字符串。

sql 复制代码
SELECT emp_name,
       SYS_CONNECT_BY_PATH(emp_name, ' → ') AS path,
       LEVEL
FROM emp
START WITH mgr_id IS NULL
CONNECT BY PRIOR emp_id = mgr_id;

结果:

EMP_NAME PATH LEVEL
大老板 大老板 1
经理A 大老板 → 经理A 2
员工A1 大老板 → 经理A → 员工A1 3
员工A2 大老板 → 经理A → 员工A2 3
经理B 大老板 → 经理B 2
员工B1 大老板 → 经理B → 员工B1 3

3.3 限制层级深度

只查询前 N 层,提高性能并避免过深递归。

sql 复制代码
-- 只查询根节点及其直接下级(前2层)
SELECT emp_name, LEVEL
FROM emp
START WITH mgr_id IS NULL
CONNECT BY PRIOR emp_id = mgr_id
AND LEVEL <= 2;   -- 注意条件放在 CONNECT BY 后面

3.4 对兄弟节点排序(ORDER SIBLINGS BY)

如果想按照某个字段(如名字)对同一父节点下的子节点排序,使用 ORDER SIBLINGS BY

sql 复制代码
SELECT LPAD(' ', 2 * (LEVEL - 1)) || emp_name AS name,
       emp_id, LEVEL
FROM emp
START WITH mgr_id IS NULL
CONNECT BY PRIOR emp_id = mgr_id
ORDER SIBLINGS BY emp_name DESC;

3.5 获取根节点值(CONNECT_BY_ROOT)

sql 复制代码
SELECT emp_name,
       CONNECT_BY_ROOT emp_name AS root_name,
       LEVEL
FROM emp
START WITH mgr_id IS NULL
CONNECT BY PRIOR emp_id = mgr_id;

3.6 判断叶子节点(CONNECT_BY_ISLEAF)

sql 复制代码
SELECT emp_name, CONNECT_BY_ISLEAF AS is_leaf
FROM emp
START WITH mgr_id IS NULL
CONNECT BY PRIOR emp_id = mgr_id;

结果: 叶子节点(无下属)的 is_leaf 为 1,非叶子节点为 0。


四、循环数据处理(NOCYCLE 与 CONNECT_BY_ISCYCLE)

在实际业务中,可能因为误操作导致数据出现循环引用 (例如 A 的上级是 B,B 的上级又是 A)。如果不处理,层次查询会无限递归,抛出 ORA-01436: CONNECT BY loop in user data

4.1 制造循环数据

sql 复制代码
-- 让 201 和 202 互相指向
UPDATE emp SET mgr_id = 202 WHERE emp_id = 201;
UPDATE emp SET mgr_id = 201 WHERE emp_id = 202;
COMMIT;

4.2 安全查询(使用 NOCYCLE)

sql 复制代码
SELECT emp_id, emp_name, mgr_id, LEVEL,
       CONNECT_BY_ISCYCLE AS is_cycle
FROM emp
START WITH emp_id = 201
CONNECT BY NOCYCLE PRIOR emp_id = mgr_id;

说明:

  • NOCYCLE:遇到循环节点时,不会报错,而是停止向下递归。
  • CONNECT_BY_ISCYCLE:标记当前行是否形成了循环(1 表示循环,0 正常)。

4.3 修复数据(恢复原状)

sql 复制代码
UPDATE emp SET mgr_id = 101 WHERE emp_id = 201;
UPDATE emp SET mgr_id = 102 WHERE emp_id = 202;
COMMIT;

五、性能优化建议

层次查询在处理百万级数据时,如果使用不当,性能可能急剧下降。以下是几条核心优化原则:

  1. 建立索引

    CONNECT BY 的连接列(通常是外键列,如 mgr_id)上创建索引。

    sql 复制代码
    CREATE INDEX idx_emp_mgr ON emp(mgr_id);
  2. 尽早剪枝

    CONNECT BY 中加入 AND LEVEL <= N,避免递归过深。

  3. 控制起始范围

    START WITH 的条件应尽量精确,避免从过多根节点开始。

  4. 避免在递归中调用函数

    不要在 CONNECT BY 条件中使用自定义函数或复杂计算。

  5. 使用 NOCYCLE 但注意开销

    NOCYCLE 需要维护已访问节点的路径信息,会略微增加开销,只在必要时使用。


六、Oracle 层次查询 vs 递归 CTE

从 Oracle 11gR2 开始,也支持 SQL 标准的递归 WITH(CTE)语法。两者对比如下:

特性 CONNECT BY 递归 WITH
语法简洁度 非常简洁 稍显冗长
路径拼接 内置 SYS_CONNECT_BY_PATH 需手动拼接
循环检测 NOCYCLE + CONNECT_BY_ISCYCLE 需自定义逻辑
性能(Oracle) 成熟优化,通常更快 递归优化相对较新
可移植性 Oracle 专有 标准 SQL,其他数据库可用

建议 :纯 Oracle 环境中优先使用 CONNECT BY;如果代码需要在多种数据库间迁移,可考虑递归 WITH。


七、总结:一张表记住关键用法

需求 写法
向下递归(找子孙) CONNECT BY PRIOR 父主键 = 子外键
向上递归(找祖先) CONNECT BY 子主键 = PRIOR 子外键
显示层级缩进 LPAD(' ', 2*(LEVEL-1))
显示完整路径 SYS_CONNECT_BY_PATH(列, '分隔符')
限制递归深度 CONNECT BY ... AND LEVEL <= N
防止循环 NOCYCLE,用 CONNECT_BY_ISCYCLE 检测
兄弟节点排序 ORDER SIBLINGS BY 列
获取根节点值 CONNECT_BY_ROOT 列
判断叶子节点 CONNECT_BY_ISLEAF

📚 附录:所有示例的快速执行脚本

为了方便测试,下面是一个完整的 SQL*Plus 脚本,包含了建表、插入数据和所有示例查询(循环部分已注释,可手动放开)。

sql 复制代码
-- 完整测试脚本(可直接复制到 Oracle 中运行)
-- 1. 建表及初始化
DROP TABLE emp PURGE;
CREATE TABLE emp (emp_id NUMBER PRIMARY KEY, emp_name VARCHAR2(50), mgr_id NUMBER);
INSERT INTO emp VALUES (100, '大老板', NULL);
INSERT INTO emp VALUES (101, '经理A', 100);
INSERT INTO emp VALUES (102, '经理B', 100);
INSERT INTO emp VALUES (201, '员工A1', 101);
INSERT INTO emp VALUES (202, '员工A2', 101);
INSERT INTO emp VALUES (203, '员工B1', 102);
COMMIT;

-- 2. 向下查询
SELECT emp_id, emp_name, mgr_id, LEVEL FROM emp START WITH emp_id = 101 CONNECT BY PRIOR emp_id = mgr_id;

-- 3. 向上查询
SELECT emp_id, emp_name, mgr_id, LEVEL FROM emp START WITH emp_id = 201 CONNECT BY emp_id = PRIOR mgr_id;

-- 4. 缩进显示
SELECT LPAD(' ', 2 * (LEVEL - 1)) || emp_name AS tree_name, emp_id, mgr_id, LEVEL FROM emp START WITH mgr_id IS NULL CONNECT BY PRIOR emp_id = mgr_id;

-- 5. 路径显示
SELECT emp_name, SYS_CONNECT_BY_PATH(emp_name, '->') AS path, LEVEL FROM emp START WITH mgr_id IS NULL CONNECT BY PRIOR emp_id = mgr_id;

-- 6. 限制深度
SELECT emp_name, LEVEL FROM emp START WITH mgr_id IS NULL CONNECT BY PRIOR emp_id = mgr_id AND LEVEL <= 2;

-- 7. 兄弟排序
SELECT LPAD(' ', 2 * (LEVEL - 1)) || emp_name AS name FROM emp START WITH mgr_id IS NULL CONNECT BY PRIOR emp_id = mgr_id ORDER SIBLINGS BY emp_name;

-- 8. 根节点和叶子节点
SELECT emp_name, CONNECT_BY_ROOT emp_name AS root_name, CONNECT_BY_ISLEAF AS is_leaf FROM emp START WITH mgr_id IS NULL CONNECT BY PRIOR emp_id = mgr_id;

-- 9. 循环检测(需要先制造循环,请谨慎操作)
-- UPDATE emp SET mgr_id = 202 WHERE emp_id = 201; UPDATE emp SET mgr_id = 201 WHERE emp_id = 202; COMMIT;
-- SELECT emp_id, emp_name, mgr_id, LEVEL, CONNECT_BY_ISCYCLE AS is_cycle FROM emp START WITH emp_id = 201 CONNECT BY NOCYCLE PRIOR emp_id = mgr_id;
-- UPDATE emp SET mgr_id = 101 WHERE emp_id = 201; UPDATE emp SET mgr_id = 102 WHERE emp_id = 202; COMMIT;

🎯 写在最后

Oracle 的层次查询功能强大、语法简洁,是处理树状结构数据的首选工具。掌握了 START WITHCONNECT BYLEVELSYS_CONNECT_BY_PATH 等核心组件,你就能轻松应对组织架构、权限树、BOM 等常见业务场景。

如果你在实际开发中遇到更复杂的层次查询问题(例如需同时向上和向下、多表连接后的层次查询等),欢迎留言交流。

本文中的示例均在 Oracle 19c 上测试通过,早期版本(10g、11g)同样适用。


🎉 感谢阅读,希望对你有帮助!

相关推荐
闪电悠米2 小时前
黑马点评-优惠券秒杀-03_basic_seckill_and_oversell
java·数据库·spring boot·spring·缓存·oracle·面试
逍遥德2 小时前
PostgreSQL --- 数组函数详解
数据库·sql·postgresql
.Cnn2 小时前
MySQL事务和Spring事务
数据库·后端·mysql·spring
福大大架构师每日一题2 小时前
redis 8.8.0 发布:新数据结构、字段级通知、INCREX、XNACK 全面升级,8.6 到 8.8 变化一文看懂
数据结构·数据库·redis
霸道流氓气质2 小时前
Spring Data JPA 完全指南
开发语言·数据库
Demon1_Coder2 小时前
Day4-LangChain4j-向量数据库-检索增强RAG
数据库
phltxy2 小时前
RabbitMQ 应用问题
数据库·分布式·rabbitmq
星晨雪海2 小时前
基于 SpringBoot + Redis (Lettuce) + RabbitMQ 实现「Redis 预扣库存 + 异步同步数据库」
数据库·spring boot·java-rabbitmq
mosaic_born2 小时前
centos 7.9 离线部署Zabbix 6.0.46 监控详细方案(解决数据库字符集问题)
数据库·centos·zabbix