树状结构数据怎么查?一文掌握 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,每下一层 +1CONNECT_BY_ISLEAF: 叶子节点为 1,否则为 0NOCYCLE: 遇到循环引用时不会报错,继续执行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_idPRIOR 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起点是员工A1CONNECT BY emp_id = PRIOR mgr_idPRIOR 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;
五、性能优化建议
层次查询在处理百万级数据时,如果使用不当,性能可能急剧下降。以下是几条核心优化原则:
-
建立索引
在
CONNECT BY的连接列(通常是外键列,如mgr_id)上创建索引。sqlCREATE INDEX idx_emp_mgr ON emp(mgr_id); -
尽早剪枝
在
CONNECT BY中加入AND LEVEL <= N,避免递归过深。 -
控制起始范围
START WITH的条件应尽量精确,避免从过多根节点开始。 -
避免在递归中调用函数
不要在
CONNECT BY条件中使用自定义函数或复杂计算。 -
使用
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 WITH、CONNECT BY、LEVEL、SYS_CONNECT_BY_PATH 等核心组件,你就能轻松应对组织架构、权限树、BOM 等常见业务场景。
如果你在实际开发中遇到更复杂的层次查询问题(例如需同时向上和向下、多表连接后的层次查询等),欢迎留言交流。
本文中的示例均在 Oracle 19c 上测试通过,早期版本(10g、11g)同样适用。
🎉 感谢阅读,希望对你有帮助!