gpt4 book ai didi

sql - Oracle SQL 中的有向图使用递归查询仅访问每个节点一次

转载 作者:行者123 更新时间:2023-12-04 15:40:10 25 4
gpt4 key购买 nike

说明

在我们的问题域中,我们正在研究一组连接在一起形成图形的边。
从给定节点(或多个节点)开始,我们必须列出整个图中连接到给定节点(或多个节点)的所有链接。
我们必须从左到右、从上到下显示这些链接。

对于循环次数有限的图,我们有一个针对此问题的有效查询。更多的循环次数会以指数方式增加执行时间。

我们需要在递归期间限制对同一节点的访问以获得有效的解决方案。

下面的示例只包含一个循环,但是这个单个循环已经导致 86 个额外的和过时的行。

在类似的帖子中,使用 ROW 和 ANY 运算符为 postgresql 提供了解决方案,但我找不到 Oracle 解决方案。

我们正在寻找解决方案的替代方案或限制对相同边的访问次数的方法。

任何帮助是极大的赞赏!

类似

Visiting a directed graph as if it were an undirected one, using a recursive query在 postgresql 中提供了一个解决方案。
我们需要使用Oracle11g。

示例

边缘

A-B, B-D, C-A, C-E, C-F, H-F, E-B, G-D, G-I

图形化
    A
/ \
C - E - B - D
\ /
H - F G - I

DDL 和 DML
CREATE TABLE EDGE (
FROM_ID VARCHAR(10),
TO_ID VARCHAR(10)
);

INSERT INTO EDGE VALUES ('A', 'B');
INSERT INTO EDGE VALUES ('E', 'B');
INSERT INTO EDGE VALUES ('C', 'E');
INSERT INTO EDGE VALUES ('C', 'A');
INSERT INTO EDGE VALUES ('C', 'F');
INSERT INTO EDGE VALUES ('B', 'D');
INSERT INTO EDGE VALUES ('G', 'D');
INSERT INTO EDGE VALUES ('H', 'F');
INSERT INTO EDGE VALUES ('G', 'I');

输入
nodes: 'A'

所需输出
C   A
C E
C F
H F
A B
E B
B D
G D
G I

当前解决方案

我们当前的解决方案准确地返回了我们需要的内容,但如上所述,每个额外的循环都会以指数方式增加执行时间。
SELECT
c.LVL,
c.FROM_ID,
c.TO_ID,
CASE
WHEN lag(C.TO_ID)
OVER (
PARTITION BY C.LVL
ORDER BY C.LVL, C.TO_ID ) = C.TO_ID
THEN C.LVL || '-' || C.TO_ID
WHEN lead(C.TO_ID)
OVER (
PARTITION BY C.LVL
ORDER BY C.LVL, C.TO_ID ) = C.TO_ID
THEN C.LVL || '-' || C.TO_ID
ELSE C.LVL || '-' || C.FROM_ID
END GROUP_ID
FROM (
WITH chain(LVL, FROM_ID, TO_ID ) AS (
SELECT
1 LVL,
root.FROM_ID FROM_ID,
root.TO_ID TO_ID
FROM EDGE root
WHERE root.TO_ID IN (:nodes)
OR (root.FROM_ID IN (:nodes) AND NOT EXISTS(
SELECT *
FROM EDGE
WHERE TO_ID IN (:nodes)
))
UNION ALL
SELECT
LVL +
CASE
WHEN previous.TO_ID = the_next.FROM_ID
THEN 1
WHEN previous.TO_ID = the_next.TO_ID
THEN 0
WHEN previous.FROM_ID = the_next.FROM_ID
THEN 0
ELSE -1
END LVL,
the_next.FROM_ID FROM_ID,
the_next.TO_ID TO_ID
FROM EDGE the_next
JOIN chain previous ON previous.TO_ID = the_next.FROM_ID
OR the_next.TO_ID = previous.FROM_ID
OR (previous.TO_ID = the_next.TO_ID AND previous.FROM_ID <> the_next.FROM_ID)
OR (previous.TO_ID <> the_next.TO_ID AND previous.FROM_ID = the_next.FROM_ID)
)
SEARCH BREADTH FIRST BY FROM_ID SET ORDER_ID
CYCLE FROM_ID, TO_ID SET CYCLE TO 1 DEFAULT 0
SELECT
C.*,
row_number()
OVER (
PARTITION BY LVL, FROM_ID, TO_ID
ORDER BY ORDER_ID ) rank
FROM chain C
ORDER BY LVL, FROM_ID, TO_ID
) C
WHERE C.rank = 1;

最佳答案

为了防止遍历算法返回到已经访问过的边,确实可以将访问过的边保留在某处。正如您已经发现的那样,使用字符串连接不会取得太大的成功。但是,还有其他可用的“值串联”技术可用......

您必须拥有一个方便的模式级标量集合供您使用:

create or replace type arr_strings is table of varchar2(64);

然后您可以在每次迭代中将访问过的边收集到该集合中:
with nondirected$ as (
select from_id, to_id, from_id||'-'||to_id as edge_desc
from edge
where from_id != to_id
union all
select to_id, from_id, from_id||'-'||to_id as edge_desc
from edge
where (to_id, from_id) not in (
select from_id, to_id
from edge
)
),
graph$(lvl, from_id, to_id, edge_desc, visited_edges) as (
select 1, from_id, to_id, edge_desc,
arr_strings(edge_desc)
from nondirected$ R
where from_id in (&nodes)
--
union all
--
select
lvl+1,
Y.from_id, Y.to_id, Y.edge_desc,
X.visited_edges multiset union arr_strings(Y.edge_desc)
from graph$ X
join nondirected$ Y
on Y.from_id = X.to_id
where not exists (
select 1
from table(X.visited_edges) Z
where Y.edge_desc = Z.column_value
)
)
search breadth first by edge_desc set order_id
cycle edge_desc set is_cycle to 1 default 0,
ranked_graph$ as (
select C.*,
row_number() over (partition by edge_desc order by lvl, order_id) as rank$
from graph$ C
-- where is_cycle = 0
)
select *
from ranked_graph$
--where rank$ <= 1
order by lvl, order_id
;

笔记
  • 我通过 union 将有向图预处理为无向图-ing 一组输入的反向边。这应该使递归遍历谓词更容易阅读。仅出于我更容易阅读+编写 SQL 的目的。当然,您不必这样做。
  • 我记得几年前在 Oracle 11.2 上尝试过这样的事情。我记得它失败了,虽然我不记得为什么。在 12.2 上,它运行正常。也试试 11g 吧;我没有可用的。
  • 由于每次迭代除了遍历内连接外,还有反连接,我真诚地怀疑这是否会提高性能。但是,它确实解决了减少递归嵌套数量的问题。
  • 正如您从我的评论中可能理解的那样,您必须自己解决所需的排序。 :-)

  • 将重新访问的边限制为零

    在 SQL 中,你不能。您提到的 PostgreSQL 解决方案确实做到了。但是,在 Oracle 中,您不能。对于每个遍历连接,您必须为所有其他遍历连接测试行。这将意味着某种聚合或分析...... Oracle 禁止并抛出 ORA 异常。

    PLSQL 来救援?

    不过,您可以在 PL/SQL 中执行此操作。它应该有多少性能,取决于您想从数据库预取边上花费多少内存,以及您愿意从“当前”节点遍历图形时愿意花费多少 SQL 往返,或者您是否愿意使用甚至更多的内存来将访问的节点保存在一个奇特的按边索引集合中,而如果您宁愿反对常规 arr_output收藏 l_visited_nodes .您有多种选择,请明智地选择。

    无论如何,对于大量使用 SQL 引擎的最简单场景,这可能是您正在寻找的代码......
    create or replace
    package pkg_so_recursive_traversal
    is


    type rec_output is record (
    from_id edge.from_id%type,
    to_id edge.to_id%type,
    lvl integer
    );
    type arr_output is table of rec_output;


    function traverse_a_graph
    ( i_from in arr_strings
    , i_is_directed in varchar2 default 'NO' )
    return arr_output
    pipelined;


    end pkg_so_recursive_traversal;
    /
    create or replace
    package body pkg_so_recursive_traversal
    is


    function traverse_a_graph
    ( i_from in arr_strings
    , i_is_directed in varchar2 )
    return arr_output
    pipelined
    is
    l_next_edges arr_output;
    l_current_edges arr_output;
    l_visited_edges arr_output := arr_output();
    l_out rec_output;
    i pls_integer;
    l_is_directed varchar2(32) := case when i_is_directed = 'YES' then 'YES' else 'NO' end;
    begin
    select E.from_id, E.to_id, 0
    bulk collect into l_next_edges
    from table(i_from) F
    join edge E
    on F.column_value in (E.from_id, case when l_is_directed = 'YES' then null else E.to_id end)
    where E.from_id != E.to_id;

    l_out.lvl := 0;

    loop
    dbms_output.put_line(l_next_edges.count());
    exit when l_next_edges.count() <= 0;
    l_out.lvl := l_out.lvl + 1;

    -- spool the edges to output
    i := l_next_edges.first();
    while i is not null loop
    l_out.from_id := l_next_edges(i).from_id;
    l_out.to_id := l_next_edges(i).to_id;
    pipe row(l_out);
    i := l_next_edges.next(i);
    end loop;

    l_current_edges := l_next_edges;
    l_visited_edges := l_visited_edges multiset union l_current_edges;

    -- find next edges
    select unique E.from_id, E.to_id, 0
    bulk collect into l_next_edges
    from table(l_current_edges) CE
    join edge E
    on CE.to_id in (E.from_id, case when l_is_directed = 'YES' then null else E.to_id end)
    or l_is_directed = 'NO' and CE.from_id in (E.from_id, E.to_id)
    where E.from_id != E.to_id
    and not exists (
    select 1
    from table(l_visited_edges) VE
    where VE.from_id = E.from_id
    and VE.to_id = E.to_id
    );
    end loop;

    return;
    end;


    end pkg_so_recursive_traversal;

    /

    当为 A 的起始节点调用时并考虑到图是无向的......
    select *
    from table(pkg_so_recursive_traversal.traverse_a_graph(
    i_from => arr_strings('A'),
    i_is_directed => 'NO'
    ));

    ......它产生......
    FROM_ID    TO_ID             LVL
    ---------- ---------- ----------
    A B 1
    C A 1
    C E 2
    B D 2
    C F 2
    E B 2
    G D 3
    H F 3
    G I 4

    笔记
  • 同样,我没有努力保持您要求的顺序,因为您说这并不重要。
  • 这是对 edge 执行多次(对于您的示例输入正好是 5 次)SQL 往返。 table 。与具有冗余边缘访问的纯 SQL 解决方案相比,这可能会也可能不会对性能造成更大的影响。正确测试更多解决方案,看看哪一个最适合您。
  • 这段特殊的代码适用于 12c 及更高版本。对于 11g 及更低版本,您必须声明 rec_outputarr_output模式级别的类型。
  • 关于sql - Oracle SQL 中的有向图使用递归查询仅访问每个节点一次,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44799192/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com