Eu tenho uma tabela postgress incoming_records que contém 2 colunas supervisor_id, emp_id
create table incoming_records(supervisor_id,emp_id)as values
(null,01) --top level resources with blank supervisor id
,(01,02)
,(02,03)
,(03,04)
,(04,05)
,(05,06)
,(06,07)
,(07,08)
,(null,10) -- supervisor with 3 employees directly under them
,(10,11)
,(10,12)
,(10,13)
,(14,14) -- loop length 1
,(15,16)
,(16,15) -- loop length 2
,(17,18)
,(18,19)
,(19,17) -- loop length 3
,(20,21)
,(21,22)
,(22,23)
,(23,20) -- loop length 4
,(null,24) -- supervisor with no employees
,(null,25) -- fork, then an employee with 2 supervisors
,(25,26),(26,28)
,(25,27),(27,28)
,(null,29) -- supervisor with a null employee
,(29,null)
,(null,null) --nobody working for nobody
,(99,30) -- somebody working for someone who doesn't exist
,(99,null); -- nobody working for someone who doesn't exist
Esta tabela recebe dados de um aplicativo externo e é totalmente atualizada todos os dias e contém cerca de 60.000 registros. Com base nesses registros, preciso construir uma hierarquia de recursos. Para construir a hierarquia, escrevi uma consulta recursiva e ela funciona bem. Mas às vezes minha consulta recursiva entra em loop infinito devido a dados errados. Veja o cenário abaixo, onde uma dependência circular está sendo criada. Neste caso, a consulta recursiva entra em loop infinito e nunca termina.
supervisor_id | emp_id |
---|---|
rt08 | rt09 |
rt09 | rt08 |
Estou tentando escrever uma consulta (escrita abaixo) que pode descobrir esses registros defeituosos. Mas até agora não tive sucesso.
WITH recursive tmp AS (
SELECT supervisor_id,
emp_id
FROM incoming_records
UNION ALL
SELECT tmp.supervisor_id,
t.emp_id
FROM tmp
INNER JOIN incoming_records AS t
ON t.supervisor_id = tmp.emp_id
WHERE t.supervisor_id = tmp.emp_id)
select * from tmp
Qualquer sugestão será apreciada
A detecção de ciclo é um recurso integrado de CTEs recursivos: demonstração em db<>fiddle
CYCLE
cláusula informa qual valor indica um loop se ele se repetirboolean
campo, preenchido para os registros com loops, depois um campo de matriz que contém todo o caminho/grupo com o ciclo.path
acumula todas as chaves, ele pode crescer rapidamente em conjuntos maiores e mais complexos. Como uma alternativa otimizada, rápida e com uso eficiente de memória, considerepgrouting
.Se você quiser evitá-los em vez de selecionar apenas esses, mude para
WHERE NOT is_cycle;
.A cláusula padrão SQL
CYCLE
para detecção de ciclo integrada foi adicionada na v14 .pgr_connectedComponents()
Em comparação com CTEs recursivos do tipo "faça você mesmo", as funções que vêm na
pgrouting
extensão são executadas mais rapidamente, usando menos memória, graças a anos de desenvolvimento do Boost C++ .Em conjuntos de amostra com alguns milhares de linhas, ele roda algumas vezes mais rápido do que todas as alternativas aqui e é o único aplicável acima desse tamanho. Os CTEs recursivos ficam sem memória, vazam para o disco e, em seguida, ficam sem espaço nos milhares inferiores. Aqui está um teste em 7,5 mil linhas em que ele leva 143 ms , enquanto
pgrouting
termina em 31 ms .Se você aumentar o tamanho da amostra acima disso, o db<>fiddle fica sem recursos necessários para manter o exemplo de CTE recursivo ativo.
Em 50k amostras,
pgrouting
o exemplo abaixo encontra 3899 linhas de grupos com loops em 170 ms :demonstração em db<>fiddle
Note que isso pressupõe que você não pode tornar um funcionário seu próprio supervisor e que cada funcionário pode ter no máximo um supervisor por vez. Neste cenário, é suficiente estabelecer que o supervisor de ponta nula está faltando para determinar que uma árvore contém um loop.
Exibindo o máximo possível até chegar ao loop
Em vez de identificar os loops em uma tarefa separada,
você pode obter um passo a passo à prova de falhas, lembrando-se do caminho para chegar a cada item e parando quando encontrar um funcionário já visto.
A estrutura correta para mantê-lo é a do PostgreSQL
array
:(veja-o tocando violino )
Um trabalho de 2 etapas (primeiro identificando os loops e depois calculando a árvore quando não houver mais loops) exigiria, de qualquer forma, o cálculo do caminho durante o CTE recursivo,
mas:
delete
um dos culpadosCom exibição de culpado
Você pode até mesmo exibir a entrada em loop durante a marcação em vez de filtrar (… e então na próxima iteração, parar a entrada marcada, é claro):
Então você pode usá-lo tanto na sua consulta de produção (onde você filtrará com um
WHERE NOT looping
para manter apenas umrt08
) quanto no seu diagnóstico manual.Limites
Duplicatas
Observe que ainda pode haver duplicatas se você tiver duas maneiras de entrar no loop.
Por exemplo, se 8 e 9 estiverem abaixo de 7, e um abaixo do outro,
você obterá 9 como
7 > 9
e7 > 8 > 9
(o que não é um loop, pois9
ainda não está no caminho; haverá um loop na próxima iteração, com7 > 8 > 9 > 8
).Mas isso não é específico para loops, aqui é um problema de gráfico.
Você pode evitar isso escolhendo um
DISTINCT ON (emp_id) ORDER BY ARRAY_LENGTH(path, 1) DESC
(isso elege o caminho mais curto para o funcionário, mas se dois caminhos levam ao mesmo funcionário (por exemplo, se3 > 5
e4 > 5
), um será eleito arbitrariamente; e os subordinados deste incerto não necessariamente escolherão o mesmo caminho, portanto você pode terminar com3 > 5
para,5
mas4 > 5 > 6
com6
).Nenhum ponto de entrada
Este de cima para baixo requer que seus elementos de loop tenham múltiplos pais, com pelo menos um acessível a partir de um funcionário não supervisionado.
Um loop puro nunca aparecerá nos resultados. (por exemplo: if
8 > 9
e9 > 8
loop juntos com no7 > 8
para conectá-los ao resto do mundo).Neste caso, você não terá outra escolha a não ser ter um "detector de loop puro" separado, como visto na minha resposta dedicada .
Detectando loops puros
Se você tiver loops puros em circuito fechado (sem nenhuma maneira de chegar a um chefe de hierarquia sem um supervisor),
minha abordagem de exibir o máximo possível não funcionará, porque requer um chefe de hierarquia como ponto de entrada.
Nesse caso, você ainda pode usar matrizes do PostgreSQL para construir o caminho de baixo para cima para cada funcionário e
parar assim que encontrar um funcionário já visto: