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

问题描述 投票:5回答:1

描述

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

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

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

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

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

我们正在寻找替代解决方案或限制访问相同边缘的方式。

任何帮助是极大的赞赏!

类似

qazxsw poi在postgresql中提供了一个解决方案。我们需要使用Oracle 11g。

边缘

Visiting a directed graph as if it were an undirected one, using a recursive query

图形

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

DDL和DML

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

输入

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
sql oracle11g common-table-expression recursive-query
1个回答
2
投票

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

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

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);

笔记

  1. 我通过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 ; 输入一组反向边缘,将有向图预处理为非定向图。这应该使递归遍历谓词更容易阅读。仅仅是为了我更容易阅读和编写SQL的目的。当然,你不必这样做。
  2. 我记得几年前在Oracle 11.2上尝试类似的东西。我记得它失败了,虽然我不记得为什么。在12.2,它运行正常。也试试11g吧;我没有可用的。
  3. 由于每次迭代都会执行,除了遍历内连接外,还有反连接,我真诚地怀疑这将是更高效的。但是,它肯定解决了降低递归嵌套数量的问题。
  4. 你必须自己解决所需的顺序,正如你可能从我的评论中理解的那样。 :-)

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

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

PLSQL来救援?

但是,您可以在PL / SQL中执行此操作。它应该具有多大的性能,取决于您希望从DB预取边缘所花费的内存量与您愿意从“当前”节点遍历图形所花费的SQL往返次数,或者您是否愿意使用甚至更多的内存来保持被访问的节点在一个花哨的索引边缘集合中,而不是你反对加入常规的union集合arr_output。你有多种选择,明智地选择。

无论如何,对于使用较多SQL引擎的最简单方案,这可能是您正在寻找的代码......

l_visited_nodes

当调用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'
    ));

笔记

  1. 同样,我没有付出任何努力来保持您要求的订单,因为您说它并不重要。
  2. 这是对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 表执行多次(对于您的示例输入正好为5)SQL往返。与具有冗余边缘访问的纯SQL解决方案相比,这可能会或可能不会成为更大的性能损失。正确测试更多解决方案,看看哪一个最适合您。
  3. 这段特殊代码将在12c及更高版本上运行。对于11g及更低版本,您必须在架构级别声明edgerec_output类型。
© www.soinside.com 2019 - 2024. All rights reserved.