Preciso calcular a profundidade de um descendente de seu ancestral. Quando um registro possui object_id = parent_id = ancestor_id
, ele é considerado um nó raiz (o ancestral). Eu tenho tentado obter uma WITH RECURSIVE
consulta em execução com o PostgreSQL 9.4 .
Eu não controlo os dados ou as colunas. O esquema de dados e tabelas vem de uma fonte externa. A tabela está crescendo continuamente . Agora em cerca de 30 mil registros por dia. Qualquer nó na árvore pode estar faltando e eles serão extraídos de uma fonte externa em algum momento. Eles geralmente são extraídos em created_at DESC
ordem, mas os dados são extraídos com trabalhos assíncronos em segundo plano.
Inicialmente, tínhamos uma solução de código para esse problema, mas agora com mais de 5 milhões de linhas, leva quase 30 minutos para ser concluída.
Exemplo de definição de tabela e dados de teste:
CREATE TABLE objects (
id serial NOT NULL PRIMARY KEY,
customer_id integer NOT NULL,
object_id integer NOT NULL,
parent_id integer,
ancestor_id integer,
generation integer NOT NULL DEFAULT 0
);
INSERT INTO objects(id, customer_id , object_id, parent_id, ancestor_id, generation)
VALUES (2, 1, 2, 1, 1, -1), --no parent yet
(3, 2, 3, 3, 3, -1), --root node
(4, 2, 4, 3, 3, -1), --depth 1
(5, 2, 5, 4, 3, -1), --depth 2
(6, 2, 6, 5, 3, -1), --depth 3
(7, 1, 7, 7, 7, -1), --root node
(8, 1, 8, 7, 7, -1), --depth 1
(9, 1, 9, 8, 7, -1); --depth 2
Observe que object_id
não é único, mas a combinação (customer_id, object_id)
é única.
Executando uma consulta como esta:
WITH RECURSIVE descendants(id, customer_id, object_id, parent_id, ancestor_id, depth) AS (
SELECT id, customer_id, object_id, parent_id, ancestor_id, 0
FROM objects
WHERE object_id = parent_id
UNION
SELECT o.id, o.customer_id, o.object_id, o.parent_id, o.ancestor_id, d.depth + 1
FROM objects o
INNER JOIN descendants d ON d.parent_id = o.object_id
WHERE
d.id <> o.id
AND
d.customer_id = o.customer_id
) SELECT * FROM descendants d;
Eu gostaria que a generation
coluna fosse definida como a profundidade que foi calculada. Quando um novo registro é adicionado, a coluna de geração é definida como -1. Existem alguns casos em que um parent_id
pode não ter sido puxado ainda. Se parent_id
não existir, deve deixar a coluna de geração definida como -1.
Os dados finais devem ficar assim:
id | customer_id | object_id | parent_id | ancestor_id | generation
2 1 2 1 1 -1
3 2 3 3 3 0
4 2 4 3 3 1
5 2 5 4 3 2
6 2 6 5 3 3
7 1 7 7 7 0
8 1 8 7 7 1
9 1 9 8 7 2
O resultado da consulta deve ser atualizar a coluna de geração para a profundidade correta.
Comecei a trabalhar com as respostas a esta pergunta relacionada no SO .
A consulta que você tem está basicamente correta. O único erro está na segunda parte (recursiva) do CTE onde tem:
Deveria ser o contrário:
Você deseja juntar os objetos com seus pais (que já foram encontrados).
Assim, a consulta que calcula a profundidade pode ser escrita (nada mais alterado, apenas a formatação):
Para a atualização, basta substituir o último
SELECT
, peloUPDATE
, juntando o resultado do cte, de volta à tabela:Testado no SQLfiddle
Comentários adicionais:
ancestor_id
e theparent_id
não precisam estar na lista de seleção (antepassado é óbvio, pai um pouco complicado para descobrir o porquê), então você pode mantê-los naSELECT
consulta se quiser, mas pode removê-los com segurança do arquivoUPDATE
.(customer_id, object_id)
parece ser um candidato a umaUNIQUE
restrição. Se seus dados estiverem em conformidade com isso, adicione essa restrição. As uniões realizadas no CTE recursivo não fariam sentido se não fossem únicas (caso contrário, um nó poderia ter 2 pais).(customer_id, parent_id)
seria um candidato a umaFOREIGN KEY
restrição queREFERENCES
o (exclusivo)(customer_id, object_id)
. Você provavelmente não deseja adicionar essa restrição FK, pois, pela sua descrição, você está adicionando novas linhas e algumas linhas podem fazer referência a outras que ainda não foram adicionadas.O
AND o.generation = -1
na atualização final garantirá que as linhas que foram atualizadas na 1ª execução não sejam atualizadas novamente, mas o CTE ainda é uma parte cara.A seguir, uma tentativa de resolver esses problemas: melhorar o CTE para considerar o menor número possível de linhas e usar
(customer_id, obejct_id)
em vez de(id)
para identificar linhas (portanto,id
é completamente removido da consulta. Pode ser usado como a 1ª atualização ou uma subsequente:Observe como o CTE tem 3 partes. As duas primeiras são as partes estáveis. A primeira parte encontra os nós raiz que não foram atualizados antes e ainda
generation=-1
devem ser nós adicionados recentemente. A 2ª parte encontra filhos (comgeneration=-1
) de nós pais que foram atualizados anteriormente.A terceira parte, recursiva, encontra todos os descendentes das duas primeiras partes, como antes.
Testado no SQLfiddle-2
@ypercube já fornece ampla explicação, então vou direto ao ponto o que tenho a acrescentar.
Presumo que isso deva ser aplicado recursivamente, ou seja, o restante da árvore sempre ocorre
generation = -1
após qualquer nó ausente.Se algum nó na árvore pode estar faltando (ainda), precisamos encontrar linhas com
generation = -1
isso ...... são nós raiz
... ou têm um pai com
generation > -1
.E atravesse a árvore a partir daí. Os nós filhos desta seleção também devem ter
generation = -1
.Pegue o
generation
do pai incrementado em um ou volte para 0 para os nós raiz:A parte não recursiva é única
SELECT
dessa maneira, mas logicamente equivalente ao two union'ed de @ypercubeSELECT
. Não tenho certeza qual é mais rápido, você terá que testar.O ponto muito mais importante para o desempenho é:
Índice!
Se você adicionar repetidamente linhas a uma tabela grande dessa maneira, adicione um índice parcial :
Isso alcançará mais desempenho do que todas as outras melhorias discutidas até agora - para pequenas adições repetidas a uma grande tabela.
Adicionei a condição de índice à parte recursiva do CTE (embora logicamente redundante) para ajudar o planejador de consulta a entender que o índice parcial é aplicável.
Além disso, você provavelmente também deve ter a
UNIQUE
restrição(object_id, customer_id)
naquele @ypercube já mencionado. Ou, se você não puder impor exclusividade por algum motivo (por quê?) Adicione um índice simples. A ordem das colunas do índice é importante, btw: