在关系型数据库中,处理具有层级或树状结构的数据(如组织架构、物料清单、分类目录等)是一项常见需求。Oracle 数据库提供了强大的递归查询机制,允许开发者在单条 SQL 语句中遍历这种层次关系。
目前,Oracle 支持两种主流的递归查询方式:
CONNECT BY层次查询:Oracle 专有的传统语法,语法简洁,在特定场景下性能优异。WITH RECURSIVE递归公用表表达式 (CTE):符合 ANSI SQL 标准的现代语法,自 Oracle 11g Release 2 起引入,具有更好的可移植性和灵活性。
在oracle数据库中,有如下表:
1 | -- 1. 创建表结构(如果表已存在,请注释掉或删除此段) |
想要将字段RECEIVERS和INCLUDEPATHS按照分号;拆分后一一对应,形成一个新的表,使用递归查询 connect by level 的sql如下下:
1 | WITH t_base AS ( |
对这个sql的说明如下,写法适用于Oracle 10g 及以上版本:
**
t_base:首先获取原始表数据,并提取ROWID。这是关键锚点,确保我们在后续拆分时不会把 A 行的接收人和 B 行的路径搞混。t_recv_split&t_path_split:这两个子查询分别负责“切分”工作。它们利用 CONNECT BY LEVEL将字符串切成多行。注意这里的PRIOR rid = rid和PRIOR SYS_GUID() IS NOT NULL是 Oracle 中防止层级查询在多行数据上发生混乱的标准写法。最终 SELECT:这里使用了标准的 JOIN … ON …。因为 t_recv_split 和 t_path_split 都只包含 rid 和拆分后的值,当它们通过 rid 关联回 t_base 时,Oracle 会自动执行 笛卡尔积。 例如:对于 rid=’AAA’ 的行,如果有 4 个接收人和 3 个路径,数据库会自动生成 4×3=12 种组合。
对
CONNECT BY LEVEL <= LENGTH(...) - LENGTH(REPLACE(...)) + 1的作用为我要拆成几行:这是层级查询的终止条件。Oracle 的 CONNECT BY会不断递归生成新行,直到这个条件不再满足为止。我们需要告诉数据库:对于当前这一行数据,到底有多少个分号分隔的值?
原理推导:
假设字符串是 ‘A;B;C’。
LENGTH(‘A;B;C’) = 5。
REPLACE(‘A;B;C’, ‘;’, ‘’) 变成了 ‘ABC’,长度为 3。
两者相减:5−3=2 。这说明有 2 个分号。
值的数量 = 分隔符数量 + 1。所以公式是 2+1=3 。
结果: 数据库知道这一行需要生成 3 层数据(Level 1, Level 2, Level 3),分别对应 A、B、C。AND PRIOR rid = rid
作用: “自连接”锚点,锁定当前行。
这是最关键的一句,防止数据“串台”。
为什么需要它?
CONNECT BY 本质上是一种树形遍历(Hierarchical Query)。如果不加限制,Oracle 会把表里所有的行都看作一棵大树的一部分。比如第 1 行的 ‘A’ 可能会错误地连接到第 2 行的 ‘X’ 下面去。
原理:
PRIOR 关键字代表“上一级(父节点)”的数据。
rid 是我们预先取出的 ROWID(物理行唯一标识)。
PRIOR rid = rid 的意思是:下一级的数据,其 ROWID 必须和上一级的 ROWID 相同。
效果: 这强制规定了递归只能在 同一行内部 进行。第 1 行只会在第 1 行内部拆分,绝对不会跑到第 2 行去。AND PRIOR SYS_GUID() IS NOT NULL
作用: 打破死循环的“欺骗性”条件。这是一个在 Oracle 单表层级查询中非常著名的“黑科技”写 法。为什么要加它?
如果你只写 PRIOR rid = rid,Oracle 优化器会发现:既然父节点的 ID 等于子节点的 ID,那么这就构成了一个无限循环(A 的父亲是 A,A 的儿子也是 A…)。为了防止这种逻辑上的死循环报错,或者为了强制 Oracle 放弃使用基于索引的快速路径(Index Fast Full Scan)而改用全表扫描式的递归,我们需要引入一个 “永远不相等” 或 “永远为真但无法预测” 的条件来混淆优化器。
原理:
SYS_GUID() 每次调用都会生成一个全新的、唯一的随机字符串(UUID)。
PRIOR SYS_GUID() 获取的是上一层生成的 UUID。
当前层的 SYS_GUID() 肯定不等于上一层的 UUID。
虽然逻辑上我们写的是 IS NOT NULL(GUID 永远不会是 NULL,所以这个条件恒为真),但它的存在告诉 Oracle 引擎:“嘿,每一层都有一个独特的、不可预测的值,别试图把它们合并或者建立错误的索引连接。”
sql的另外一个写法,性能不如上面的sql,但是通用性更好
1 | select REGEXP_SUBSTR(RECEIVERS, '[^;]+', 1, L) AS RECEIVERS,alarm_mode, |
在mysql8的版本中,不支持connect by level的写法,使用标准递归CTE的写法
1 | -- 1. 创建表结构 (如果表已存在,请注释掉或删除此段) |
1 | -- 递归查询 |