使用递归子查询分解进行循环检测

Posted

技术标签:

【中文标题】使用递归子查询分解进行循环检测【英文标题】:Cycle detection with recursive subquery factoring 【发布时间】:2010-12-16 10:57:22 【问题描述】:

自 v2 以来,Oracle SQL 可以使用其专有的 CONNECT BY 语法进行分层查询。在他们最新的 11g 第 2 版中,他们添加了递归子查询分解,也称为递归 with 子句。这是 ANSI 标准,如果我理解正确的话,其他 RDBMS 供应商也已经实现了这一标准。

在比较连接方式和递归方式时,我注意到使用循环检测时结果集有所不同。按结果连接对我来说更直观,所以我想知道 Oracle 的实现是否包含错误,或者这是否是标准 ANSI 和预期行为。因此,我的问题是您是否可以使用 mysql、DB2、SQL Server 等其他数据库来检查递归查询。当然,前提是那些数据库支持递归 with 子句。

这是它在 Oracle 11.2.0.1.0 上的工作方式

SQL> select *
  2    from t
  3  /

        ID  PARENT_ID
---------- ----------
         1          2
         2          1

2 rows selected.

使用 CONNECT BY 语法的查询:

SQL>  select id
  2        , parent_id
  3        , connect_by_iscycle
  4     from t
  5  connect by nocycle parent_id = prior id
  6    start with id = 1
  7  /

        ID  PARENT_ID CONNECT_BY_ISCYCLE
---------- ---------- ------------------
         1          2                  0
         2          1                  1

2 rows selected.

这对我来说很直观。然而,使用新的 ANSI 语法,它又返回一行:

SQL> with tr (id,parent_id) as
  2  ( select id
  3         , parent_id
  4      from t
  5     where id = 1
  6     union all
  7    select t.id
  8         , t.parent_id
  9      from t
 10           join tr on t.parent_id = tr.id
 11  ) cycle id set is_cycle to '1' default '0'
 12  select id
 13       , parent_id
 14       , is_cycle
 15    from tr
 16  /

        ID  PARENT_ID I
---------- ---------- -
         1          2 0
         2          1 0
         1          2 1

3 rows selected.

这是您可以用来检查的脚本:

create table t
( id        number
, parent_id number
);
insert into t values (1, 2);
insert into t values (2, 1);
commit;
with tr (id,parent_id) as
( select id
       , parent_id
    from t
   where id = 1
   union all
  select t.id
       , t.parent_id
    from t
         join tr on t.parent_id = tr.id
) cycle id set is_cycle to '1' default '0'
select id
     , parent_id
     , is_cycle
  from tr;

【问题讨论】:

我知道的唯一支持WITH 子句的数据库是Oracle 9i+、SQL Server 2005+ 和DB2(不知道版本)。 MySQL 绝对不支持 WITH 子句 - 该请求自 2006 年以来一直存在:/ 【参考方案1】:

来自CONNECT_BY_ISCYCLE 上的文档:

CONNECT_BY_ISCYCLE 伪列返回 1 如果当前行有一个子元素,它也是它的祖先

还有CYCLE:

如果一行的祖先行与循环列的值相同,则认为该行形成了一个循环。

在您的示例中,行 2 确实有一个子代,它也是它的祖先,但它的 id 尚未返回。

换句话说,CONNECT_BY_ISCYCLE 检查 children(尚未返回),而 CYCLE 检查 当前行(已返回) .

CONNECT BY 是基于行的,而递归 CTE 是基于集合的。

请注意,Oracle 在CYCLE 上的文档提到了“祖先行”。但是,一般来说,递归CTE 中没有“祖先行”的概念。这是一个基于集合的操作,可以完全从树中产生结果。一般来说,锚部分和递归部分甚至可以使用不同的表。

由于递归CTE通常用于构建层次结构树,Oracle 决定添加循环检查。但是由于递归CTE 的操作基于集合的方式,通常无法判断下一步是否会生成循环,因为如果没有明确定义“祖先行”循环条件也无法定义。

要执行“下一个”步骤,整个“当前”集需要可用,但要生成当前集的每一行(包括循环列),我们只需要“下一个”的结果操作。

如果当前集合总是由单行组成(如CONNECT BY),这不是问题,但如果将递归操作定义为一个整体,则会有问题。

还没有研究Oracle 11,但SQL Server 实现了递归CTE,只需在它们后面隐藏CONNECT BY,这需要设置许多限制(所有这些都有效地禁止所有基于集合的操作)。

另一方面,PostgreSQL 的实现是真正基于集合的:您可以在递归部分对锚部分进行任何操作。但是,它没有任何方法可以检测循环,因为循环一开始就没有定义。

如前所述,MySQL 根本没有实现 CTE (它也没有实现 HASH JOINMERGE JOINs,只有嵌套循环,所以不要感到惊讶很多)。

具有讽刺意味的是,我今天收到了一封关于这个主题的信,我将在我的博客中介绍。

更新:

SQL Server 中的递归CTE 只不过是伪装的CONNECT BY。有关令人震惊的详细信息,请参阅我博客中的这篇文章:

SQL Server: are the recursive CTE’s really set-based?

【讨论】:

很好的解释,Quassnoi。谢谢。所以递归 with 子句按预期工作。它与连接方式不同。很高兴知道两者之间的这些细微差别。 这个解释很好,但我认为它是不正确的。具体来说,不清楚的是“祖先”的定义。如果按照您的建议定义是基于集合的,那么我们应该检测具有以下数据和递归查询的循环。但是,如果您查看输出,CYCLE 在所有行中都是 0。如果您的解释是正确的,那么 (1, 3) 行之一应该有 CYCLE = 1 - 实际上,两列中的值是相同的,并且在锚成员中生成一行,而另一行来自递归成员。如果定义是基于 set 的,则一个是另一个的祖先。 with t(parent, child) as ( select 1, 2 from dual union all select 1, 3 from dual union all select 2, 3 from dual ), r (parent, child) as ( select parent, child from t where parent = 1 union all select r.parent, t.child from t join r on t.parent = r.child ) cycle parent, child set cycle to '1' default '0' select * from r; @mathguy:我在帖子中明确提到过,是的,不清楚,不,我“还没有研究过 Oracle 11”。 Oracle 要求递归成员必须跟随锚成员并且必须引用query_name 一次。我没有方便的安装,但我相信有效地使递归 CTE 基于行的其他限制也适用。如果您像在示例中那样明确定义循环列,Oracle 会使用它;如果不这样做,它会使用连接到递归部分的锚部分列的子集。 @mathguy:正如我所说,PostgreSQL 没有内置循环检测器,不会以任何方式限制递归 CTE 复杂性,也不会反对运行无限递归【参考方案2】:

PostgreSQL 支持 WITH 样式的分层查询,但没有任何自动循环检测。这意味着您需要自己编写,返回的行数取决于您在查询的递归部分中指定连接条件的方式。

两个示例都使用数组 if ID(称为 all_ids)来检测循环:

WITH recursive tr (id, parent_id, all_ids, cycle) AS (
    SELECT id, parent_id, ARRAY[id], false
    FROM t
    WHERE id = 1
    UNION ALL
    SELECT t.id, t.parent_id, all_ids || t.id, t.id = ANY(all_ids)
    FROM t
    JOIN tr ON t.parent_id = tr.id AND NOT cycle)
SELECT id, parent_id, cycle
FROM tr;

 id | parent_id | cycle
----+-----------+-------
  1 |         2 | f
  2 |         1 | f
  1 |         2 | t


WITH recursive tr (id, parent_id, all_ids, cycle) AS (
    SELECT id, parent_id, ARRAY[id], false
    FROM t
    WHERE id = 1
    UNION ALL
    SELECT t.id, t.parent_id, all_ids || t.id, (EXISTS(SELECT 1 FROM t AS x WHERE x.id = t.parent_id))
    FROM t
    JOIN tr ON t.parent_id = tr.id
    WHERE NOT t.id = ANY(all_ids))
SELECT id, parent_id, cycle
FROM tr;

 id | parent_id | cycle
----+-----------+-------
  1 |         2 | f
  2 |         1 | t

【讨论】:

非常有趣的查询。 Oracle 不支持该数组语法,但很高兴看到结果与 Oracle 的 ANSI 语法中的结果相同。它使我认为 ANSI 查询可能根本不包含错误。谢谢,亚历山大。 迟到了,但现在 PostgreSQL 也允许您使用 UNION 而不是 UNION ALL 来自动删除周期。【参考方案3】:

阿法伊克:

MySQL 不支持递归 CTE SQL Sever 不支持循环 递归 CTE 中的检测

【讨论】:

MySQL 根本不支持 CTE。 很清楚,谢谢。所以不能用 MySQL 和 SQL Server 来测试这个脚本。 确实,MS SQL Server 有一个最大递归限制,默认为 100 左右。【参考方案4】:

MySQL Server 5.0.45 版不喜欢with

ERROR 1064 (42000):您的 SQL 语法有错误;检查 与您的 MySQL 服务器版本相对应的手册 在 'with tr (id, parent_id) as (select id, parent_id) 附近使用的语法 from t where id = 1 union all s' at line 1.

【讨论】:

感谢您的尝试,沃利克。但是 MySQL 是否支持类似的东西而这只是不正确的语法,还是 MySQL 根本不支持递归查询? 我不这么认为。仅仅几年前它还不支持存储过程,所以它发展得相当快。如果这些内容出现在最新版本或即将发布的版本中,我不会感到惊讶。【参考方案5】:

连接方式的结果可能并不总是直观的。

以下查询演示了检测图片上图形的以id = 3 开头的循环的不同方法。

create table graph (id, id_parent) as
(select 2, 1 from dual
union all select 3, 1 from dual
union all select 4, 3 from dual
union all select 5, 4 from dual
union all select 3, 5 from dual)

SQL> select level lvl, graph.*, connect_by_iscycle cycle
  2    from graph
  3   start with id = 3
  4  connect by nocycle prior id = id_parent;

       LVL         ID  ID_PARENT      CYCLE
---------- ---------- ---------- ----------
         1          3          1          0
         2          4          3          0
         3          5          4          1
         1          3          5          0
         2          4          3          0
         3          5          4          1

6 rows selected.

SQL> select level lvl, graph.*, connect_by_iscycle cycle
  2    from graph
  3   start with id = 3
  4  connect by nocycle prior id = id_parent
  5         and prior id_parent is not null;

       LVL         ID  ID_PARENT      CYCLE
---------- ---------- ---------- ----------
         1          3          1          0
         2          4          3          0
         3          5          4          0
         4          3          5          1
         1          3          5          0
         2          4          3          0
         3          5          4          1

7 rows selected.

SQL> with t(id, id_parent) as
  2   (select *
  3      from graph
  4     where id = 3
  5    union all
  6    select g.id, g.id_parent
  7      from t
  8      join graph g
  9        on t.id = g.id_parent)
 10  search depth first by id set ord
 11  cycle id set cycle to 1 default 0
 12  select * from t;

        ID  ID_PARENT        ORD C
---------- ---------- ---------- -
         3          1          1 0
         4          3          2 0
         5          4          3 0
         3          5          4 1
         3          5          5 0
         4          3          6 0
         5          4          7 0
         3          5          8 1

8 rows selected.

id = 3 的节点有两个父节点,因此 Oracle 在本例中遍历两个周期。

(1, 3) -> (3, 4) -> (4, 5) -> (5, 3)

(5, 3) -> (3, 4) -> (4, 5)

第一个查询和第一个循环的结果中缺少边 (5, 3)。 同时边 (5, 3) 出现在第三次查询和第二次循环两次的结果中。

为什么会这样?您可以在 Quassnoi 提供的答案中检查循环检测逻辑的描述。用简单的英语,它意味着

(1) 如果 当前行的子 ID 是其中的一部分,则 connect by 检测循环 到目前为止访问过的 ID

(2) 如果 当前行的 ID 是 ID 的一部分,则 rec 会检测到一个循环 到目前为止访问过

第二个查询的结果看起来最自然,尽管有额外的谓词and prior id_parent is not null。在这种情况下

(3) 如果 当前行的 ID父 ID 的一部分,它会检测到一个循环 到目前为止访问过

所有这些条件都在下面查询的cnt1、cnt2、cnt3列中实现。

SQL> with t(id, id_parent, path_id, path_id_parent, cnt1, cnt2, cnt3) as
  2   (select g.*,
  3           cast('->' || g.id as varchar2(4000)),
  4           cast('->' || g.id_parent as varchar2(4000)),
  5           0,
  6           0,
  7           0
  8      from graph g
  9     where id = 3
 10    union all
 11    select g.id,
 12           g.id_parent,
 13           t.path_id || '->' || g.id,
 14           t.path_id_parent || '->' || g.id_parent,
 15           regexp_count(t.path_id || '->', '->' ||
 16            (select id from graph c where c.id_parent = g.id) || '->'),
 17           regexp_count(t.path_id || '->', '->' || g.id || '->'),
 18           regexp_count(t.path_id_parent || '->', '->' || g.id || '->')
 19      from t
 20      join graph g
 21        on t.id = g.id_parent
 22    -- and t.cnt1 = 0
 23    -- and t.cnt2 = 0
 24    -- and t.cnt3 = 0
 25    )
 26  search depth first by id set ord
 27  cycle id set cycle to 1 default 0
 28  select * from t;

        ID  ID_PARENT PATH_ID         PATH_ID_PARENT  CNT1 CNT2 CNT3        ORD C
---------- ---------- --------------- --------------- ---- ---- ---- ---------- -
         3          1 ->3             ->1                0    0    0          1 0
         4          3 ->3->4          ->1->3             0    0    0          2 0
         5          4 ->3->4->5       ->1->3->4          1    0    0          3 0
         3          5 ->3->4->5->3    ->1->3->4->5       1    1    1          4 1
         3          5 ->3             ->5                0    0    0          5 0
         4          3 ->3->4          ->5->3             0    0    0          6 0
         5          4 ->3->4->5       ->5->3->4          1    0    1          7 0
         3          5 ->3->4->5->3    ->5->3->4->5       1    1    1          8 1

8 rows selected.

如果您取消注释按 cnt1/cnt2/cnt3 过滤并删除“cycle id set cycle to 1 default 0”,则查询将返回上述相应查询的结果。换句话说,您可以避免使用cycle clause,并实施您认为更直观的任何循环检测逻辑

关于遍历层次结构和循环检测的更多细节可以在书Oracle SQL Revealed中找到。

【讨论】:

【参考方案6】:
WITH RECURSIVE s (master, slave, all_ids, cycle) AS
(
    SELECT master, slave, ARRAY[master], false FROM binding WHERE master=3477

    UNION ALL

    SELECT d.master, d.slave, all_ids || d.master, d.slave = ANY(all_ids)
    FROM
        binding AS d
    JOIN
        s
    ON (d.master = s.slave)
    WHERE NOT d.master = ANY(all_ids)
)
SELECT *
FROM s;

我认为这个条件更好d.slave = ANY(all_ids)

【讨论】:

【参考方案7】:

“因此我的问题是,您是否可以使用 MySQL、DB2、SQL Server 等其他数据库检查递归查询”

MariaDB 10.5.2 及更新版本支持循环检测:

WITH

CYCLE 子句启用 CTE 循环检测,避免过多或无限循环,MariaDB 支持宽松的非标准语法。

WITH RECURSIVE ... (
 ...
)
CYCLE <cycle column list> RESTRICT

例子:

CREATE TABLE t(id INT, parent_id INT);
INSERT INTO t(id, parent_id) VALUES (1, NULL),(2,1),(3,2),(1,3);

WITH RECURSIVE cte AS (
  SELECT id, parent_id, 0 lvl 
  FROM t WHERE parent_id IS NULL
  UNION ALL
  SELECT t.id, t.parent_id, lvl + 1 AS lvl
  FROM cte c1
  JOIN t ON c1.id = t.parent_id
)
CYCLE id, parent_id RESTRICT 
SELECT * FROM cte ORDER BY lvl;

db<>fiddle demo

【讨论】:

以上是关于使用递归子查询分解进行循环检测的主要内容,如果未能解决你的问题,请参考以下文章

子查询分解问题

oracle 递归 通过子节点查询根节点

SQL Server 树形表非循环递归查询

SQL相关子查询与非相关子查询

200分求助!SQL递归查询所有子节点

SQL递归查询所有子节点