Oracle
 sql >> Base de Dados >  >> RDS >> Oracle

Gráfico direcionado no Oracle SQL usando consulta recursiva visitando cada nó apenas uma vez


Para evitar que o algoritmo de travessia retorne às arestas já visitadas, pode-se de fato manter as arestas visitadas em algum lugar. Como você já descobriu, você não terá muito sucesso com uma concatenação de strings. No entanto, existem outras técnicas úteis de "concatenação de valor" disponíveis ...

Você precisa ter uma coleção acessível de escalares em nível de esquema à sua disposição:
create or replace type arr_strings is table of varchar2(64);

E então você pode coletar as arestas visitadas para essa coleção em cada iteração:
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
;

Observações
  1. Pré-processei o gráfico direcionado para um não direcionado por union -ing um conjunto de arestas reversas para a entrada. Isso deve tornar os predicados de travessia recursiva mais fáceis de ler. Apenas para meus propósitos de facilitar a leitura + escrita do SQL. Você não precisa fazer isso, é claro.
  2. Lembro-me de tentar algo assim alguns anos atrás no Oracle 11.2. E lembro que estava falhando, embora não me lembre por quê. No 12.2, funcionou bem. Experimente também o 11g; Não tenho um disponível.
  3. Como cada iteração faz, além da junção interna transversal, também uma antijunção, duvido sinceramente que isso seja mais eficiente. No entanto, com certeza resolve o problema de diminuir o número de aninhamentos recursivos.
  4. Você terá que resolver o pedido desejado por conta própria, como você provavelmente entendeu pelos meus comentários. :-)

Limitando as arestas revisitadas a zero

No SQL, você não pode. A solução PostgreSQL que você mencionou faz isso. No Oracle, no entanto, você não pode. Você teria que, para cada junção transversal, testar linhas para todas as outras junções transversais. E isso significaria algum tipo de agregação ou análise... que a Oracle proíbe e lança uma exceção ORA.

PLSQL ao resgate?

Você pode fazer isso em PL/SQL, no entanto. Quanto desempenho deve ser, depende de quanta memória você deseja gastar na pré-busca de arestas do banco de dados versus quantas viagens de ida e volta SQL você está disposto a fazer para percorrer o gráfico de nós "atuais" ou se você está disposto a usar ainda mais memória para manter os nós visitados em uma coleção sofisticada indexada por borda versus se você preferir anti-juntar contra um arr_output normal coleção l_visited_nodes . Você tem várias opções, escolha sabiamente.

De qualquer forma, para o cenário mais simples com uso mais pesado do mecanismo SQL, esse pode ser o código que você está procurando...
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;

/

Quando chamado para o nó inicial de A e considerando o gráfico como não direcionado...
select *
from table(pkg_so_recursive_traversal.traverse_a_graph(
        i_from => arr_strings('A'),
        i_is_directed => 'NO'
    ));

... rende...
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

Observações
  1. Mais uma vez, não fiz nenhum esforço para manter o pedido que você solicitou, pois você disse que não é tão importante.
  2. Isso está fazendo várias (exatamente 5 para suas entradas de exemplo) SQL roundtrips para a edge tabela. Isso pode ou não ser um impacto maior no desempenho quando comparado à solução de SQL puro com visita de borda redundante. Teste mais soluções adequadamente, veja qual funciona melhor para você.
  3. Esta parte específica do código funcionará em 12c e superior. Para 11g e inferior, você terá que declarar o rec_output e arr_output tipos no nível do esquema.