Oracle SQL 循环自连接
Oracle SQL looped self-join
上下文:
假设我有 table 有一个外键引用它自己的主键,像这样:
|---------------------|------------------|------------------|
| ID | NAME | PARENT_ID |
|---------------------|------------------|------------------|
| 01 | John | 04 |
|---------------------|------------------|------------------|
| 02 | Paul | 01 |
|---------------------|------------------|------------------|
| 03 | George | 02 |
|---------------------|------------------|------------------|
| 04 | Ringo | 03 |
|---------------------|------------------|------------------|
问题:
因此,如您所见,存在循环层次结构:Ringo->George->Paul->John->Ringo->George->Paul->John->etc.
问题:
是否有SQL select可以检测到这种循环?
我知道我可以编写递归 PL/SQL 过程,但我更喜欢 "pure" SQL.
的解决方案
提前致谢
您可以使用带有 CONNECT_BY_ISCYCLE 伪列的 CONNECT BY 查询来查找循环 - 请参阅 example from Oracle docs:
SELECT last_name "Employee", CONNECT_BY_ISCYCLE "Cycle"
FROM employees
WHERE level <= 3
AND department_id = 80
START WITH last_name = 'King'
CONNECT BY NOCYCLE PRIOR employee_id = manager_id;
您可以使用 connect by nocycle
和 connect_by_iscycle
执行此操作。对于您的 table 结构,它看起来像:
select id, name, parent_id, connect_by_iscycle
from mytable
connect by nocycle id = prior parent_id
start with id = 4
connect by nocycle
导致查询在遇到循环时停止迭代,并且伪列 connect_by_iscycle
包含一个标志,指示它发生在哪个点,如 shown in this demo:
ID | NAME | PARENT_ID | CONNECT_BY_ISCYCLE
-: | :----- | --------: | -----------------:
4 | Ringo | 3 | 0
3 | George | 2 | 0
2 | Paul | 1 | 0
1 | John | 4 | 1 --> cycle detected here
这是一个递归 cte 的解决方案:
with cte (id, parent_id, ids) as
(
select id, parent_id, to_char(id) from mytable
union all
select t.id, t.parent_id, ids || ' -> ' || t.id
from cte
join mytable t on t.id = cte.parent_id
)
cylce id set cycle to 1 default 0
select ids as cycling_ids
from cte
where cycle = 1
order by ids;
结果:
+ ----------------------+
| CYCLING_IDS |
+ ----------------------+
| 1 -> 4 -> 3 -> 2 -> 1 |
| 2 -> 1 -> 4 -> 3 -> 2 |
| 3 -> 2 -> 1 -> 4 -> 3 |
| 4 -> 3 -> 2 -> 1 -> 4 |
+ ----------------------+
如果您只想查看每个循环一次(我假设),请记住每个循环的最小 ID,并且每个最小 ID 只显示一个循环:
with cte (id, parent_id, ids, min_id) as
(
select id, parent_id, to_char(id), id from mytable
union all
select t.id, t.parent_id, ids || ' -> ' || t.id, least(t.id, cte.min_id)
from cte
join mytable t on t.id = cte.parent_id
)
cycle id set cycle to 1 default 0
select min(ids) as cycling_ids
from cte
where cycle = 1
group by min_id
order by min_id;
结果:
+ ----------------------+
| CYCLING_IDS |
+ ----------------------+
| 1 -> 4 -> 3 -> 2 -> 1 |
+ ----------------------+
具有更多 ID 和不同案例的演示:https://dbfiddle.uk/?rdbms=oracle_18&fiddle=f7f924cd8759d67a188b7c11f2d071ef
(这仍然不完美。如果一个非常小的 ID 导致更高的 ID 形成一个循环,例如,如果我们插入一个 ID 0 引用 ID 3 作为父级,查询将多次显示循环。这不容易避免,因为我们必须检测圆圈内 的最小 ID。我可能会写一个小的 PL/SQL 函数来从 ID 中获取这个最小 ID字符串。)
上下文:
假设我有 table 有一个外键引用它自己的主键,像这样:
|---------------------|------------------|------------------|
| ID | NAME | PARENT_ID |
|---------------------|------------------|------------------|
| 01 | John | 04 |
|---------------------|------------------|------------------|
| 02 | Paul | 01 |
|---------------------|------------------|------------------|
| 03 | George | 02 |
|---------------------|------------------|------------------|
| 04 | Ringo | 03 |
|---------------------|------------------|------------------|
问题:
因此,如您所见,存在循环层次结构:Ringo->George->Paul->John->Ringo->George->Paul->John->etc.
问题:
是否有SQL select可以检测到这种循环?
我知道我可以编写递归 PL/SQL 过程,但我更喜欢 "pure" SQL.
的解决方案提前致谢
您可以使用带有 CONNECT_BY_ISCYCLE 伪列的 CONNECT BY 查询来查找循环 - 请参阅 example from Oracle docs:
SELECT last_name "Employee", CONNECT_BY_ISCYCLE "Cycle"
FROM employees
WHERE level <= 3
AND department_id = 80
START WITH last_name = 'King'
CONNECT BY NOCYCLE PRIOR employee_id = manager_id;
您可以使用 connect by nocycle
和 connect_by_iscycle
执行此操作。对于您的 table 结构,它看起来像:
select id, name, parent_id, connect_by_iscycle
from mytable
connect by nocycle id = prior parent_id
start with id = 4
connect by nocycle
导致查询在遇到循环时停止迭代,并且伪列 connect_by_iscycle
包含一个标志,指示它发生在哪个点,如 shown in this demo:
ID | NAME | PARENT_ID | CONNECT_BY_ISCYCLE -: | :----- | --------: | -----------------: 4 | Ringo | 3 | 0 3 | George | 2 | 0 2 | Paul | 1 | 0 1 | John | 4 | 1 --> cycle detected here
这是一个递归 cte 的解决方案:
with cte (id, parent_id, ids) as
(
select id, parent_id, to_char(id) from mytable
union all
select t.id, t.parent_id, ids || ' -> ' || t.id
from cte
join mytable t on t.id = cte.parent_id
)
cylce id set cycle to 1 default 0
select ids as cycling_ids
from cte
where cycle = 1
order by ids;
结果:
+ ----------------------+ | CYCLING_IDS | + ----------------------+ | 1 -> 4 -> 3 -> 2 -> 1 | | 2 -> 1 -> 4 -> 3 -> 2 | | 3 -> 2 -> 1 -> 4 -> 3 | | 4 -> 3 -> 2 -> 1 -> 4 | + ----------------------+
如果您只想查看每个循环一次(我假设),请记住每个循环的最小 ID,并且每个最小 ID 只显示一个循环:
with cte (id, parent_id, ids, min_id) as
(
select id, parent_id, to_char(id), id from mytable
union all
select t.id, t.parent_id, ids || ' -> ' || t.id, least(t.id, cte.min_id)
from cte
join mytable t on t.id = cte.parent_id
)
cycle id set cycle to 1 default 0
select min(ids) as cycling_ids
from cte
where cycle = 1
group by min_id
order by min_id;
结果:
+ ----------------------+ | CYCLING_IDS | + ----------------------+ | 1 -> 4 -> 3 -> 2 -> 1 | + ----------------------+
具有更多 ID 和不同案例的演示:https://dbfiddle.uk/?rdbms=oracle_18&fiddle=f7f924cd8759d67a188b7c11f2d071ef
(这仍然不完美。如果一个非常小的 ID 导致更高的 ID 形成一个循环,例如,如果我们插入一个 ID 0 引用 ID 3 作为父级,查询将多次显示循环。这不容易避免,因为我们必须检测圆圈内 的最小 ID。我可能会写一个小的 PL/SQL 函数来从 ID 中获取这个最小 ID字符串。)