web-dev-qa-db-fra.com

Quelles sont les options pour stocker des données hiérarchiques dans une base de données relationnelle?

bons aperçus

En règle générale, vous prenez une décision entre des temps de lecture rapides (par exemple, un ensemble imbriqué) et des temps d'écriture rapides (liste de contiguïté). Habituellement, vous obtenez une combinaison des options ci-dessous qui répondent le mieux à vos besoins. Ce qui suit fournit une lecture en profondeur:

Options

Je connais et caractéristiques générales:

  1. Liste de proximité :
    • Colonnes: ID, ParentID
    • Facile à mettre en œuvre.
    • Le nœud bon marché se déplace, insère et supprime.
    • Cher pour trouver le niveau, ascendance et descendants, chemin
    • Evitez N + 1 via Common Table Expressions dans les bases de données qui les prennent en charge
  2. jeu imbriqué (a.k.a traversée d'arbre de précommande modifiée )
    • Colonnes: gauche, droite
    • Ascendance pas cher, descendants
    • Très coûteux O(n/2) se déplace, insère, supprime en raison d'un encodage volatile
  3. table de pont (a.k.a. table de fermeture/w déclencheurs )
    • Utilise une table de jointure séparée avec: ancêtre, descendant, profondeur (facultatif)
    • Ascendance et descendants bon marché
    • Coûts d’écriture O(log n) (taille de la sous-arborescence) pour l’insertion, la mise à jour, la suppression
    • Encodage normalisé: bon pour les statistiques SGBDR et le planificateur de requêtes dans les jointures
    • Nécessite plusieurs lignes par nœud
  4. colonne de lignage (a.k.a. chemin matérialisé , énumération de chemin)
    • Colonne: lignage (par exemple,/parent/child/petitchild/etc ...)
    • Descendants économiques via une requête préfixe (par exemple, LEFT(lineage, #) = '/enumerated/path')
    • Coûts d’écriture O(log n) (taille de la sous-arborescence) pour l’insertion, la mise à jour, la suppression
    • Non relationnel: s'appuie sur le type de données Array ou le format de chaîne sérialisé
  5. intervalles imbriqués
    • Comme un ensemble imbriqué, mais avec real/float/decimal afin que l'encodage ne soit pas volatile (déplacement/insertion/suppression peu coûteux)
    • A des problèmes réels/flottants/de représentation décimale/de précision
    • variante de codage Matrix ajoute le codage ancêtre (chemin matérialisé) pour "libre", mais avec la difficulté supplémentaire de l'algèbre linéaire.
  6. Table plate
    • Une liste d’adjacence modifiée qui ajoute une colonne de niveau et de rang (par exemple, un ordre) à chaque enregistrement.
    • Pas cher pour itérer/paginer
    • Mouvement coûteux et supprimer
    • Bonne utilisation: discussion par fil de discussion - commentaires sur les forums/blog
  7. Colonnes à lignées multiples
    • Colonnes: une pour chaque niveau de lignage, fait référence à tous les parents jusqu'à la racine, les niveaux inférieurs au niveau de l'élément sont définis sur NULL
    • Ancêtres bon marché, descendants, niveau
    • Insert bon marché, supprimer, déplacer des feuilles
    • Cher insertion, suppression, déplacement des noeuds internes
    • Limite stricte de la profondeur de la hiérarchie

Remarques spécifiques à la base de données

MySQL

Oracle

  • Utilisez CONNECT BY pour parcourir les listes d’adjacence

PostgreSQL

SQL Server

  • Résumé général
  • Offres 2008 HierarchyId Le type de données semble faciliter l’approche des colonnes de lignage et élargir la profondeur pouvant être représentée.
1248
orangepips

Ma réponse préférée est celle suggérée par la première phrase de ce fil. Utilisez une liste d’adjacence pour gérer la hiérarchie et utilisez des ensembles imbriqués pour interroger la hiérarchie.

Le problème jusqu'à présent est que la méthode de recouvrement d'une liste Adjacecy à des ensembles imbriqués a été terriblement lente, car la plupart des gens utilisent la méthode RBAR extrême connue sous le nom de "Push Stack" pour effectuer la conversion. atteindre le Nirvana de la simplicité de la maintenance par la liste d’adjacence et de l’impressionnante performance des ensembles imbriqués. En conséquence, la plupart des gens doivent se contenter de l’un ou de l’autre, en particulier s’il ya plus de 100 000 nœuds, par exemple. L'utilisation de la méthode de pile Push peut prendre une journée entière pour effectuer la conversion de ce que les MLM considèrent comme une petite hiérarchie de noeuds.

Je pensais donner un peu de concurrence à Celko en proposant une méthode pour convertir une liste Adjacency en ensembles imbriqués à des vitesses qui semblaient tout simplement impossibles. Voici les performances de la méthode de pile Push sur mon ordinateur portable i5.

Duration for     1,000 Nodes = 00:00:00:870 
Duration for    10,000 Nodes = 00:01:01:783 (70 times slower instead of just 10)
Duration for   100,000 Nodes = 00:49:59:730 (3,446 times slower instead of just 100) 
Duration for 1,000,000 Nodes = 'Didn't even try this'

Et voici la durée de la nouvelle méthode (avec la méthode de pile Push entre parenthèses).

Duration for     1,000 Nodes = 00:00:00:053 (compared to 00:00:00:870)
Duration for    10,000 Nodes = 00:00:00:323 (compared to 00:01:01:783)
Duration for   100,000 Nodes = 00:00:03:867 (compared to 00:49:59:730)
Duration for 1,000,000 Nodes = 00:00:54:283 (compared to something like 2 days!!!)

Oui c'est correct. 1 million de noeuds convertis en moins d'une minute et 100 000 noeuds en moins de 4 secondes.

Vous pouvez en savoir plus sur la nouvelle méthode et obtenir une copie du code à l'URL suivante. http://www.sqlservercentral.com/articles/Hierarchy/94040/

J'ai également développé une hiérarchie "pré-agrégée" en utilisant des méthodes similaires. Les MLM et les auteurs de nomenclatures seront particulièrement intéressés par cet article. http://www.sqlservercentral.com/articles/T-SQL/94570/

Si vous vous arrêtez pour consulter l'un ou l'autre article, sautez dans le lien "Rejoignez la discussion" et dites-moi ce que vous en pensez.

60
Jeff Moden

C'est une réponse très partielle à votre question, mais j'espère toujours utile.

Microsoft SQL Server 2008 implémente deux fonctionnalités extrêmement utiles pour la gestion de données hiérarchiques:

  • le type de données HierarchyId .
  • expressions de table communes, en utilisant le mot clé with .

Jetez un œil à "Modélisez vos hiérarchies de données avec SQL Server 2008" par Kent Tegels sur MSDN pour les démarrages. Voir aussi ma propre question: Requête récursive à table identique dans SQL Server 2008

31
CesarGon

Ce dessin n'a pas encore été mentionné:

Colonnes à lignées multiples

Bien qu'il y ait des limites, si vous pouvez les supporter, c'est très simple et très efficace. Fonctionnalités:

  • Colonnes: une pour chaque niveau de lignée, fait référence à tous les parents jusqu'à la racine, les niveaux inférieurs au niveau des éléments actuels sont définis sur 0 (ou NULL)
  • Il existe une limite fixe à la profondeur de la hiérarchie.
  • Ancêtres bon marché, descendants, niveau
  • Insert bon marché, supprimer, déplacer des feuilles
  • Cher insertion, suppression, déplacement des noeuds internes

Voici un exemple - arbre taxonomique des oiseaux, ainsi la hiérarchie est Classe/Ordre/Famille/Genre/Espèce - l’espèce est le niveau le plus bas, 1 rangée = 1 taxon (ce qui correspond à l’espèce dans le cas des nœuds foliaires):

CREATE TABLE `taxons` (
  `TaxonId` smallint(6) NOT NULL default '0',
  `ClassId` smallint(6) default NULL,
  `OrderId` smallint(6) default NULL,
  `FamilyId` smallint(6) default NULL,
  `GenusId` smallint(6) default NULL,
  `Name` varchar(150) NOT NULL default ''
);

et l'exemple des données:

+---------+---------+---------+----------+---------+-------------------------------+
| TaxonId | ClassId | OrderId | FamilyId | GenusId | Name                          |
+---------+---------+---------+----------+---------+-------------------------------+
|     254 |       0 |       0 |        0 |       0 | Aves                          |
|     255 |     254 |       0 |        0 |       0 | Gaviiformes                   |
|     256 |     254 |     255 |        0 |       0 | Gaviidae                      |
|     257 |     254 |     255 |      256 |       0 | Gavia                         |
|     258 |     254 |     255 |      256 |     257 | Gavia stellata                |
|     259 |     254 |     255 |      256 |     257 | Gavia arctica                 |
|     260 |     254 |     255 |      256 |     257 | Gavia immer                   |
|     261 |     254 |     255 |      256 |     257 | Gavia adamsii                 |
|     262 |     254 |       0 |        0 |       0 | Podicipediformes              |
|     263 |     254 |     262 |        0 |       0 | Podicipedidae                 |
|     264 |     254 |     262 |      263 |       0 | Tachybaptus                   |

C’est formidable, car cela vous permet de réaliser très facilement toutes les opérations nécessaires, à condition que les catégories internes ne changent pas de niveau dans l’arborescence.

27
TMS

Modèle d'adjacence + modèle d'ensembles imbriqués

J'y suis allé parce que je pouvais insérer facilement de nouveaux éléments dans l'arborescence (vous avez simplement besoin d'un identifiant de branche pour y insérer un nouvel élément) et l'interroger assez rapidement.

+-------------+----------------------+--------+-----+-----+
| category_id | name                 | parent | lft | rgt |
+-------------+----------------------+--------+-----+-----+
|           1 | ELECTRONICS          |   NULL |   1 |  20 |
|           2 | TELEVISIONS          |      1 |   2 |   9 |
|           3 | TUBE                 |      2 |   3 |   4 |
|           4 | LCD                  |      2 |   5 |   6 |
|           5 | PLASMA               |      2 |   7 |   8 |
|           6 | PORTABLE ELECTRONICS |      1 |  10 |  19 |
|           7 | MP3 PLAYERS          |      6 |  11 |  14 |
|           8 | FLASH                |      7 |  12 |  13 |
|           9 | CD PLAYERS           |      6 |  15 |  16 |
|          10 | 2 WAY RADIOS         |      6 |  17 |  18 |
+-------------+----------------------+--------+-----+-----+
  • Chaque fois que vous avez besoin de tous les enfants d'un parent, vous interrogez simplement la colonne parent.
  • Si vous avez besoin de tous les descendants d'un parent, vous interrogez les éléments dont la lft est comprise entre lft et rgt du parent.
  • Si vous avez besoin de tous les parents d'un nœud jusqu'à la racine de l'arborescence, vous recherchez des éléments ayant lft inférieur à celui du nœud lft et rgt supérieur à celui du nœud rgt. et trier le par parent.

J'avais besoin de rendre l'accès et l'interrogation de l'arbre plus rapides que les insertions, c'est pourquoi j'ai choisi ceci

Le seul problème est de corriger les colonnes left et right lors de l'insertion de nouveaux éléments. Eh bien, j’ai créé une procédure stockée et l’appelle chaque fois que j’ai inséré un nouvel élément, ce qui était rare dans mon cas, mais c’est très rapide. J'ai eu l'idée du livre de Joe Celko, et la procédure stockée et la façon dont je l'ai créée sont expliquées ici dans DBA SE https://dba.stackexchange.com/q/89051/41481

19
azerafati

Si votre base de données prend en charge les tableaux, vous pouvez également implémenter une colonne de lignage ou un chemin matérialisé en tant que tableau d'identifiants parent.

Spécifiquement, avec Postgres, vous pouvez ensuite utiliser les opérateurs de l'ensemble pour interroger la hiérarchie et obtenir d'excellentes performances avec les index GIN. Cela rend la recherche des parents, des enfants et de la profondeur assez triviale en une seule requête. Les mises à jour sont également très gérables.

J'ai une description complète de l'utilisation de tableaux de chemins matérialisés si vous êtes curieux.

13
Adam Sanderson

C’est vraiment une question carrée, un trou rond.

Si les bases de données relationnelles et SQL sont le seul marteau que vous avez ou êtes prêt à utiliser, les réponses postées jusqu'à présent sont adéquates. Cependant, pourquoi ne pas utiliser un outil conçu pour gérer les données hiérarchiques? base de données Graph sont idéales pour les données hiérarchiques complexes.

Les inefficacités du modèle relationnel ainsi que les complexités de toute solution de code/requête pour mapper un modèle graphique/hiérarchique sur un modèle relationnel ne valent tout simplement pas la peine d'être tentées par rapport à la facilité avec laquelle une solution de base de données graphique peut résoudre le même problème.

Considérez une nomenclature comme une structure de données hiérarchique commune.

class Component extends Vertex {
    long assetId;
    long partNumber;
    long material;
    long amount;
};

class PartOf extends Edge {
};

class AdjacentTo extends Edge {
};

Le plus court chemin entre deux sous-assemblages : algorithme de traversée de graphe simple. Les chemins acceptables peuvent être qualifiés en fonction de critères.

Similarité : Quel est le degré de similitude entre deux assemblées? Effectuer une traversée sur les deux sous-arbres en calculant l'intersection et l'union des deux sous-arbres. Le pourcentage similaire est l'intersection divisée par le syndicat.

Fermeture transitive : parcourez le sous-arbre et récapitulez le (s) champ (s) d’intérêt, par exemple. "Combien y a-t-il d'aluminium dans un sous-assemblage?"

Oui, vous pouvez résoudre le problème avec SQL et une base de données relationnelle. Cependant, il existe de bien meilleures approches si vous êtes prêt à utiliser le bon outil pour le poste.

9
djhallx

J'utilise PostgreSQL avec des tables de fermeture pour mes hiérarchies. J'ai une procédure stockée universelle pour toute la base de données:

CREATE FUNCTION nomen_tree() RETURNS trigger
    LANGUAGE plpgsql
    AS $_$
DECLARE
  old_parent INTEGER;
  new_parent INTEGER;
  id_nom INTEGER;
  txt_name TEXT;
BEGIN
-- TG_ARGV[0] = name of table with entities with PARENT-CHILD relationships (TBL_ORIG)
-- TG_ARGV[1] = name of helper table with ANCESTOR, CHILD, DEPTH information (TBL_TREE)
-- TG_ARGV[2] = name of the field in TBL_ORIG which is used for the PARENT-CHILD relationship (FLD_PARENT)
    IF TG_OP = 'INSERT' THEN
    EXECUTE 'INSERT INTO ' || TG_ARGV[1] || ' (child_id,ancestor_id,depth) 
        SELECT $1.id,$1.id,0 UNION ALL
      SELECT $1.id,ancestor_id,depth+1 FROM ' || TG_ARGV[1] || ' WHERE child_id=$1.' || TG_ARGV[2] USING NEW;
    ELSE                                                           
    -- EXECUTE does not support conditional statements inside
    EXECUTE 'SELECT $1.' || TG_ARGV[2] || ',$2.' || TG_ARGV[2] INTO old_parent,new_parent USING OLD,NEW;
    IF COALESCE(old_parent,0) <> COALESCE(new_parent,0) THEN
      EXECUTE '
      -- prevent cycles in the tree
      UPDATE ' || TG_ARGV[0] || ' SET ' || TG_ARGV[2] || ' = $1.' || TG_ARGV[2]
        || ' WHERE id=$2.' || TG_ARGV[2] || ' AND EXISTS(SELECT 1 FROM '
        || TG_ARGV[1] || ' WHERE child_id=$2.' || TG_ARGV[2] || ' AND ancestor_id=$2.id);
      -- first remove edges between all old parents of node and its descendants
      DELETE FROM ' || TG_ARGV[1] || ' WHERE child_id IN
        (SELECT child_id FROM ' || TG_ARGV[1] || ' WHERE ancestor_id = $1.id)
        AND ancestor_id IN
        (SELECT ancestor_id FROM ' || TG_ARGV[1] || ' WHERE child_id = $1.id AND ancestor_id <> $1.id);
      -- then add edges for all new parents ...
      INSERT INTO ' || TG_ARGV[1] || ' (child_id,ancestor_id,depth) 
        SELECT child_id,ancestor_id,d_c+d_a FROM
        (SELECT child_id,depth AS d_c FROM ' || TG_ARGV[1] || ' WHERE ancestor_id=$2.id) AS child
        CROSS JOIN
        (SELECT ancestor_id,depth+1 AS d_a FROM ' || TG_ARGV[1] || ' WHERE child_id=$2.' 
        || TG_ARGV[2] || ') AS parent;' USING OLD, NEW;
    END IF;
  END IF;
  RETURN NULL;
END;
$_$;

Ensuite, pour chaque table où j'ai une hiérarchie, je crée un déclencheur

CREATE TRIGGER nomenclature_tree_tr AFTER INSERT OR UPDATE ON nomenclature FOR EACH ROW EXECUTE PROCEDURE nomen_tree('my_db.nomenclature', 'my_db.nom_helper', 'parent_id');

Pour remplir une table de fermeture à partir d'une hiérarchie existante, j'utilise cette procédure stockée:

CREATE FUNCTION rebuild_tree(tbl_base text, tbl_closure text, fld_parent text) RETURNS void
    LANGUAGE plpgsql
    AS $$
BEGIN
    EXECUTE 'TRUNCATE ' || tbl_closure || ';
    INSERT INTO ' || tbl_closure || ' (child_id,ancestor_id,depth) 
        WITH RECURSIVE tree AS
      (
        SELECT id AS child_id,id AS ancestor_id,0 AS depth FROM ' || tbl_base || '
        UNION ALL 
        SELECT t.id,ancestor_id,depth+1 FROM ' || tbl_base || ' AS t
        JOIN tree ON child_id = ' || fld_parent || '
      )
      SELECT * FROM tree;';
END;
$$;

Les tables de fermeture sont définies avec 3 colonnes - ANCESTOR_ID, DESCENDANT_ID, DEPTH. Il est possible (et je conseille même) de stocker des enregistrements avec la même valeur pour ANCESTOR et DESCENDANT, et une valeur de zéro pour DEPTH. Cela simplifiera les requêtes pour la récupération de la hiérarchie. Et ils sont vraiment très simples:

-- get all descendants
SELECT tbl_orig.*,depth FROM tbl_closure LEFT JOIN tbl_orig ON descendant_id = tbl_orig.id WHERE ancestor_id = XXX AND depth <> 0;
-- get only direct descendants
SELECT tbl_orig.* FROM tbl_closure LEFT JOIN tbl_orig ON descendant_id = tbl_orig.id WHERE ancestor_id = XXX AND depth = 1;
-- get all ancestors
SELECT tbl_orig.* FROM tbl_closure LEFT JOIN tbl_orig ON ancestor_id = tbl_orig.id WHERE descendant_id = XXX AND depth <> 0;
-- find the deepest level of children
SELECT MAX(depth) FROM tbl_closure WHERE ancestor_id = XXX;
5
IVO GELOV