Oracle
 sql >> Base de données >  >> RDS >> Oracle

Graphe dirigé dans Oracle SQL utilisant une requête récursive visitant chaque nœud une seule fois

Pour éviter que l'algorithme de traversée ne revienne sur des arêtes déjà visitées, on peut en effet conserver les arêtes visitées quelque part. Comme vous l'avez déjà découvert, vous n'obtiendrez pas beaucoup de succès avec une concaténation de chaînes. Cependant, il existe d'autres techniques utilisables de "concaténation de valeurs"...

Vous devez avoir à votre disposition une collection pratique de scalaires au niveau du schéma :

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

Et ensuite, vous pouvez collecter les arêtes visitées dans cette collection à chaque itération :

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
;

Remarques

  1. J'ai prétraité le graphe dirigé en un graphe non dirigé par union -ing un ensemble de fronts inverses à l'entrée. Cela devrait faciliter la lecture des prédicats de parcours récursifs. Uniquement dans le but de faciliter la lecture et l'écriture du SQL. Vous n'êtes pas obligé de le faire, bien sûr.
  2. Je me souviens avoir essayé quelque chose comme ça il y a quelques années sur Oracle 11.2. Et je me souviens qu'il échouait, même si je ne me souviens pas pourquoi. Le 12.2, ça fonctionnait bien. Essayez-le également sur 11g; Je n'en ai pas de disponible.
  3. Étant donné que chaque itération fait, en plus de la jointure interne transversale, également une anti-jointure, je doute sincèrement que cela soit plus performant. Cependant, cela résout à coup sûr le problème de la réduction du nombre d'imbrications récursives.
  4. Vous devrez résoudre vous-même la commande souhaitée, comme vous l'avez probablement compris d'après mes commentaires. :-)

Limiter les arêtes revisitées à zéro

En SQL, vous ne pouvez pas. La solution PostgreSQL que vous avez mentionnée le fait. Dans Oracle, cependant, vous ne pouvez pas. Vous devrez, pour chaque jointure de traversée, tester les lignes pour toutes les autres jointures de traversée. Et cela signifierait une sorte d'agrégation ou d'analyse... qu'Oracle interdit et rejette une exception ORA.

PLSQL à la rescousse ?

Vous pouvez le faire en PL/SQL, cependant. Le degré de performance qu'il est censé être dépend de la quantité de mémoire que vous souhaitez consacrer à la prélecture des bords de la base de données par rapport au nombre d'allers-retours SQL que vous êtes prêt à effectuer pour parcourir le graphique à partir des nœuds "actuels" ou si vous êtes prêt à utiliser encore plus de mémoire pour conserver les nœuds visités dans une collection fantaisiste indexée par bord par rapport à si vous préférez l'anti-join avec un arr_output normal collection l_visited_nodes . Vous avez plusieurs choix, choisissez judicieusement.

Quoi qu'il en soit, pour le scénario le plus simple avec une utilisation plus intensive du moteur SQL, cela pourrait être le code que vous recherchez...

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;

/

Lorsqu'il est appelé pour le nœud de départ de A et en considérant le graphe comme non orienté...

select *
from table(pkg_so_recursive_traversal.traverse_a_graph(
        i_from => arr_strings('A'),
        i_is_directed => 'NO'
    ));

... ça donne...

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

Remarques

  1. Encore une fois, je n'ai fait aucun effort pour conserver l'ordre que vous avez demandé, car vous avez dit que ce n'était pas si important.
  2. Cela fait plusieurs allers-retours SQL (exactement 5 pour vos exemples d'entrées) vers le edge table. Cela peut ou non être un impact plus important sur les performances par rapport à la solution purement SQL avec une visite de périphérie redondante. Testez plus de solutions correctement, voyez celle qui vous convient le mieux.
  3. Ce morceau de code particulier fonctionnera sur 12c et supérieur. Pour 11g et moins, vous devrez déclarer le rec_output et arr_output types au niveau du schéma.