web-dev-qa-db-fra.com

SQL optimisé pour les arborescences

Comment obtiendriez-vous des données structurées en arborescence à partir d'une base de données offrant les meilleures performances? Par exemple, supposons que vous ayez une hiérarchie de dossiers dans une base de données. Où la ligne de la base de données de dossiers aID, Nom et ParentID columns.

Souhaitez-vous utiliser un algorithme spécial pour obtenir toutes les données en même temps, minimiser le nombre d'appels à la base de données et les traiter en code?

Ou utiliseriez-vous beaucoup d'appels à la base de données et sortez-vous de la structure directement à partir de la base de données?

Peut-être y at-il différentes réponses basées sur x quantité de lignes de base de données, profondeur de hiérarchie ou autre?

Edit : J'utilise Microsoft SQL Server, mais les réponses provenant d'autres perspectives sont également intéressantes.

35
Seb Nilsson

regardez dans le modèle hiérarchique ensembles imbriqués . C'est plutôt cool et utile.

13
Mladen Prajdic

Cela dépend vraiment de la manière dont vous allez accéder à l’arbre.

Une technique intelligente consiste à attribuer à chaque nœud un identifiant de chaîne, l'identifiant du parent étant une sous-chaîne prévisible de l'enfant. Par exemple, le parent pourrait être '01' et les enfants, '0100', '0101', '0102', etc. Ainsi, vous pouvez sélectionner un sous-arbre entier de la base de données à la fois avec:

SELECT * FROM treedata WHERE id LIKE '0101%';

Étant donné que le critère est une sous-chaîne initiale, un index sur la colonne ID accélérerait la requête.

16
Ned Batchelder

Parmi toutes les manières de stocker une arborescence dans un RDMS, les plus courantes sont les listes de contiguïté et les ensembles imbriqués. Les ensembles imbriqués sont optimisés pour les lectures et peuvent extraire une arborescence complète en une seule requête. Les listes d’adjacence sont optimisées pour les écritures et peuvent être ajoutées à une requête simple.

Avec les listes d’adjacence, chaque nœud a une colonne qui fait référence au nœud parent ou au nœud enfant (d’autres liens sont possibles). En utilisant cela, vous pouvez construire la hiérarchie basée sur les relations parent-enfant. Malheureusement, à moins de restreindre la profondeur de votre arbre, vous ne pouvez pas extraire le tout dans une requête et sa lecture est généralement plus lente que sa mise à jour.

Avec le modèle d'ensemble imbriqué, l'inverse est vrai, la lecture est rapide et facile, mais les mises à jour deviennent complexes car vous devez conserver le système de numérotation. Le modèle d'ensemble imbriqué code à la fois la parenté et l'ordre de tri en énumérant tous les noeuds à l'aide d'un système de numérotation basé sur le précommande.

J'ai utilisé le modèle d'ensemble imbriqué et, bien qu'il soit complexe pour la lecture, l'optimisation d'une grande hiérarchie en vaut la peine. Une fois que vous avez fait quelques exercices pour dessiner l’arbre et numéroter les nœuds, vous devriez comprendre.

Mes recherches sur cette méthode ont commencé à cet article: Gestion des données hiérarchiques dans MySQL .

15
Bernard Igiri

Dans le produit sur lequel je travaille, nous avons des arborescences stockées dans SQL Server et utilisons la technique mentionnée ci-dessus pour stocker la hiérarchie d'un nœud dans l'enregistrement. c'est à dire.

tblTreeNode
TreeID = 1
TreeNodeID = 100
ParentTreeNodeID = 99
Hierarchy = ".33.59.99.100."
[...] (actual data payload for node)

Maintenir la hiérarchie est la partie la plus délicate du cours et utilise des déclencheurs. Mais le générer sur un insert/delete/move n'est jamais récursif, car la hiérarchie parent ou enfant contient toutes les informations dont vous avez besoin.

vous pouvez ainsi obtenir tous les descendants des noeuds:

SELECT * FROM tblNode WHERE Hierarchy LIKE '%.100.%'

Voici le déclencheur d'insertion:

--Setup the top level if there is any
UPDATE T 
SET T.TreeNodeHierarchy = '.' + CONVERT(nvarchar(10), T.TreeNodeID) + '.'
FROM tblTreeNode AS T
    INNER JOIN inserted i ON T.TreeNodeID = i.TreeNodeID
WHERE (i.ParentTreeNodeID IS NULL) AND (i.TreeNodeHierarchy IS NULL)

WHILE EXISTS (SELECT * FROM tblTreeNode WHERE TreeNodeHierarchy IS NULL)
    BEGIN
        --Update those items that we have enough information to update - parent has text in Hierarchy
        UPDATE CHILD 
        SET CHILD.TreeNodeHierarchy = PARENT.TreeNodeHierarchy + CONVERT(nvarchar(10),CHILD.TreeNodeID) + '.'
        FROM tblTreeNode AS CHILD 
            INNER JOIN tblTreeNode AS PARENT ON CHILD.ParentTreeNodeID = PARENT.TreeNodeID
        WHERE (CHILD.TreeNodeHierarchy IS NULL) AND (PARENT.TreeNodeHierarchy IS NOT NULL)
    END

et voici le déclencheur de la mise à jour:

--Only want to do something if Parent IDs were changed
IF UPDATE(ParentTreeNodeID)
    BEGIN
        --Update the changed items to reflect their new parents
        UPDATE CHILD
        SET CHILD.TreeNodeHierarchy = CASE WHEN PARENT.TreeNodeID IS NULL THEN '.' + CONVERT(nvarchar,CHILD.TreeNodeID) + '.' ELSE PARENT.TreeNodeHierarchy + CONVERT(nvarchar, CHILD.TreeNodeID) + '.' END
        FROM tblTreeNode AS CHILD 
            INNER JOIN inserted AS I ON CHILD.TreeNodeID = I.TreeNodeID
            LEFT JOIN tblTreeNode AS PARENT ON CHILD.ParentTreeNodeID = PARENT.TreeNodeID

        --Now update any sub items of the changed rows if any exist
        IF EXISTS (
                SELECT * 
                FROM tblTreeNode 
                    INNER JOIN deleted ON tblTreeNode.ParentTreeNodeID = deleted.TreeNodeID
            )
            UPDATE CHILD 
            SET CHILD.TreeNodeHierarchy = NEWPARENT.TreeNodeHierarchy + RIGHT(CHILD.TreeNodeHierarchy, LEN(CHILD.TreeNodeHierarchy) - LEN(OLDPARENT.TreeNodeHierarchy))
            FROM tblTreeNode AS CHILD 
                INNER JOIN deleted AS OLDPARENT ON CHILD.TreeNodeHierarchy LIKE (OLDPARENT.TreeNodeHierarchy + '%')
                INNER JOIN tblTreeNode AS NEWPARENT ON OLDPARENT.TreeNodeID = NEWPARENT.TreeNodeID

    END

un bit supplémentaire, une contrainte de vérification pour empêcher une référence circulaire dans les nœuds d'arborescence:

ALTER TABLE [dbo].[tblTreeNode]  WITH NOCHECK ADD  CONSTRAINT [CK_tblTreeNode_TreeNodeHierarchy] CHECK  
((charindex(('.' + convert(nvarchar(10),[TreeNodeID]) + '.'),[TreeNodeHierarchy],(charindex(('.' + convert(nvarchar(10),[TreeNodeID]) + '.'),[TreeNodeHierarchy]) + 1)) = 0))

Je recommanderais également les déclencheurs pour empêcher plus d'un nœud racine (parent nul) par arbre et pour empêcher les nœuds associés d'appartenir à des TreeID différents (mais ceux-ci sont un peu plus triviaux que ceux ci-dessus.)

Vous voudrez vérifier votre cas particulier pour voir si cette solution fonctionne de manière acceptable. J'espère que cela t'aides!

6
James Orr
4
Gene T

Il existe plusieurs types courants de requêtes sur une hiérarchie. La plupart des autres types de requêtes sont des variantes.

  1. D'un parent, trouvez tous les enfants.

    une. À une profondeur spécifique. Par exemple, étant donné mon parent immédiat, tous les enfants jusqu'à une profondeur de 1 seront mes frères et soeurs.

    b. Au bas de l'arbre.

  2. D'un enfant, trouvez tous les parents.

    une. À une profondeur spécifique. Par exemple, mon parent immédiat est les parents à une profondeur de 1.

    b. À une profondeur illimitée.

Les cas (a) (une profondeur spécifique) sont plus faciles en SQL. Le cas particulier (profondeur = 1) est trivial en SQL. La profondeur non nulle est plus difficile. Une profondeur finie, mais non nulle, peut être réalisée via un nombre fini de jointures. Les cas (b), avec une profondeur indéfinie (du haut vers le bas), sont vraiment difficiles.

Si votre arbre estÉNORME(des millions de nœuds), alors vous êtes dans un monde de blessures, peu importe ce que vous essayez de faire. 

Si votre arbre compte moins d'un million de nœuds, il suffit de tout récupérer en mémoire et de travailler dessus. La vie est beaucoup plus simple dans un monde OO. Il suffit de récupérer les lignes et de construire l'arborescence au fur et à mesure que les lignes sont renvoyées.

Si vous avez un Huge tree, vous avez deux choix.

  • Curseurs récursifs pour gérer l'extraction illimitée. Cela signifie que la maintenance de la structure est O(1) - il suffit de mettre à jour quelques nœuds et vous avez terminé. Cependant, la récupération est O (n * log (n)) car vous devez ouvrir un curseur pour chaque nœud avec des enfants.

  • Des algorithmes intelligents de "numérotation de tas" peuvent coder la parenté de chaque nœud. Une fois que chaque nœud est correctement numéroté, un SQL SELECT trivial peut être utilisé pour les quatre types de requêtes. Les modifications apportées à la structure arborescente nécessitent toutefois de renuméroter les nœuds, ce qui rend le coût d'une modification relativement élevé par rapport au coût de récupération.

2
S.Lott

Si vous avez plusieurs arbres dans la base de données et que vous n'obtenez que l'arborescence complète, je stocke un ID d'arborescence (ou ID de nœud racine) et un ID de nœud parent pour chaque nœud de la base de données, obtenez tous les nœuds d'un ID d’arbre particulier et traitement en mémoire.

Toutefois, si vous voulez extraire des sous-arbres, vous ne pouvez obtenir qu’un sous-arbre d’un ID de nœud parent particulier. Vous devez donc stocker tous les nœuds parents de chaque nœud pour utiliser la méthode ci-dessus ou exécuter plusieurs requêtes SQL lorsque vous descendez dans le répertoire. tree (espérons qu'il n'y a pas de cycles dans votre arbre!), bien que vous puissiez réutiliser la même instruction préparée (en supposant que les nœuds sont du même type et sont tous stockés dans une seule table) pour empêcher la recompilation du code SQL. ne soyez pas plus lent, en effet, avec les optimisations de base de données appliquées à la requête, cela pourrait être préférable. Peut-être envie de lancer des tests pour le savoir.

Si vous ne stockez qu'une seule arborescence, votre question devient une requête d'interrogation de sous-arborescences et la deuxième réponse est appliquée.

1
JeeBee

Je suis un partisan de la méthode simple de stockage d'un identifiant associé à son identifiant parent:

ID     ParentID
1      null
2      null
3      1
4      2
...    ...

Il est facile à maintenir et très évolutif.

1
Galwegian

Google pour "chemin matérialisé" ou "arbres génétiques" ...

1
Thomas Hansen

Dans Oracle, il existe une instruction SELECT ... CONNECT BY pour récupérer des arbres. 

1
Dmitry Khalatov

Ne pas aller au travail pour toutes les situations, mais par exemple donné une structure de commentaires:

ID | ParentCommentID

Vous pouvez également stocker TopCommentID qui représente le commentaire le plus élevé:

ID | ParentCommentID | TopCommentID

TopCommentID et ParentCommentID sont null ou 0 lorsqu'il s'agit du commentaire le plus élevé. Pour les commentaires enfants, ParentCommentID pointe sur le commentaire ci-dessus, et TopCommentID pointe sur le parent le plus élevé.

0
Tom Gullen

Cet article est intéressant car il présente des méthodes de récupération ainsi qu'un moyen de stocker le lignage sous forme de colonne dérivée. La lignée fournit une méthode de raccourci pour extraire la hiérarchie sans trop de jointures.

0
Turnkey