使用递归查询访问有向图,就好像它是无向图一样
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) <> 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) <> 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 过程示例?谢谢!以上是关于使用递归查询访问有向图,就好像它是无向图一样的主要内容,如果未能解决你的问题,请参考以下文章