web-dev-qa-db-fra.com

Quel est le moyen le plus efficace / élégant d’analyser une table plate dans un arbre?

Supposons que vous ayez une table plate qui stocke une hiérarchie arborescente ordonnée:

Id   Name         ParentId   Order
 1   'Node 1'            0      10
 2   'Node 1.1'          1      10
 3   'Node 2'            0      20
 4   'Node 1.1.1'        2      10
 5   'Node 2.1'          3      10
 6   'Node 1.2'          1      20

Voici un diagramme où nous avons [id] Name. Le nœud racine 0 est fictif.

 [0] ROOT 
/\ 
 [1] Node 1 [3] Node 2 
/\\
 [2] Node 1.1 [6] Node 1.2 [5] Node 2.1 
/
 [4] Node 1.1.1 

Quelle approche minimaliste utiliseriez-vous pour générer cela en HTML (ou en texte, d'ailleurs) sous forme d'arborescence correctement ordonnée et correctement mise en retrait?

Supposons en outre que vous n’avez que des structures de données de base (tableaux et hashmaps), aucun objet de fantaisie avec références parent/enfant, aucun ORM, aucun cadre, seulement vos deux mains. La table est représentée sous la forme d'un ensemble de résultats auquel il est possible d'accéder de manière aléatoire.

Le pseudo-code ou l’anglais clair est correct, c’est une question purement conceptuelle.

Question bonus: Existe-t-il un meilleur moyen de stocker une telle arborescence dans un SGBDR?


MODIFICATIONS ET AJOUTS

Pour répondre à la question d'un commentateur ( Mark Bessey ): Un nœud racine n'est pas nécessaire, car il ne sera jamais affiché de toute façon. ParentId = 0 est la convention pour exprimer "ce sont le plus haut niveau". La colonne Order définit comment les nœuds avec le même parent vont être triés.

Le "jeu de résultats" dont j'ai parlé peut être décrit comme un tableau de hashmaps (pour rester dans cette terminologie). Pour mon exemple était censé être déjà là. Certaines réponses vont un peu plus loin et le construisent d’abord, mais c’est bien.

L'arbre peut être arbitrairement profond. Chaque nœud peut avoir N enfants. Je n'avais pas exactement un arbre "des millions d'entrées" à l'esprit, cependant.

Ne confondez pas mon choix de dénomination de nœud ('Node 1.1.1') avec quelque chose sur lequel compter. Les nœuds pourraient tout aussi bien s'appeler 'Frank' ou 'Bob', aucune structure de nommage n'est impliquée, c'était simplement pour le rendre lisible.

J'ai posté ma propre solution pour que vous puissiez la mettre en pièces.

501
Tomalak

Maintenant que MySQL 8.0 est sur le point d'être publié, toutes les bases de données SQL populaires prendront en charge les requêtes récursives en syntaxe standard.

WITH RECURSIVE MyTree AS (
    SELECT * FROM MyTable WHERE ParentId IS NULL
    UNION ALL
    SELECT m.* FROM MyTABLE AS m JOIN MyTree AS t ON m.ParentId = t.Id
)
SELECT * FROM MyTree;

J'ai testé les requêtes récursives dans MySQL 8.0 dans ma présentation Thrashdown de requêtes récursives en 2017.

Voici ma réponse originale de 2008:


Il existe plusieurs façons de stocker des données structurées en arborescence dans une base de données relationnelle. Ce que vous montrez dans votre exemple utilise deux méthodes:

  • Adjacency List (la colonne "parent") et
  • Énumération du chemin (les nombres en pointillés dans la colonne de votre nom).

Une autre solution s'appelle jeux imbriqués et peut également être stockée dans la même table. Lisez " Arbres et Hiérarchies dans SQL pour Smarties " par Joe Celko pour plus d’informations sur ces conceptions.

Je préfère généralement un modèle appelé Closure Table (ou "relation d'adjacence") pour stocker des données structurées en arborescence. Cela nécessite une autre table, mais interroger les arbres est assez facile.

Je couvre Closure Table dans ma présentation Modèles pour les données hiérarchiques avec SQL et PHP et dans mon livre Antipatternes SQL: éviter les pièges de la programmation par base de données .

CREATE TABLE ClosureTable (
  ancestor_id   INT NOT NULL REFERENCES FlatTable(id),
  descendant_id INT NOT NULL REFERENCES FlatTable(id),
  PRIMARY KEY (ancestor_id, descendant_id)
);

Stockez tous les chemins dans la table de fermeture, où il existe une ascendance directe d'un noeud à un autre. Inclure une ligne pour chaque nœud pour se référencer. Par exemple, en utilisant l'ensemble de données que vous avez montré dans votre question:

INSERT INTO ClosureTable (ancestor_id, descendant_id) VALUES
  (1,1), (1,2), (1,4), (1,6),
  (2,2), (2,4),
  (3,3), (3,5),
  (4,4),
  (5,5),
  (6,6);

Vous pouvez maintenant obtenir un arbre commençant au noeud 1 comme ceci:

SELECT f.* 
FROM FlatTable f 
  JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1;

La sortie (dans le client MySQL) a l'aspect suivant:

+----+
| id |
+----+
|  1 | 
|  2 | 
|  4 | 
|  6 | 
+----+

En d'autres termes, les nœuds 3 et 5 sont exclus, car ils font partie d'une hiérarchie distincte et ne descendent pas du nœud 1.


Re: commentaire de e-satis sur les enfants immédiats (ou parent immédiat). Vous pouvez ajouter une colonne "path_length" à la ClosureTable pour faciliter la recherche spécifique d'un enfant immédiat ou d'un parent (ou de toute autre distance).

INSERT INTO ClosureTable (ancestor_id, descendant_id, path_length) VALUES
  (1,1,0), (1,2,1), (1,4,2), (1,6,1),
  (2,2,0), (2,4,1),
  (3,3,0), (3,5,1),
  (4,4,0),
  (5,5,0),
  (6,6,0);

Vous pouvez ensuite ajouter un terme dans votre recherche pour interroger les enfants immédiats d'un nœud donné. Ce sont des descendants dont path_length est 1.

SELECT f.* 
FROM FlatTable f 
  JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1
  AND path_length = 1;

+----+
| id |
+----+
|  2 | 
|  6 | 
+----+

Commentaire de @ashraf: "Pourquoi ne pas trier l’arbre entier [par son nom]?"

Voici un exemple de requête visant à renvoyer tous les nœuds descendants du nœud 1, à les associer au FlatTable contenant d'autres attributs de nœud tels que name, et à effectuer un tri en fonction du nom.

SELECT f.name
FROM FlatTable f 
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1
ORDER BY f.name;

Re commentaire de @Nate:

SELECT f.name, GROUP_CONCAT(b.ancestor_id order by b.path_length desc) AS breadcrumbs
FROM FlatTable f 
JOIN ClosureTable a ON (f.id = a.descendant_id) 
JOIN ClosureTable b ON (b.descendant_id = a.descendant_id) 
WHERE a.ancestor_id = 1 
GROUP BY a.descendant_id 
ORDER BY f.name

+------------+-------------+
| name       | breadcrumbs |
+------------+-------------+
| Node 1     | 1           |
| Node 1.1   | 1,2         |
| Node 1.1.1 | 1,2,4       |
| Node 1.2   | 1,6         |
+------------+-------------+

Un utilisateur a suggéré une modification aujourd'hui. SO les modérateurs ont approuvé l'édition, mais je l'inverse.

La modification a suggéré que ORDER BY dans la dernière requête ci-dessus soit ORDER BY b.path_length, f.name, probablement pour s'assurer que l'ordre correspond à la hiérarchie. Mais cela ne fonctionne pas, car cela ordonnerait "Node 1.1.1" après "Node 1.2".

Si vous souhaitez que l'ordre corresponde à la hiérarchie de manière rationnelle, vous pouvez le faire, mais pas simplement en fonction de la longueur du chemin. Par exemple, voir ma réponse à base de données hiérarchique MySQL Closure Table - Comment extraire les informations dans le bon ordre .

437
Bill Karwin

Si vous utilisez des ensembles imbriqués (parfois désignés Traversal d’arborescence de pré-commande modifiée), vous pouvez extraire l’arborescence complète ou tout sous-arbre de cette arborescence avec une seule requête, au prix d’insertions plus coûteuses, car vous devez gérer les colonnes qui décrivent un chemin dans l’ordre à travers l’arborescence.

Pour Django-mptt , j'ai utilisé une structure comme celle-ci:

 id parent_id arbre_id niveau lft rght 
 - --------- ------- ----- --- ---- 
 1 null 1 0 1 14 
 2 1 1 1 2 7 
 3 2 1 2 3 4 
 4 2 1 2 5 6 
 5 1 1 1 8 13 
 6 5 1 2 9 10 
 7 5 1 2 11 12 

Ce qui décrit un arbre qui ressemble à ceci (avec id représentant chaque élément):

 1 
 + - 2 
 | + - 3 
 | + - 4 
 | 
 + - 5 
 + - 6 
 + - 7 

Ou, en tant que diagramme d'ensemble imbriqué qui rend plus évident le fonctionnement des valeurs lft et rght:

 __________________________________________________________________________ 
 | Racine 1 | 
 | ________________________________ ________________________________ | 
 | | Enfant 1.1 | | Enfant 1.2 | | 
 | | ___________ ___________ | | ___________ ___________ | | 
 | | | C 1.1.1 | | C 1.1.2 | | | | C 1.2.1 | | C 1.2.2 | | | 
 1 2 3___________4 5___________6 7 8 9___________10 11__________12 13 14 
 | | ________________________________ | | ________________________________ | | 
 | __________________________________________________________________________ | 

Comme vous pouvez le voir, pour obtenir le sous-arbre entier pour un nœud donné, dans l’arbre, il vous suffit de sélectionner toutes les lignes qui ont des valeurs lft et rght comprises entre lft et rght valeurs. Il est également simple de récupérer l’arbre des ancêtres pour un nœud donné.

La colonne level est un peu de dénormalisation pour plus de commodité et la colonne _tree_id_ vous permet de redémarrer la numérotation lft et rght pour chaque nœud de niveau supérieur, lequel réduit le nombre de colonnes affectées par les insertions, les déplacements et les suppressions, car les colonnes lft et rght doivent être ajustées en conséquence lorsque ces opérations ont lieu afin de créer ou de fermer des espaces. J'ai fait quelques notes de développement au moment où j'essayais de comprendre les requêtes requises pour chaque opération.

En ce qui concerne l’utilisation effective de ces données pour afficher un arbre, j’ai créé une fonction tree_item_iterator qui devrait, pour chaque nœud, vous donner suffisamment d’informations pour générer le type d’affichage souhaité.

Plus d'infos sur MPTT:

55
Jonny Buchanan

C'est une question assez ancienne, mais comme elle suscite de nombreux points de vue, je pense qu'il vaut la peine de présenter une solution de rechange, et à mon avis très élégante.

Pour lire une arborescence, vous pouvez utiliser des expressions de table communes récursives (CTE). Cela donne la possibilité d'extraire la totalité de l'arborescence à la fois, d'avoir les informations sur le niveau du nœud, son nœud parent et son ordre au sein des enfants du nœud parent.

Laissez-moi vous montrer comment cela fonctionnerait dans PostgreSQL 9.1.

  1. Créer une structure

    CREATE TABLE tree (
        id int  NOT NULL,
        name varchar(32)  NOT NULL,
        parent_id int  NULL,
        node_order int  NOT NULL,
        CONSTRAINT tree_pk PRIMARY KEY (id),
        CONSTRAINT tree_tree_fk FOREIGN KEY (parent_id) 
          REFERENCES tree (id) NOT DEFERRABLE
    );
    
    
    insert into tree values
      (0, 'ROOT', NULL, 0),
      (1, 'Node 1', 0, 10),
      (2, 'Node 1.1', 1, 10),
      (3, 'Node 2', 0, 20),
      (4, 'Node 1.1.1', 2, 10),
      (5, 'Node 2.1', 3, 10),
      (6, 'Node 1.2', 1, 20);
    
  2. Écrire une requête

    WITH RECURSIVE 
    tree_search (id, name, level, parent_id, node_order) AS (
      SELECT 
        id, 
        name,
        0,
        parent_id, 
        1 
      FROM tree
      WHERE parent_id is NULL
    
      UNION ALL 
      SELECT 
        t.id, 
        t.name,
        ts.level + 1, 
        ts.id, 
        t.node_order 
      FROM tree t, tree_search ts 
      WHERE t.parent_id = ts.id 
    ) 
    SELECT * FROM tree_search 
    WHERE level > 0 
    ORDER BY level, parent_id, node_order;
    

    Voici les résultats:

     id |    name    | level | parent_id | node_order 
    ----+------------+-------+-----------+------------
      1 | Node 1     |     1 |         0 |         10
      3 | Node 2     |     1 |         0 |         20
      2 | Node 1.1   |     2 |         1 |         10
      6 | Node 1.2   |     2 |         1 |         20
      5 | Node 2.1   |     2 |         3 |         10
      4 | Node 1.1.1 |     3 |         2 |         10
    (6 rows)
    

    Les nœuds d'arbre sont classés par niveau de profondeur. Dans le résultat final, nous les présenterons dans les lignes suivantes.

    Pour chaque niveau, ils sont classés par parent_id et node_order dans le parent. Cela nous indique comment les présenter dans le noeud de lien de sortie au parent dans cet ordre.

    Avec une telle structure, il ne serait pas difficile de faire une très belle présentation en HTML.

    Les CTE récursifs sont disponibles dans PostgreSQL, IBM DB2, MS SQL Server et Oracle .

    Si vous souhaitez en savoir plus sur les requêtes SQL récursives, vous pouvez consulter la documentation de votre SGBD préféré ou lire mes deux articles traitant de ce sujet:

19

À partir d'Oracle 9i, vous pouvez utiliser CONNECT BY.

SELECT LPAD(' ', (LEVEL - 1) * 4) || "Name" AS "Name"
FROM (SELECT * FROM TMP_NODE ORDER BY "Order")
CONNECT BY PRIOR "Id" = "ParentId"
START WITH "Id" IN (SELECT "Id" FROM TMP_NODE WHERE "ParentId" = 0)

A partir de SQL Server 2005, vous pouvez utiliser une expression de table commune récursive (CTE).

WITH [NodeList] (
  [Id]
  , [ParentId]
  , [Level]
  , [Order]
) AS (
  SELECT [Node].[Id]
    , [Node].[ParentId]
    , 0 AS [Level]
    , CONVERT([varchar](MAX), [Node].[Order]) AS [Order]
  FROM [Node]
  WHERE [Node].[ParentId] = 0
  UNION ALL
  SELECT [Node].[Id]
    , [Node].[ParentId]
    , [NodeList].[Level] + 1 AS [Level]
    , [NodeList].[Order] + '|'
      + CONVERT([varchar](MAX), [Node].[Order]) AS [Order]
  FROM [Node]
    INNER JOIN [NodeList] ON [NodeList].[Id] = [Node].[ParentId]
) SELECT REPLICATE(' ', [NodeList].[Level] * 4) + [Node].[Name] AS [Name]
FROM [Node]
  INNER JOIN [NodeList] ON [NodeList].[Id] = [Node].[Id]
ORDER BY [NodeList].[Order]

Les deux produiront les résultats suivants.

 Nom 
 'Noeud 1' 
 'Node 1.1' 
 'Node 1.1.1' 
 'Node 1.2' 
 'Node 2' 
 'Node 2.1' 
18
Eric Weilnau

La réponse de Bill est diablement bonne, cette réponse ajoute quelques éléments qui me font souhaiter que SO reçoive des réponses partagées.

Quoi qu'il en soit, je voulais supporter l'arborescence et la propriété Order. J'ai inclus une propriété unique dans chaque Node appelé leftSibling qui fait la même chose que Order est censé faire dans la question d'origine (maintien de l'ordre de gauche à droite).

 mysql> desc noeuds; 
 + ------------- + -------------- + ----- - + ----- + --------- + ---------------- + 
 | Domaine | Type | Null | Clé | Par défaut | Extra | 
 + ------------- + -------------- + ------ + ----- + --------- + ---------------- + 
 | id | int (11) | NON | PRI | NULL | auto_increment | 
 | nom | varchar (255) | OUI | NULL | | 
 | gaucheSibling | int (11) | NON | | 0 | | 
 + ------------- + -------------- + ------ + ----- + - -------- + ---------------- + 
 3 lignes dans l'ensemble (0.00 sec) 
 
 Mysql > desc adjacences; 
 + ------------ + --------- + ------ + ----- + ---- ----- + ---------------- + 
 | Domaine | Type | Null | Clé | Par défaut | Extra | 
 + ------------ + --------- + ------ + ----- + ------ --- + ---------------- + 
 | relationId | int (11) | NON | PRI | NULL | auto_increment | 
 | parent | int (11) | NON | | NULL | | 
 | enfant | int (11) | NON | | NULL | | 
 | pathLen | int (11) | NON | | NULL | | 
 + ------------ + --------- + ------ + ----- + ------- - + ---------------- + 
 4 lignes dans l'ensemble (0.00 sec) 

Plus de détails et code SQL sur mon blog .

Merci Bill, ta réponse a été utile pour commencer!

9
bobobobo

Bien choisi, j'utiliserais des objets. Je créerais un objet pour chaque enregistrement où chaque objet aurait une collection children et les stockerais tous dans un tableau assoc (/ hashtable) où l'Id est la clé. Et blitzez une fois dans la collection en ajoutant les enfants aux champs correspondants. Simple.

Mais parce que vous n'êtes pas amusant en limitant l'utilisation d'une bonne POO, je ferais probablement une itération basée sur:

function PrintLine(int pID, int level)
    foreach record where ParentID == pID
        print level*tabs + record-data
        PrintLine(record.ID, level + 1)

PrintLine(0, 0)

Edit: cela ressemble à quelques autres entrées, mais je pense que c'est légèrement plus propre. J'ajouterai une chose: c'est extrêmement gourmand en SQL. C'est méchant . Si vous avez le choix, passez par la route OOP.

7
Oli

Ceci a été écrit rapidement, et n’est ni beau ni efficace (en plus, il utilise beaucoup de boîtes automatiques, convertir entre int et Integer est gênant!), Mais cela fonctionne.

Cela déroge probablement aux règles puisque je crée mes propres objets mais bon je fais cela comme une diversion du vrai travail :)

Cela suppose également que la table resultSet/est complètement lue dans une sorte de structure avant de commencer à construire des nœuds, ce qui ne serait pas la meilleure solution si vous avez des centaines de milliers de lignes.

public class Node {

    private Node parent = null;

    private List<Node> children;

    private String name;

    private int id = -1;

    public Node(Node parent, int id, String name) {
        this.parent = parent;
        this.children = new ArrayList<Node>();
        this.name = name;
        this.id = id;
    }

    public int getId() {
        return this.id;
    }

    public String getName() {
        return this.name;
    }

    public void addChild(Node child) {
        children.add(child);
    }

    public List<Node> getChildren() {
        return children;
    }

    public boolean isRoot() {
        return (this.parent == null);
    }

    @Override
    public String toString() {
        return "id=" + id + ", name=" + name + ", parent=" + parent;
    }
}

public class NodeBuilder {

    public static Node build(List<Map<String, String>> input) {

        // maps id of a node to it's Node object
        Map<Integer, Node> nodeMap = new HashMap<Integer, Node>();

        // maps id of a node to the id of it's parent
        Map<Integer, Integer> childParentMap = new HashMap<Integer, Integer>();

        // create special 'root' Node with id=0
        Node root = new Node(null, 0, "root");
        nodeMap.put(root.getId(), root);

        // iterate thru the input
        for (Map<String, String> map : input) {

            // expect each Map to have keys for "id", "name", "parent" ... a
            // real implementation would read from a SQL object or resultset
            int id = Integer.parseInt(map.get("id"));
            String name = map.get("name");
            int parent = Integer.parseInt(map.get("parent"));

            Node node = new Node(null, id, name);
            nodeMap.put(id, node);

            childParentMap.put(id, parent);
        }

        // now that each Node is created, setup the child-parent relationships
        for (Map.Entry<Integer, Integer> entry : childParentMap.entrySet()) {
            int nodeId = entry.getKey();
            int parentId = entry.getValue();

            Node child = nodeMap.get(nodeId);
            Node parent = nodeMap.get(parentId);
            parent.addChild(child);
        }

        return root;
    }
}

public class NodePrinter {

    static void printRootNode(Node root) {
        printNodes(root, 0);
    }

    static void printNodes(Node node, int indentLevel) {

        printNode(node, indentLevel);
        // recurse
        for (Node child : node.getChildren()) {
            printNodes(child, indentLevel + 1);
        }
    }

    static void printNode(Node node, int indentLevel) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < indentLevel; i++) {
            sb.append("\t");
        }
        sb.append(node);

        System.out.println(sb.toString());
    }

    public static void main(String[] args) {

        // setup dummy data
        List<Map<String, String>> resultSet = new ArrayList<Map<String, String>>();
        resultSet.add(newMap("1", "Node 1", "0"));
        resultSet.add(newMap("2", "Node 1.1", "1"));
        resultSet.add(newMap("3", "Node 2", "0"));
        resultSet.add(newMap("4", "Node 1.1.1", "2"));
        resultSet.add(newMap("5", "Node 2.1", "3"));
        resultSet.add(newMap("6", "Node 1.2", "1"));

        Node root = NodeBuilder.build(resultSet);
        printRootNode(root);

    }

    //convenience method for creating our dummy data
    private static Map<String, String> newMap(String id, String name, String parentId) {
        Map<String, String> row = new HashMap<String, String>();
        row.put("id", id);
        row.put("name", name);
        row.put("parent", parentId);
        return row;
    }
}
5
matt b

Il existe de très bonnes solutions qui exploitent la représentation btree interne des indices SQL. C’est basé sur de grandes recherches effectuées vers 1998.

Voici un exemple de table (en mysql).

CREATE TABLE `node` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `tw` int(10) unsigned NOT NULL,
  `pa` int(10) unsigned DEFAULT NULL,
  `sz` int(10) unsigned DEFAULT NULL,
  `nc` int(11) GENERATED ALWAYS AS (tw+sz) STORED,
  PRIMARY KEY (`id`),
  KEY `node_tw_index` (`tw`),
  KEY `node_pa_index` (`pa`),
  KEY `node_nc_index` (`nc`),
  CONSTRAINT `node_pa_fk` FOREIGN KEY (`pa`) REFERENCES `node` (`tw`) ON DELETE CASCADE
)

Les seuls champs nécessaires à la représentation sous forme d'arborescence sont:

  • tw: Index de pré-commande DFS de gauche à droite, où root = 1.
  • pa: La référence (en utilisant tw) au nœud parent, la racine a la valeur null.
  • sz: La taille de la branche du noeud, y compris lui-même.
  • nc: est utilisé comme sucre syntaxique. c'est tw + nc et représente le tw du "prochain enfant" du noeud.

Voici un exemple de population de 24 nœuds, ordonnée par tw:

+-----+---------+----+------+------+------+
| id  | name    | tw | pa   | sz   | nc   |
+-----+---------+----+------+------+------+
|   1 | Root    |  1 | NULL |   24 |   25 |
|   2 | A       |  2 |    1 |   14 |   16 |
|   3 | AA      |  3 |    2 |    1 |    4 |
|   4 | AB      |  4 |    2 |    7 |   11 |
|   5 | ABA     |  5 |    4 |    1 |    6 |
|   6 | ABB     |  6 |    4 |    3 |    9 |
|   7 | ABBA    |  7 |    6 |    1 |    8 |
|   8 | ABBB    |  8 |    6 |    1 |    9 |
|   9 | ABC     |  9 |    4 |    2 |   11 |
|  10 | ABCD    | 10 |    9 |    1 |   11 |
|  11 | AC      | 11 |    2 |    4 |   15 |
|  12 | ACA     | 12 |   11 |    2 |   14 |
|  13 | ACAA    | 13 |   12 |    1 |   14 |
|  14 | ACB     | 14 |   11 |    1 |   15 |
|  15 | AD      | 15 |    2 |    1 |   16 |
|  16 | B       | 16 |    1 |    1 |   17 |
|  17 | C       | 17 |    1 |    6 |   23 |
| 359 | C0      | 18 |   17 |    5 |   23 |
| 360 | C1      | 19 |   18 |    4 |   23 |
| 361 | C2(res) | 20 |   19 |    3 |   23 |
| 362 | C3      | 21 |   20 |    2 |   23 |
| 363 | C4      | 22 |   21 |    1 |   23 |
|  18 | D       | 23 |    1 |    1 |   24 |
|  19 | E       | 24 |    1 |    1 |   25 |
+-----+---------+----+------+------+------+

Chaque résultat d'arbre peut être effectué de manière non récursive. Par exemple, pour obtenir une liste des ancêtres du noeud à tw = '22 '

Ancêtres

select anc.* from node me,node anc 
where me.tw=22 and anc.nc >= me.tw and anc.tw <= me.tw 
order by anc.tw;
+-----+---------+----+------+------+------+
| id  | name    | tw | pa   | sz   | nc   |
+-----+---------+----+------+------+------+
|   1 | Root    |  1 | NULL |   24 |   25 |
|  17 | C       | 17 |    1 |    6 |   23 |
| 359 | C0      | 18 |   17 |    5 |   23 |
| 360 | C1      | 19 |   18 |    4 |   23 |
| 361 | C2(res) | 20 |   19 |    3 |   23 |
| 362 | C3      | 21 |   20 |    2 |   23 |
| 363 | C4      | 22 |   21 |    1 |   23 |
+-----+---------+----+------+------+------+

Les frères et sœurs et les enfants sont triviaux - il suffit d'utiliser l'ordre des champs pa par tw.

Descendants

Par exemple, l'ensemble (la branche) des nœuds enracinés à tw = 17.

select des.* from node me,node des 
where me.tw=17 and des.tw < me.nc and des.tw >= me.tw 
order by des.tw;
+-----+---------+----+------+------+------+
| id  | name    | tw | pa   | sz   | nc   |
+-----+---------+----+------+------+------+
|  17 | C       | 17 |    1 |    6 |   23 |
| 359 | C0      | 18 |   17 |    5 |   23 |
| 360 | C1      | 19 |   18 |    4 |   23 |
| 361 | C2(res) | 20 |   19 |    3 |   23 |
| 362 | C3      | 21 |   20 |    2 |   23 |
| 363 | C4      | 22 |   21 |    1 |   23 |
+-----+---------+----+------+------+------+

Notes complémentaires

Cette méthodologie est extrêmement utile pour les cas où le nombre de lectures est beaucoup plus important que les insertions ou les mises à jour.

L'insertion, le déplacement ou la mise à jour d'un nœud dans l'arborescence nécessitant l'ajustement de l'arborescence, il est nécessaire de verrouiller la table avant de commencer l'action.

Le coût d'insertion/suppression est élevé car les valeurs tw index et sz (taille de branche) devront être mises à jour sur tous les nœuds après le point d'insertion et pour tous les ancêtres, respectivement.

Le déplacement d'une branche implique le déplacement de la valeur tw de la branche en dehors de sa plage. Il est donc également nécessaire de désactiver les contraintes de clé étrangère lors du déplacement d'une branche. Il faut essentiellement quatre requêtes pour déplacer une branche:

  • Déplacez la branche hors de portée.
  • Fermer le fossé qu'il a laissé. (l'arbre restant est maintenant normalisé).
  • Ouvrez l'espace où il ira.
  • Déplacez la branche dans sa nouvelle position.

Ajuster les requêtes d'arbre

L'ouverture/la fermeture des espaces dans l'arborescence est une sous-fonction importante utilisée par les méthodes create/update/delete, je l'inclue donc ici.

Nous avons besoin de deux paramètres: un drapeau indiquant si nous sommes en train de réduire ou de réduire la taille, et l'index tw du noeud. Ainsi, par exemple tw = 18 (qui a une taille de branche de 5). Supposons que nous procédons à une réduction des effectifs (suppression de tw) - cela signifie que nous utilisons "-" au lieu de "+" dans les mises à jour de l'exemple suivant.

Nous utilisons d’abord une fonction ancêtre (légèrement modifiée) pour mettre à jour la valeur sz.

update node me, node anc set anc.sz = anc.sz - me.sz from 
node me, node anc where me.tw=18 
and ((anc.nc >= me.tw and anc.tw < me.pa) or (anc.tw=me.pa));

Ensuite, nous devons ajuster le tw pour ceux dont le tw est supérieur à la branche à supprimer.

update node me, node anc set anc.tw = anc.tw - me.sz from 
node me, node anc where me.tw=18 and anc.tw >= me.tw;

Ensuite, nous devons ajuster le parent pour ceux dont la valeur de pa est supérieure à la branche à supprimer.

update node me, node anc set anc.pa = anc.pa - me.sz from 
node me, node anc where me.tw=18 and anc.pa >= me.tw;
4
Konchog

Vous pouvez émuler n'importe quelle autre structure de données avec une table de hachage, ce qui ne constitue pas une limitation terrible. En analysant de haut en bas, vous créez une table de hachage pour chaque ligne de la base de données, avec une entrée pour chaque colonne. Ajoutez chacun de ces hashmaps à un hashmap "maître", indexé sur l'id. Si un nœud a un "parent" que vous n'avez pas encore vu, créez une entrée de substitution pour celui-ci dans la table de hachage principale et complétez-le lorsque vous voyez le nœud réel.

Pour l’imprimer, faites une simple première analyse en profondeur des données, en gardant une trace du niveau de retrait le long du chemin. Vous pouvez rendre cela plus facile en conservant une entrée "enfants" pour chaque ligne et en la renseignant au fur et à mesure que vous numérisez les données.

Quant à savoir s'il existe un "meilleur" moyen de stocker une arborescence dans une base de données, cela dépend de la manière dont vous allez utiliser les données. J'ai vu des systèmes dont la profondeur maximale connue utilisait une table différente pour chaque niveau de la hiérarchie. Cela a beaucoup de sens si les niveaux dans l’arbre ne sont pas tout à fait équivalents après tout (les catégories de niveau supérieur étant différentes des feuilles).

3
Mark Bessey

En supposant que vous sachiez que les éléments racines sont à zéro, voici le pseudocode à afficher en texte:

function PrintLevel (int curr, int level)
    //print the indents
    for (i=1; i<=level; i++)
        print a tab
    print curr \n;
    for each child in the table with a parent of curr
        PrintLevel (child, level+1)


for each elementID where the parentid is zero
    PrintLevel(elementID, 0)
3
wcm

Si des cartes ou des tableaux de hachage imbriqués peuvent être créés, je peux simplement parcourir la table depuis le début et ajouter chaque élément au tableau imbriqué. Je dois tracer chaque ligne jusqu'au nœud racine afin de savoir à quel niveau du tableau imbriqué insérer. Je peux utiliser la mémoisation pour ne pas avoir à chercher le même parent encore et encore.

Edit: Je lirais d’abord la table entière dans un tableau pour qu’il n’interroge pas le DB à plusieurs reprises. Bien sûr, cela ne sera pas pratique si votre table est très grande.

Une fois la structure construite, je dois faire une première traversée et imprimer le code HTML.

Il n'y a pas de meilleur moyen fondamental de stocker cette information en utilisant une seule table (je pourrais me tromper cependant;), et j'aimerais bien trouver une meilleure solution). Cependant, si vous créez un schéma pour utiliser des tables de base de données créées de manière dynamique, vous ouvrez un tout nouveau monde au sacrifice de la simplicité et des risques de l'enfer SQL;).

1
tchen

Pour étendre la solution SQL de Bill, vous pouvez en principe faire de même en utilisant un tableau à plat. De plus, si vos chaînes ont toutes la même longueur et que votre nombre maximal d’enfants est connu (par exemple dans un arbre binaire), vous pouvez le faire en utilisant une seule chaîne (tableau de caractères). Si vous avez un nombre arbitraire d'enfants, cela complique un peu les choses ... Il faudrait que je vérifie mes anciennes notes pour voir ce qui peut être fait.

Ensuite, en sacrifiant un peu de mémoire, en particulier si votre arbre est peu dense et/ou non équilibré, vous pouvez, avec un peu de maths d’index, accéder à toutes les chaînes de façon aléatoire en stockant votre arbre, la largeur en premier dans le tableau, arbre):

String[] nodeArray = [L0root, L1child1, L1child2, L2Child1, L2Child2, L2Child3, L2Child4] ...

vous connaissez votre longueur de corde, vous le savez

Je suis au travail maintenant, donc je ne peux pas y passer beaucoup de temps, mais avec intérêt, je peux aller chercher un peu de code pour le faire.

Nous avons l'habitude de le faire pour rechercher dans des arbres binaires constitués de codons d'ADN, un processus construit de l'arbre, puis nous l'avons aplati pour rechercher des modèles de texte et, une fois trouvé, bien que l'index math (inverse de ci-dessus) nous récupère le nœud ... très rapide et efficace, robuste, notre arbre avait rarement des nœuds vides, mais nous pouvions rechercher des giga-octets de données en un tournemain.

1
Newtopian

Si les éléments sont dans l’arbre, comme indiqué dans votre exemple, vous pouvez utiliser quelque chose comme l’exemple suivant Python:

delimiter = '.'
stack = []
for item in items:
  while stack and not item.startswith(stack[-1]+delimiter):
    print "</div>"
    stack.pop()
  print "<div>"
  print item
  stack.append(item)

Cela permet de maintenir une pile représentant la position actuelle dans l'arborescence. Pour chaque élément de la table, il extrait les éléments de la pile (en fermant les div correspondants) jusqu'à ce qu'il trouve le parent de l'élément en cours. Ensuite, il affiche le début de ce nœud et le place dans la pile.

Si vous souhaitez afficher l'arborescence à l'aide d'indentation plutôt que d'éléments imbriqués, vous pouvez simplement ignorer les instructions d'impression pour imprimer les div et imprimer un nombre d'espaces égal à un multiple de la taille de la pile avant chaque élément. Par exemple, en Python:

print "  " * len(stack)

Vous pouvez également facilement utiliser cette méthode pour construire un ensemble de listes ou de dictionnaires imbriqués.

Edit: Je vois dans votre clarification que les noms n'étaient pas destinés à être des chemins de noeuds. Cela suggère une autre approche:

idx = {}
idx[0] = []
for node in results:
  child_list = []
  idx[node.Id] = child_list
  idx[node.ParentId].append((node, child_list))

Ceci construit un arbre de tableaux de tuples (!). idx [0] représente la ou les racine (s) de l'arbre. Chaque élément d'un tableau est un 2-Tuple composé du nœud lui-même et d'une liste de tous ses enfants. Une fois construit, vous pouvez conserver idx [0] et ignorer idx, sauf si vous souhaitez accéder aux nœuds par leur ID.

1
Nick Johnson

Pensez à utiliser des outils nosql tels que neo4j pour les structures hiérarchiques. Par exemple, une application en réseau telle que linkedin utilise couchbase (une autre solution nosql)

Mais utilisez nosql uniquement pour les requêtes de niveau données et ne pas stocker/gérer les transactions

0