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

时间:2009-11-13 20:59:42

标签: sql oracle db2 hierarchical-query

Oracle SQL可以使用其专有的CONNECT BY语法从v2开始执行分层查询。在他们最新的11g版本2中,他们添加了递归子查询因子,也称为recursive with子句。这是ANSI标准,如果我理解正确,这个也已由其他RDBMS供应商实现。

当比较连接与递归时,我注意到使用循环检测时结果集的差异。结果连接对我来说更直观,所以我想知道Oracle的实现是否包含错误,或者这是否是标准ANSI和预期行为。因此,我的问题是,如果您可以使用其他数据库(如MySQL,DB2,SQL Server等)检查递归查询。如果这些数据库当然支持recursive 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;

7 个答案:

答案 0 :(得分:13)

来自CONNECT_BY_ISCYCLE的文档:

  

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

以及CYCLE上的内容:

  

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

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

换句话说,CONNECT_BY_ISCYCLE检查孩子(尚未退回),而CYCLE检查当前行(已经退回了。)

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

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

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

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

如果当前集合总是由单行组成(如CONNECT BY中),则不是问题,但如果在集合上定义的递归操作是一个问题。

尚未查看Oracle 11,但SQL Server只是隐藏CTE后面的CONNECT BY来实现递归PostgreSQL,这需要多次限制(所有这有效地禁止所有基于集合的操作。)

另一方面,

MySQL的实现是真正基于集合的:你可以在递归部分中使用锚点部分进行任何操作。但是,它没有任何检测周期的方法,因为首先没有定义周期。

如前所述,CTE根本没有实现HASH JOIN(它也没有实现MERGE JOINCTE,只有嵌套循环,所以不要太惊讶。)

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

<强>更新

{p> SQL Server中的递归CONNECT BY伪装不超过{{1}}。有关令人震惊的详细信息,请参阅我的博客中的这篇文章:

答案 1 :(得分:6)

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

如果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

答案 2 :(得分:3)

AFAIK:

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

答案 3 :(得分:1)

MySQL服务器版本5.0.45不喜欢with

  

错误1064(42000):您的SQL语法有错误;检查   手册,对应右边的MySQL服务器版本   在'with tr(id,parent_id)附近使用的语法为(select id,parent_id   从t,其中id = 1,在第1行结合所有s'。

答案 4 :(得分:0)

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)

答案 5 :(得分:0)

连接依据的结果可能并不总是很直观。

以下查询展示了用于检测图片上图形从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)

enter image description here

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提供的答案中检查周期检测逻辑的描述。用简单的英语来表示

  如果当前行的子代ID 是其中的一部分,

(1)connect by检测到一个周期   到目前为止已访问的ID

     

(2)rec with如果当前行的ID 是ID的一部分,则检测到一个周期   到目前为止已访问

第二个查询的结果看起来是最自然的,尽管还有其他谓词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 :(得分:0)

“因此,我的问题是,是否可以使用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