J'ai besoin de calculer la profondeur d'un descendant de son ancêtre. Lorsqu'un enregistrement a object_id = parent_id = ancestor_id
, il est considéré comme un nœud racine (l'ancêtre). J'ai essayé de lancer une WITH RECURSIVE
requête avec PostgreSQL 9.4 .
Je ne contrôle pas les données ou les colonnes. Le schéma de données et de table provient d'une source externe. La table ne cesse de croître . À l'heure actuelle, environ 30 000 enregistrements par jour. Tous les nœuds de l'arborescence peuvent être manquants et ils seront extraits d'une source externe à un moment donné. Ils sont généralement extraits dans l' created_at DESC
ordre, mais les données sont extraites avec des tâches d'arrière-plan asynchrones.
Nous avions initialement une solution de code à ce problème, mais ayant maintenant 5M + lignes, cela prend presque 30 minutes pour terminer.
Exemple de définition de table et de données de test:
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
Notez que ce object_id
n'est pas unique, mais la combinaison (customer_id, object_id)
est unique.
Exécuter une requête comme celle-ci:
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;
Je voudrais que la generation
colonne soit définie comme la profondeur qui a été calculée. Lorsqu'un nouvel enregistrement est ajouté, la colonne de génération est définie sur -1. Il y a des cas où un parent_id
n'a peut - être pas encore été retiré. Si le parent_id
n'existe pas, il doit laisser la colonne de génération définie sur -1.
Les données finales devraient ressembler à:
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
Le résultat de la requête doit être de mettre à jour la colonne de génération à la profondeur correcte.
J'ai commencé à travailler à partir des réponses à cette question connexe sur SO .
la source
update
le tableau avec le résultat de votre CTE récursif?ancestor_id
est déjà défini, vous n'avez donc qu'à affecter la génération à partir de CTE.depth?Réponses:
La requête que vous avez est fondamentalement correcte. La seule erreur se trouve dans la deuxième partie (récursive) du CTE où vous avez:
Il devrait être l'inverse:
Vous souhaitez joindre les objets à leurs parents (qui ont déjà été trouvés).
Ainsi, la requête qui calcule la profondeur peut être écrite (rien d'autre n'a changé, seulement le formatage):
Pour la mise à jour, vous remplacez simplement le dernier
SELECT
, par leUPDATE
, en joignant le résultat du cte, de retour au tableau:Testé sur SQLfiddle
Commentaires supplémentaires:
ancestor_id
Ilparent_id
n'est pas nécessaire que le et le soient dans la liste de sélection (l'ancêtre est évident, le parent est un peu difficile à comprendre pourquoi), vous pouvez donc les conserver dans laSELECT
requête si vous le souhaitez mais vous pouvez les supprimer en toute sécurité duUPDATE
.(customer_id, object_id)
semble être un candidat pour uneUNIQUE
contrainte. Si vos données sont conformes à cela, ajoutez une telle contrainte. Les jointures effectuées dans le CTE récursif n'auraient aucun sens si elles n'étaient pas uniques (un nœud pourrait avoir 2 parents sinon).(customer_id, parent_id)
serait candidate à uneFOREIGN KEY
contrainte quiREFERENCES
la (unique)(customer_id, object_id)
. Cependant, vous ne voulez probablement pas ajouter cette contrainte FK, car d'après votre description, vous ajoutez de nouvelles lignes et certaines lignes peuvent en référencer d'autres qui n'ont pas encore été ajoutées.La mise
AND o.generation = -1
à jour finale s'assurera que les lignes qui ont été mises à jour lors de la première exécution ne seront pas mises à jour à nouveau, mais le CTE est toujours une partie coûteuse.Ce qui suit est une tentative pour résoudre ces problèmes: améliorer le CTE de manière à prendre en compte le moins de lignes possible et à utiliser
(customer_id, obejct_id)
au lieu d'(id)
identifier les lignes (ilid
est donc complètement supprimé de la requête. Il peut être utilisé comme première mise à jour ou ultérieure:Notez comment le CTE comprend 3 parties. Les deux premiers sont les parties stables. La 1ère partie trouve les nœuds racine qui n'ont pas été mis à jour auparavant et qui le sont encore
generation=-1
donc ils doivent être des nœuds nouvellement ajoutés. La 2e partie trouve les enfants (avecgeneration=-1
) des nœuds parents qui ont été précédemment mis à jour.La 3ème partie, récursive, retrouve tous les descendants des deux premières parties, comme précédemment.
Testé sur SQLfiddle-2
la source
@ypercube fournit déjà de nombreuses explications, je vais donc aller au bout de ce que je dois ajouter.
Je suppose que cela est censé appliquer récursive, à savoir le reste de l'arbre toujours a
generation = -1
après tout noeud manquant.Si un nœud de l'arborescence peut (encore) être manquant, nous devons trouver des lignes avec
generation = -1
ce ...... sont des nœuds racine
... ou avoir un parent avec
generation > -1
.Et traversez l'arbre à partir de là. Les nœuds enfants de cette sélection doivent avoir
generation = -1
.Prenez le
generation
parent incrémenté de un ou retombez à 0 pour les nœuds racine:La partie non récursive est unique de
SELECT
cette façon, mais logiquement équivalente aux deux unions de @ ypercubeSELECT
. Vous ne savez pas lequel est le plus rapide, vous devrez le tester.Le point beaucoup plus important pour la performance est:
Indice!
Si vous ajoutez à plusieurs reprises des lignes à une grande table de cette façon, ajoutez un index partiel :
Cela permettra d'obtenir plus de performances que toutes les autres améliorations discutées jusqu'à présent - pour de petits ajouts répétés à une grande table.
J'ai ajouté la condition d'index à la partie récursive du CTE (même si elle est logiquement redondante) pour aider le planificateur de requêtes à comprendre que l'index partiel est applicable.
De plus, vous devriez probablement également avoir la
UNIQUE
contrainte sur(object_id, customer_id)
ce @ypercube déjà mentionné. Ou, si vous ne pouvez pas imposer l'unicité pour une raison (pourquoi?), Ajoutez plutôt un index simple. L'ordre des colonnes d'index est important, entre autres:la source
ON objects (customer_id, parent_id, object_id) WHERE generation = -1;
et peut-être un autreON objects (customer_id, object_id) WHERE generation > -1;
. La mise à jour devra également «basculer» toutes les lignes mises à jour d'un index à un autre, donc vous ne savez pas si c'est une bonne idée pour l'exécution initiale de la MISE À JOUR.