当前位置: 首页 > 面试题库 >

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

全丰
2023-03-14
问题内容

描述

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

对于循环数量有限的图形,我们有一个针对此问题的有效查询。循环次数越多,执行时间就成倍增加。

我们需要在递归过程中限制对同一节点的访问,以获取有效的解决方案。

下面的示例仅包含一个循环,但是此循环已引起86个额外的和过时的行。

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

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

任何帮助是极大的赞赏!

例子

边缘

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
;

笔记

  1. 通过将union一组反向边添加到输入中,我将有向图预处理为一个无向图。这应该使递归遍历谓词更易于阅读。仅出于我的目的,以便于更轻松地读取和编写SQL。当然,您不必这样做。
  2. 我记得几年前在Oracle 11.2上尝试过类似的方法。我记得那是失败的,尽管我不记得为什么。在12.2上,它运行正常。也尝试一下11g。我没有一个。
  3. 由于除了遍历内部联接之外,每次迭代都还进行了反联接,因此,我真诚地怀疑这会提高性能。但是,它肯定解决了减少递归嵌套数量的问题。
  4. 正如您可能从我的评论中了解到的那样,您必须自己解决所需的排序。:-)

将重新访问的边限制为零

在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

笔记

  1. 同样,我没有做出任何努力来保持您所请求的顺序,因为您说的并不那么重要。
  2. 这是对edge表的多次SQL往返(对于您的示例输入而言,恰好是5个)。与具有冗余边缘访问功能的纯SQL解决方案相比,这可能会或可能不会对性能造成更大的影响。正确测试更多解决方案,看看哪一种最适合您。
  3. 这段特定的代码将在12c及更高版本上运行。对于11g及更低版本,您必须在架构级别声明rec_outputarr_output类型。


 类似资料:
  • 问题内容: 关于访问存储在数据库中的有向图,我需要您的帮助。 考虑以下有向图 一个表存储这些关系: 我想从一个节点提取图的所有半连接边(即连接边忽略方向) 。即,如果我从parent = 1开始,我希望获得以下输出 我正在使用 postgresql 。 我已经修改了Postgres手册上的示例,该手册解释了递归查询,并且我将联接条件调整为“向上”和“向下”(这样做使我忽略了方向)。我的查询如下:

  • 问题内容: 我有这个架构 样本数据 SQL Fiddle演示。我已经插入了一些示例数据。 查兰芝 我需要找到唱片标题的所有父母。如何仅通过一个查询就可以获取所有父母? 我的意思是我需要这个结果: 期望的输出 假设我想使用其所有父项来获取条目,并且要使用where条件,那么它应该获取上述记录。 问题答案: 演示版

  • 问题内容: 如何在MySql中运行此查询? 它会显示如下错误消息: 问题答案: 该语句/方法适用于PostgreSQL和Sybase(我想可能还有更多),所以也许您可以看一下: http://www.artfulsoftware.com/mysqlbook/sampler/mysqled1ch20.html 它应该向您展示一些使用MySQL的方法(以及PHP中的一两个方法,我只知道它不在您的标签列

  • 我正在使用NetworkX Python库。对我试图解决的问题的更广泛的描述在这里。 我想找到1)至少访问每个节点一次的有效路径和2)基于边权重的至少访问每个节点一次的最短路径。 这听起来像是旅行推销员问题的一个变种。另一个值得注意的是,图几乎是无向的——大多数节点是双向连接的,只有少数几个节点是双向连接的( 我研究了NetworkX算法,但似乎没有一个能满足这个问题。 用于生成图形的代码为: 这

  • 问题内容: 考虑以下简单的DAG: 现在想象一下,我还有其他一些任意准则选择第一个和最后一个边缘,即1-> 2和3-> 4。我想使用这些来查找图表的其余部分。 我可以编写一个递归CTE,如下所示(我使用的是MSDN的术语): 但是,这导致边缘3-> 4被两次选择: 如何防止查询重复出现在已经描述过的子图中?如果在查询的“递归成员”部分中,我可以引用 到目前为止递归CTE检索到的所有数据 (并在递归

  • 问题内容: 我进行了很多搜索,但找不到有用的答案: 我想通过用户给我一个开始和结束日期来列出用户定义的时间段的总计。从开始日期到开始日期之间的每次总计应该相加,并在每一天添加1天。因此最后一行给出了从开始到结束日期的总计。示例:-给定期间=开始2013-01-01,结束= 2013-01-31 所以我有一个查询谁计算所有天: 我有一个查询谁每天计算总数 现在将这两者结合起来很难得到我的最终结果。