使用递归查询访问有向图,就好像它是无向图一样

Posted

技术标签:

【中文标题】使用递归查询访问有向图,就好像它是无向图一样【英文标题】:Visiting a directed graph as if it were an undirected one, using a recursive query 【发布时间】:2012-02-04 14:06:23 【问题描述】:

关于访问存储在数据库中的有向图,我需要您的帮助。

考虑以下有向图

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

我已经修改了解释递归查询的example on Postgres' manual,并且我已经将连接条件调整为“向上”和“向下”(这样做我忽略了方向)。我的查询如下:

\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 的情况下,从给定的边开始提取 db 中图的可达边?

我还有另一个问题(这个问题解决了,看底部):从输出中可以看出,循环只有在第二次到达节点时才会停止。 IE。我有(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) &lt;&gt; any(path)) 在再次进入循环边缘之前应该避免循环但不起作用)。 如何在关闭循环的节点前一步停止循环?

编辑:按照 danihp 的建议,解决我使用的第二个问题

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

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

当我发布我的第一个查询时,我不明白我缺少一些连接,因为它发生在以下情况:如果我从节点 3 开始,数据库选择行 (2,3)(3,1) .然后,查询的第一个归纳步骤将从这些行中选择并连接行(1,2)(2,3)(3,1),缺少应该包含在结果中的行 (2,1) 作为概念上的算法将暗示((2,1) 是“接近”(3,1)

当我尝试修改 Postgresql 手册中的示例时,我尝试加入 ownership 的父母和孩子是正确的,但我没有保存每个步骤中必须加入的 graph 的值是错误的。

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

【问题讨论】:

看起来像地图缩减问题 你试过and NOT (ROW(o.parent, o.child) = any(path)) 吗?还不够:select distinct g.parent, g.child from graph g where cycle = f? 谢谢danihp,“而不是ROW”有效!所以第二个问题解决了! (将更新问题)。行数从 13 到 6!但我仍然有重复。你是对的,使用 distinct 输出将是我需要的,但一些不需要的连接仍会在 WITH 查询中,如果你有一个大图,这可能是一个性能问题。我希望尽可能地提高效率,而不是 - 如果可能的话 - 重复,甚至不使用 distinct 你说它是一个“有向图”,但你也是从子图遍历到父图?这是故意的吗? 顺便说一句,更简单的语法:(ROW(o.parent, o.child) &lt;&gt; ALL(path)) 而不是 NOT (ROW(o.parent, o.child) = ANY(path)) 【参考方案1】:

可以这样工作:

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 columns instead of 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) 以这种方式有效地重复...


评论后的补充解决方案

忽略方向 仍然每条路径只能在任何边缘走一次。 使用 ARRAY 作为路径 保存路径中的原始方向,而不是实际方向。

注意,这种方式(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

【讨论】:

乍一看,在我看来你找到了节点的后代,这不是我要求的,但你对字符串的使用是有趣的,你是对的,没有需要一个循环列 我尝试仅在孩子上使用您的条件修改查询,但是,正如您可以从我的编辑 #2 中看到的那样,我获得的行数比以前多。不过,我并不完全清楚为什么。 @cdarwin:你仍然有一个逻辑错误。 g.child in (o.parent, o.child) 建议您按照指示的方向从父母到孩子走图表。但是您允许两个方向,因此您必须明确保存每个步骤的当前终点(父或子)。我为该场景添加了一个解决方案。 你完全正确。我写了一些最后的考虑作为编辑#3。我希望仅使用递归查询就可以找到更有效的解决方案,但我现在明白这是不可能的,正如您所说,必须使用 plpgsql 进行“经典”访问 @erwin-brandstetter,我在哪里可以找到这样大图的 plpgsql 过程示例?谢谢!

以上是关于使用递归查询访问有向图,就好像它是无向图一样的主要内容,如果未能解决你的问题,请参考以下文章

求高手给个遍历算法

路径双覆盖,递归设置

pintia刷题记录——图

如何使用递归 DFS 查找图是不是包含循环?

[NEFU锐格 数据结构]实验五六 图有关的操作

无向图和有向图的详细讲解