回答

收藏

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

技术问答 技术问答 258 人阅读 | 0 人回复 | 2023-09-14

描述
& A7 s5 G% V0 {1 _( Q3 q4 X在我们的问题域中,我们正在研究一组连接在一起以形成图形的边。从给定的一个或多个节点开始,我们必须列出整个图中连接到给定的一个或多个节点的所有链接。我们必须从左到右,从上到下显示这些链接。
$ ?  `! s! B* E% k" N/ Z对于循环数量有限的图形,我们有一个针对此问题的有效查询。循环次数越多,执行时间就成倍增加。* m2 G/ F# p2 \% f
我们需要在递归过程中限制对同一节点的访问,以获取有效的解决方案。
9 E9 f$ f8 d# e8 T2 B2 x$ v" q下面的示例仅包含一个循环,但是此循环已引起86个额外的和过时的行。! m% w# P: l6 T& P  W" E
在类似的帖子中,提供了使用ROW和ANY运算符的postgresql解决方案,但是我找不到Oracle解决方案。
* r2 Z( i& U5 E% D; J# x9 o% h我们正在寻找解决方案的替代方案或将访问次数限制在同一边缘的方法。
. O: c! Z" P$ |) N; g任何帮助是极大的赞赏!2 x! E* T$ R8 u) E( H( ?
例子* |. K" I$ ~) E
边缘  H$ I5 P% \: j5 z7 t
A-B, B-D, C-A, C-E, C-F, H-F, E-B, G-D, G-I5 w0 t$ W0 s/ O0 k) R0 S
图形化, t) x5 K/ A9 C  y0 @; g  M
    A3 Y$ }5 z" ~2 k+ ?6 z
  /   \
& s- s+ F: i# S1 q: ~, h" yC - E - B - D
' C" a, r1 `. S# ^- o  \       /7 J+ K- x9 j" ?7 D6 n
H - F   G - I: w- k& U: q1 ]2 v
DDL和DML! D! {4 E$ _7 ]/ i5 K+ {
CREATE TABLE EDGE (
; L) B( {4 z" Q: e2 v) S& Z5 l  FROM_ID VARCHAR(10),
* Y0 i; N! m; f1 s6 _. X# l  TO_ID   VARCHAR(10)
. q, C# A0 K+ g! A0 V7 c+ L);. u5 a$ R- Y4 w
INSERT INTO EDGE VALUES ('A', 'B');  A  i0 i8 a2 T5 J) g9 D
INSERT INTO EDGE VALUES ('E', 'B');
% `6 O$ _- r' b3 J. zINSERT INTO EDGE VALUES ('C', 'E');
' b% s* }) t" v' DINSERT INTO EDGE VALUES ('C', 'A');
* V; N9 V4 ~/ g% k+ {( U7 TINSERT INTO EDGE VALUES ('C', 'F');
) \- B# [7 e& ~5 B$ gINSERT INTO EDGE VALUES ('B', 'D');9 x1 ]) o1 L, V! ?1 {6 Y: a# {" ~% N
INSERT INTO EDGE VALUES ('G', 'D');
, }; ~: f9 N" aINSERT INTO EDGE VALUES ('H', 'F');
; @3 L6 g; a" H. c; T( h8 b( C2 iINSERT INTO EDGE VALUES ('G', 'I');
5 p6 @5 ^5 O7 ?2 j0 F% }输入  A: `0 `, A" ~/ K+ Y
nodes: 'A'/ O; q- M. P, H
要求的输出9 D. H  z% |1 _) L# ^9 H% M
C   A9 Q7 o3 _* r1 L; Z8 g- m
C   E! K2 L4 Y* @7 v8 C; l; w  o
C   F
$ }2 ~" h( `  jH   F$ [6 _9 R- j& u" g' q
A   B
4 j& s4 H9 H/ Z# zE   B) \3 ^. {9 p: n1 `; Q/ C+ m  ?
B   D' O: j, v6 E2 x+ G
G   D; ^2 t, q- P9 V0 K
G   I
4 w: u  [0 d0 s) g当前解决方案9 C5 L$ J8 P9 K: v' u
我们当前的解决方案完全返回了我们所需的内容,但是如上所述,每个附加循环都会以指数方式增加执行时间。
/ T  O( P; J- {* LSELECT+ A+ F# [5 m. z( O# G: H: Z: s9 S
  c.LVL,9 }: k0 i% V% A
  c.FROM_ID,/ _* K3 `+ e5 X
  c.TO_ID,
' T+ x! z# S" q+ g  W  CASE
5 t! _! Q& [9 e& s6 \  WHEN lag(C.TO_ID)8 Q) ^# Q+ [( w- i
       OVER (
' Q% n8 O! c3 H: V# B4 a         PARTITION BY C.LVL3 n# I2 x5 Z6 j9 \
         ORDER BY C.LVL, C.TO_ID ) = C.TO_ID
- C0 |, t( c8 x6 U2 V, [0 B    THEN C.LVL || '-' || C.TO_ID
: r7 O5 D8 u0 l8 F3 D  WHEN lead(C.TO_ID)
# {5 R9 N( w+ I8 h7 D1 H1 m: K       OVER (
% e1 y. N. H- S% M         PARTITION BY C.LVL. L5 @5 r+ J% ]/ I& Y2 t4 C
         ORDER BY C.LVL, C.TO_ID ) = C.TO_ID7 l% S8 j; r6 {1 G
    THEN C.LVL || '-' || C.TO_ID
9 m+ d/ R; b2 g% @' _7 b+ S& O! b1 I3 Q  ELSE C.LVL || '-' || C.FROM_ID
, O6 h5 c  L- A" f# O  END GROUP_ID
2 l4 R, M& K; X( Y. WFROM (
* Q, }0 W0 ~( s- V4 f6 E       WITH chain(LVL, FROM_ID, TO_ID ) AS (
! c' r7 K* o5 i, U# D/ P% _. q         SELECT8 x. S& i) f! V$ G8 n* D- C: k
           1            LVL,
& c) o7 b- {* V! r! _           root.FROM_ID FROM_ID,
2 \7 W# Y# m: X: Q( g* |           root.TO_ID   TO_ID
( v: k. H: R3 ]% n2 D) x: J5 }         FROM EDGE root# q0 |3 G. ]  L5 _4 c
         WHERE root.TO_ID IN (:nodes). t5 G0 n: u# u7 p- I- ?4 C7 q2 s
               OR (root.FROM_ID IN (:nodes) AND NOT EXISTS(
% p7 R( d# b% \- Q. K             SELECT *0 @4 I. f* \1 e
             FROM EDGE& w( L6 H$ W- }4 T' T$ p5 ^! G* z
             WHERE TO_ID IN (:nodes)' J1 O+ h) Q2 y0 t" P
         ))
1 j$ A& u$ k: v6 W; R         UNION ALL
4 ]7 f& |  v) B: W, V  m         SELECT6 u1 e5 R7 j2 {
           LVL +9 Y; V4 Y9 J. i' C7 `/ H, @
           CASE' |  Y" V0 U. B" a, Z
           WHEN previous.TO_ID = the_next.FROM_ID
' v0 |0 T7 i" Y& T6 E8 H             THEN 1; l  t$ l$ k& `7 ]. G
           WHEN previous.TO_ID = the_next.TO_ID
: y. ]- ?3 s! Y8 V$ V1 ^8 }# l             THEN 0
3 d, l, a% D; m6 g           WHEN previous.FROM_ID = the_next.FROM_ID
! }3 ?% k# s  g) H) Y             THEN 0+ k5 ]4 {7 c2 `7 O# g
           ELSE -1( i5 }6 E, [7 X, f0 ~" W
           END              LVL,
& E2 S) @3 F/ I! U, n           the_next.FROM_ID FROM_ID,
4 |$ W& w) f3 w9 U& y# A  s- _           the_next.TO_ID   TO_ID
* ?8 I  W6 v" x& b: G         FROM EDGE the_next
; v8 x  z4 t* u, z+ p- h           JOIN chain previous ON previous.TO_ID = the_next.FROM_ID
5 Y. V6 l) H  t6 L5 l                                  OR the_next.TO_ID = previous.FROM_ID' k+ [* x9 A  C; f
                                  OR (previous.TO_ID = the_next.TO_ID AND previous.FROM_ID  the_next.FROM_ID)
% @( I+ R$ M% Q                                  OR (previous.TO_ID  the_next.TO_ID AND previous.FROM_ID = the_next.FROM_ID)
# ~6 m* @9 Z% v& Z) j: Z4 h1 K1 Z       )
( v8 o7 M4 |  z+ V* o8 N8 N+ p$ f, r         SEARCH BREADTH FIRST BY FROM_ID SET ORDER_ID
2 M9 J4 [5 A& b* r7 v% w         CYCLE FROM_ID, TO_ID SET CYCLE TO 1 DEFAULT 0
; x7 W+ \7 f) w/ |       SELECT
9 M3 B% [/ Q; K9 y! g         C.*,1 P4 u" A  c# Q0 Z
         row_number()$ k  @7 \) g6 Z& K  I
         OVER (
( Z6 R, S0 A  E7 ]% a7 k1 q           PARTITION BY LVL, FROM_ID, TO_ID( B8 @. j5 _9 k. [- m2 G
           ORDER BY ORDER_ID ) rank
1 E/ \8 i, E9 n' |0 ?       FROM chain C
( g# c+ W+ L7 I, l       ORDER BY LVL, FROM_ID, TO_ID4 L, Q1 t* c: V
     ) C3 k* b- q. z" U1 Q
WHERE C.rank = 1;$ d/ I' P8 K7 x9 M; \0 E
               
1 j' a8 H4 S4 ]3 a: j2 C7 n解决方案:
) }% p& o8 {" ?1 f. F- N' [+ O                ; Z/ v2 @2 [+ d# h
1 J% I4 ^# p  i! E0 k+ V4 [
" r) c% z0 W9 I% i1 e- w' w4 ]4 \
                为了使遍历算法不返回到已经访问过的边缘,实际上可以将访问过的边缘保持在某处。正如您已经发现的那样,使用字符串连接不会取得很大的成功。但是,还有其他可用的“值级联”技术可用…+ c* J$ \8 M  I3 c- s2 b, H5 K
您必须拥有一个方便的架构级标量集合供您使用:( d1 l, ^4 ]) x) W% v* Y( c3 c/ R
create or replace type arr_strings is table of varchar2(64);
' F6 L  P( t$ c7 G# b6 Z然后,您可以在每次迭代中将访问的边收集到该收集中:7 q; s& C) @& L' l
with nondirected$ as (6 P; F0 |' E% _2 e& D, N3 s# |
    select from_id, to_id, from_id||'-'||to_id as edge_desc
! S+ Q; }4 Q' k; b5 |    from edge
( R7 E; Q5 D+ ~2 Z) m    where from_id != to_id
$ N. h$ o' E4 ?  @. N    union all
6 A  c( q3 [, z9 B6 ]( ^    select to_id, from_id, from_id||'-'||to_id as edge_desc% _  x$ f# b- @( i
    from edge
+ V4 ]% D3 @2 c! ^' G: ?) \    where (to_id, from_id) not in (
* g  v- Z$ y8 e" f- V8 |1 @            select from_id, to_id
& w5 n, b* s+ V7 D7 S4 C- v            from edge" i3 q# F' M% G- A' x
        )
  T4 e, |- x, M" z' B0 A  `),
# _( K2 Y9 a0 _6 S5 ?1 F. Dgraph$(lvl, from_id, to_id, edge_desc, visited_edges) as (
. I8 X& A$ V, E, H* a: v    select 1, from_id, to_id, edge_desc,% f: L$ T, Q8 ^& _9 |
        arr_strings(edge_desc)
6 C1 l+ ^: n0 }    from nondirected$ R
/ Q/ M5 X7 t- K9 J( I    where from_id in (&nodes)1 t! A$ Z4 a/ [3 O
    --( k; o  _) u- v
    union all( W: X: D, s. G8 ]5 D3 t
    --; N. T" C3 M, x/ Y: ?5 M9 W
    select1 b% _/ K( I- n9 f1 {5 z
        lvl+1,
" \# R3 E0 _2 Y8 V        Y.from_id, Y.to_id, Y.edge_desc,8 \  T' ?! y0 z3 u  z' l# K* n
        X.visited_edges multiset union arr_strings(Y.edge_desc)0 |& I$ v0 ~' Y
    from graph$ X% g# J2 [! E- h' K, K1 O: h% C
        join nondirected$ Y8 Z6 `2 E. |1 a( g7 B1 w
            on Y.from_id = X.to_id
8 {- i7 P0 i  e5 D) P    where not exists (
: J* a/ ^, Q: d: ^6 z& P$ e9 f( y            select 1
+ a) n+ M6 \/ d# u+ ]            from table(X.visited_edges) Z
0 b) S. T4 }6 u; e7 Y) a            where Y.edge_desc = Z.column_value1 e- z2 U* E: V& N4 f1 c4 h, y8 ^
        )
' F, r" P- h/ u; r9 J)
# {. D8 v9 u* r5 r. l$ Isearch breadth first by edge_desc set order_id
6 f" o) E% @8 C# R" g- v- X& T    cycle edge_desc set is_cycle to 1 default 0,  X) j8 {' L: C+ X2 E
ranked_graph$ as (- R! X0 [; U) m) ^8 R
    select C.*,+ {) r" X# y0 L! W6 w! L
        row_number() over (partition by edge_desc order by lvl, order_id) as rank$) H2 y) I$ P8 U& F
    from graph$ C
& g: _" Q* ]0 R& k+ W* ^--    where is_cycle = 0
5 c2 v0 T: z2 ]* r( m  K): U- h7 x$ I6 D# U! e- W6 E9 ?
select ** h! n" m  a: i! S5 c( }7 a6 e
from ranked_graph$/ z* B7 P  b9 J+ g
--where rank$ 笔记
/ {+ {4 ]8 _0 ]3 |# Z/ d5 T$ @# a, M  P[ol]通过将union一组反向边添加到输入中,我将有向图预处理为一个无向图。这应该使递归遍历谓词更易于阅读。仅出于我的目的,以便于更轻松地读取和编写SQL。当然,您不必这样做。
7 K/ I! e7 b- f2 r我记得几年前在Oracle 11.2上尝试过类似的方法。我记得那是失败的,尽管我不记得为什么。在12.2上,它运行正常。也尝试一下11g。我没有一个。  `+ |8 i! D7 S* v3 ?3 ]: y. e
由于除了遍历内部联接之外,每次迭代都还进行了反联接,因此,我真诚地怀疑这会提高性能。但是,它肯定解决了减少递归嵌套数量的问题。
  t7 d' j- y  Y9 y4 Y1 H正如您可能从我的评论中了解到的那样,您必须自己解决所需的排序。:-)
6 d  A; R" \1 }9 q9 P; ?, A[/ol]
' J: H; E3 w0 {7 D+ [# f将重新访问的边限制为零$ y% ~* E* J3 j4 i9 y2 S  Q
在SQL中,您不能这样做。您提到的PostgreSQL解决方案可以做到这一点。但是,在Oracle中,您不能这样做。对于每个遍历联接,您都必须测试所有其他遍历联接的行。这将意味着某种聚合或分析……Oracle禁止并抛出ORA异常。
- v& \: S; X: Q, I2 R3 }PLSQL可以解救吗?2 b5 W& \: Z# ]- b* |5 e
不过,您可以在PL /
4 Z* x0 ~/ p3 \  m+ iSQL中执行此操作。它应该具有多少性能,取决于要在数据库中预取边缘时要花费多少内存,以及愿意从“当前”节点遍历图时要花费多少SQL往返次数,或者是否愿意使用与您宁愿不加入常规arr_output集合的情况相比,它还有更多的内存可将访问的节点保持在按边索引的精美索引中l_visited_nodes。您有多种选择,明智地选择。/ H9 v' Y, {2 }! ^! D5 S/ g, E  p
无论如何,对于最大量使用SQL引擎的最简单情况,这可能是您正在寻找的代码…
. w% H2 W' k- m/ I8 icreate or replace7 U5 R7 h2 O6 q( @; ^' i' L9 U2 N
package pkg_so_recursive_traversal
5 j. W5 `- J* `, a2 C, W& Qis
1 F& _+ [4 R4 N0 K$ @  S/ T% H' D* r% n/ P' x! h
type rec_output                     is record (* T" U) J( p3 Z6 V& d! E. c
    from_id                             edge.from_id%type,5 \- _6 L0 J* w
    to_id                               edge.to_id%type,# @- ?& C4 B9 o, O6 V, f
    lvl                                 integer
) p' |1 }  }) V" k1 f0 j9 G7 q);
3 P! \) i' h- O* m* f, Itype arr_output                     is table of rec_output;! b5 _0 M2 Y9 i3 Z4 G3 M

% [6 O* i0 g  ?' |function traverse_a_graph
6 Y8 t! O; d. L    ( i_from                        in arr_strings6 Y$ Q- X) R- E: v! E) ]
    , i_is_directed                 in varchar2 default 'NO' )
/ K, [. }- ~6 H. ^- m    return arr_output* V" Y& M3 f( H  [* V% B
    pipelined;
9 h, j0 q  D, |. q2 t6 G
: F" J' Y, c1 E; kend pkg_so_recursive_traversal;7 d- Z7 Z$ z  b! P
/7 L7 D4 u+ S0 \  t  |
create or replace
( a1 n* Q% @" ?package body pkg_so_recursive_traversal+ Y5 h! a6 g, }2 c" N1 z% H
is0 @& m" |# l& i& ?
& o5 u; `2 {+ @) K
function traverse_a_graph+ v) A0 ~$ M8 X8 l
    ( i_from                        in arr_strings$ e% n: U& a( B+ s6 m& k3 ^7 e4 w# o
    , i_is_directed                 in varchar2 )! @& F0 F& Y" M: X2 R9 [
    return arr_output0 R& D$ U5 G: r8 @4 Q: p
    pipelined, {' p! w; |" t% U
is" m8 V0 g! K1 b" q: _! X
    l_next_edges                    arr_output;
4 R$ h/ E- x, n$ k    l_current_edges                 arr_output;
  }7 q% R+ e0 t2 S  X    l_visited_edges                 arr_output := arr_output();
! P" L: n/ z% [( f    l_out                           rec_output;
* A1 t/ F8 ~  I0 U" t  q" B    i                               pls_integer;
+ v) m! p7 s/ T, F5 ?    l_is_directed                   varchar2(32) := case when i_is_directed = 'YES' then 'YES' else 'NO' end;
: c2 g' Z, }1 Q" Gbegin% L8 d9 e+ F5 E( i: M
    select E.from_id, E.to_id, 06 Z) r2 j( u: j) f5 S
    bulk collect into l_next_edges
6 |+ k9 ^, O1 s( z    from table(i_from) F( U9 a1 e# v/ Y! u# e- i
        join edge E
( T/ x" a3 M0 G3 z/ X3 a" y- n" R            on F.column_value in (E.from_id, case when l_is_directed = 'YES' then null else E.to_id end)
0 _6 F, u4 m2 u5 A- ~+ ^8 t: K    where E.from_id != E.to_id;0 V9 K* ^" n3 \2 B' v+ B2 P
    l_out.lvl := 0;
' ~1 ?, {0 g# x. ]. X* `. I- H    loop6 i3 ]8 e! U5 I5 e5 _: d6 ~2 Q2 |& q& ~
        dbms_output.put_line(l_next_edges.count());5 S6 y8 H3 C8 A) h0 C0 Z7 n
        exit when l_next_edges.count() 当调用的起始节点A并考虑图形是无向的时…7 m) A6 ]( W+ B; n8 `% v
select *
6 q0 t# J! [/ @. I% `; D6 ]from table(pkg_so_recursive_traversal.traverse_a_graph(
* L4 r5 G$ r! _8 F2 Q7 g        i_from => arr_strings('A'),
, L0 j' e9 I* Z5 X        i_is_directed => 'NO'
! @& L/ f: b; o  C/ G    ));
) \7 b% I, K% |. ~- D% X…它产生…5 g* v$ W( \' q4 R: t8 ^
FROM_ID    TO_ID             LVL: Q4 F2 K" I( ?% b/ J$ f
---------- ---------- ----------
* [& @- f/ I5 W, r8 gA          B                   1, X* u1 R. R7 a8 e1 [8 `
C          A                   13 X1 ]- b) w- c& ?5 V
C          E                   2
2 m6 z2 [  p, k% F5 [0 H/ K) VB          D                   2
0 s* F+ f  F# C& ?5 f0 H/ lC          F                   2
6 L- O( V3 P+ Q+ _- z! WE          B                   2
6 C1 {, |! P' w2 dG          D                   3
' h6 i$ q; i. f+ b7 q" MH          F                   3- t( H$ t* Z: O0 D& ~6 {+ R, _
G          I                   4+ b: K) J3 N, m3 n" y
笔记3 c, c% ]9 {' {3 j3 a0 }2 G2 W
[ol]同样,我没有做出任何努力来保持您所请求的顺序,因为您说的并不那么重要。
% k) n; t7 L) }9 y" Z8 ~, ^% T这是对edge表的多次SQL往返(对于您的示例输入而言,恰好是5个)。与具有冗余边缘访问功能的纯SQL解决方案相比,这可能会或可能不会对性能造成更大的影响。正确测试更多解决方案,看看哪一种最适合您。0 d4 I$ K+ S, c
这段特定的代码将在12c及更高版本上运行。对于11g及更低版本,您必须在架构级别声明rec_output和arr_output类型。
2 c# ]7 o! H# G  K- n[/ol]
分享到:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则