使用递归查询访问有向图,仿佛它是一个无向图

12

我需要你关于数据库中存储的有向图的访问方面的帮助。

考虑以下有向图:

1->2 
2->1,3 
3->1

一张表用于存储这些关系:
create database test;
\c test;

create table ownership (
    parent bigint,
    child  bigint,
    primary key (parent, child)
);

insert into ownership (parent, child) values (1, 2);
insert into ownership (parent, child) values (2, 1);
insert into ownership (parent, child) values (2, 3);
insert into ownership (parent, child) values (3, 1);

我想提取从一个节点可达的所有半连接边(即忽略方向的连接边)的图形。也就是说,如果我从parent=1开始,我想得到以下输出

1,2
2,1
2,3
3,1

我正在使用postgresql数据库。
我修改了Postgres手册上的递归查询示例,并调整了连接条件以实现“向上”和“向下”查询(忽略方向)。我的查询语句如下:
\c test

WITH RECURSIVE graph(parent, child, path, depth, cycle) AS (
SELECT o.parent, o.child, ARRAY[ROW(o.parent, o.child)], 0, false
    from ownership o
    where o.parent = 1
UNION ALL
SELECT 
    o.parent, o.child,
    path||ROW(o.parent, o.child), 
    depth+1, 
    ROW(o.parent, o.child) = ANY(path)
    from 
        ownership o, graph g
    where 
        (g.parent = o.child or g.child = o.parent) 
        and not cycle

)
select  g.parent, g.child, g.path, g.cycle
from
    graph g

它的输出如下:
 parent | child |               path                | cycle 
--------+-------+-----------------------------------+-------
      1 |     2 | {"(1,2)"}                         | f
      2 |     1 | {"(1,2)","(2,1)"}                 | f
      2 |     3 | {"(1,2)","(2,3)"}                 | f
      3 |     1 | {"(1,2)","(3,1)"}                 | f
      1 |     2 | {"(1,2)","(2,1)","(1,2)"}         | t
      1 |     2 | {"(1,2)","(2,3)","(1,2)"}         | t
      3 |     1 | {"(1,2)","(2,3)","(3,1)"}         | f
      1 |     2 | {"(1,2)","(3,1)","(1,2)"}         | t
      2 |     3 | {"(1,2)","(3,1)","(2,3)"}         | f
      1 |     2 | {"(1,2)","(2,3)","(3,1)","(1,2)"} | t
      2 |     3 | {"(1,2)","(2,3)","(3,1)","(2,3)"} | t
      1 |     2 | {"(1,2)","(3,1)","(2,3)","(1,2)"} | t
      3 |     1 | {"(1,2)","(3,1)","(2,3)","(3,1)"} | t
(13 rows)

我有一个问题:查询会多次提取相同的边缘,因为它们通过不同的路径到达,我希望避免这种情况。如果我将外部查询修改为:
select  distinct g.parent, g.child from graph

我得到了期望的结果,但WITH查询中仍存在低效率,因为进行了不必要的连接。 那么,是否有一种方法可以从给定的起始点开始,在数据库中提取图的可达边缘,而不使用distinct呢?

我还有另一个问题(这个已经解决,请看底部):从输出结果可以看出,只有当第二次到达节点时,循环才会停止。也就是说,我有(1,2) (2,3) (1,2)我想在再次循环到最后一个节点之前停止循环,即拥有(1,2) (2,3) 我已尝试修改where条件如下。

where
    (g.parent = o.child or g.child = o.parent) 
    and (ROW(o.parent, o.child) <> any(path))
    and not cycle

为了避免访问已经访问过的边,但它并没有生效,我无法理解为什么((ROW(o.parent, o.child) <> any(path))应该在再次进入环之前避免循环,但实际上并没有起作用)。如何在到达关闭循环的节点之前停止循环一步?
where
    (g.parent = o.child or g.child = o.parent) 
    and not (ROW(o.parent, o.child) = any(path))
    and not cycle

现在的输出结果不包含循环。行数从13减少到6,但我仍然有重复项,因此提取所有边缘而不重复且不区分的主要问题仍然存在。使用and not ROW的当前输出结果。

 parent | child |           path            | cycle 
--------+-------+---------------------------+-------
      1 |     2 | {"(1,2)"}                 | f
      2 |     1 | {"(1,2)","(2,1)"}         | f
      2 |     3 | {"(1,2)","(2,3)"}         | f
      3 |     1 | {"(1,2)","(3,1)"}         | f
      3 |     1 | {"(1,2)","(2,3)","(3,1)"} | f
      2 |     3 | {"(1,2)","(3,1)","(2,3)"} | f
(6 rows)

编辑 #2:: 根据Erwin Brandstetter的建议,我修改了查询语句,但如果我没有错的话,所提出的查询结果比我的更多(似乎对我来说ROW比较更清晰,即使我明白字符串比较会更有效率)。 使用新的查询,我得到了20行数据,而我的查询只有6行。

WITH RECURSIVE graph(parent, child, path, depth) AS (
SELECT o.parent, o.child, ARRAY[ROW(o.parent, o.child)], 0
    from ownership o
    where 1 in (o.child, o.parent)
UNION ALL
SELECT 
    o.parent, o.child,
    path||ROW(o.parent, o.child), 
    depth+1
    from 
        ownership o, graph g
    where 
        g.child in (o.parent, o.child) 
        and ROW(o.parent, o.child) <> ALL(path)

)
select  g.parent, g.child from graph g

第三次编辑: 正如Erwin Brandstetter所指出的那样,最后一个查询仍然是错误的,而正确的查询可以在他的回答中找到。

当我发布我的第一个查询时,我没有意识到我缺少一些连接,就像在以下情况下发生的那样:如果我从节点3开始,数据库会选择行(2,3)(3,1)。然后,查询的第一个归纳步骤将选择连接这些行的行(1,2)(2,3)(3,1),错过了应该作为概念上算法暗示的结果包含的行(2,1)(2,1)“接近”(3,1))。

当我试图调整Postgresql手册中的示例时,我尝试连接ownership的父级和子级,但我没有保存必须在每个步骤中连接的graph的值。

这些类型的查询似乎会生成不同的行集,具体取决于起始节点(即基本步骤中选择的行集)。因此,我认为在基本步骤中选择只包含起始节点的一行可能很有用,因为无论如何都会得到任何其他“相邻”的节点。


看起来像是一个 MapReduce 问题。 - Adrian
1
你尝试过使用 and NOT (ROW(o.parent, o.child) = any(path)) 吗?这难道不足以满足需求了吗:select distinct g.parent, g.child from graph g where cycle = f - dani herrera
但这样它就不是一个有向图了。据我理解,“有向”的本质是边的方向,或者我理解错了吗? - Erwin Brandstetter
你说得没错,但我需要的是一种让我能够“查看”给定节点周围的爆炸方式。我的公司之间存在所有权关系,因此在给定一个公司时,我需要它的传入和传出所有权链接。你的查询可能是解决方案,我明天会更好地检查一下。 - cdarwin
1
顺便提一下,更简单的语法是:(ROW(o.parent, o.child) <> ALL(path)) 而不是 NOT (ROW(o.parent, o.child) = ANY(path)) - Erwin Brandstetter
显示剩余3条评论
1个回答

8
可以像这样工作:
WITH RECURSIVE graph AS (
    SELECT parent
          ,child
          ,',' || parent::text || ',' || child::text || ',' AS path
          ,0 AS depth
    FROM   ownership
    WHERE  parent = 1

    UNION ALL
    SELECT o.parent
          ,o.child
          ,g.path || o.child || ','
          ,g.depth + 1
    FROM   graph g
    JOIN   ownership o ON o.parent = g.child
    WHERE  g.path !~~ ('%,' || o.parent::text || ',' || o.child::text || ',%')
    )
SELECT  *
FROM    graph

你提到了性能,所以我朝着这个方向进行了优化。

主要要点:

  • 只在定义的方向中遍历图形。

  • 不需要列cycle,将其作为排除条件。少走一步。这也是直接回答以下问题的方法:

我该怎么做才能在闭合循环节点之前停止循环?

  • 使用字符串记录路径。比行数组更小更快。仍然包含所有必要的信息。但是可能会因非常大的bigint数字而改变。

  • 使用LIKE运算符(~~)检查循环,应该更快。

  • 如果您预计随着时间的推移不会超过2147483647行,请使用普通的integer列,而不是bigint。更小更快。

  • 确保在parent上有一个索引。在我的查询中,对child的索引无关紧要。(除了在您原始的查询中,在两个方向上遍历边缘。)

  • 对于巨大的图形,我会切换到plpgsql过程,在其中可以使用一行每步和匹配索引临时表维护路径。这是一种有点开销的方法,但对于巨大的图形来说,这将得到回报。


您原始查询中的问题:

WHERE (g.parent = o.child or g.child = o.parent) 

在遍历过程中,每个点只有一个终止点。当你沿着有向图双向行走时,终止点可以是父节点或子节点,但不能同时是两者。你需要保存每一步的终止点,然后:

WHERE g.child IN (o.parent, o.child) 

方向的违反也使得你的起始状态成为问题:
WHERE parent = 1

必须是
WHERE 1 IN (parent, child)

而且两行(1,2)(2,1)这样实际上是重复的...


评论后的额外解决方案

  • 忽略方向
  • 每条路径仍然只经过每条边一次。
  • 使用数组来存储路径
  • 在路径中保存原始方向,而不是实际方向。

请注意,这种方法下(2,1)(1,2)是有效重复的,但是两者都可以在同一路径中使用。

我引入了列leaf,它保存每个步骤的实际终点。

WITH RECURSIVE graph AS (
    SELECT CASE WHEN parent = 1 THEN child ELSE parent END AS leaf
          ,ARRAY[ROW(parent, child)] AS path
          ,0 AS depth
    FROM   ownership
    WHERE  1 in (child, parent)

    UNION ALL
    SELECT CASE WHEN o.parent = g.leaf THEN o.child ELSE o.parent END -- AS leaf
          ,path || ROW(o.parent, o.child) -- AS path
          ,depth + 1 -- AS depth
    FROM   graph g
    JOIN   ownership o ON g.leaf in (o.parent, o.child) 
    AND    ROW(o.parent, o.child) <> ALL(path)
    )
SELECT *
FROM   graph

乍一看,我觉得你找到了节点的后代,这不是我要求的,但是你使用字符串的方法很有趣,而且你是对的,不需要循环列。 - cdarwin
我尝试使用您的条件仅修改子查询,但是正如您可以从我的第二次编辑中看到的那样,我获得了比以前更多的行。虽然我不完全清楚原因。 - cdarwin
@cdarwin:你仍然有一个逻辑错误。g.child in (o.parent, o.child)会暗示你按照指定的方向从父节点走到子节点来遍历图形。但是你允许两个方向,所以你必须显式地保存每一步的当前终点(父节点或子节点)。我为这种情况添加了一个解决方案。 - Erwin Brandstetter
你是完全正确的。我在编辑#3中写了一些最终的考虑。我希望能够找到一种更有效的解决方案,只使用递归查询,但现在我明白这是不可能的,正如你所说,必须使用plpgsql来进行“经典”访问。 - cdarwin
1
@erwin-brandstetter,请问我在哪里可以找到一个处理如此大的图形的plpgsql过程的示例?谢谢! - jackb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接