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:
O(n/2)
se déplace, insère, supprime en raison d'un encodage volatileO(log n)
(taille de la sous-arborescence) pour l’insertion, la mise à jour, la suppressionLEFT(lineage, #) = '/enumerated/path'
)O(log n)
(taille de la sous-arborescence) pour l’insertion, la mise à jour, la suppressionRemarques spécifiques à la base de données
MySQL
Oracle
PostgreSQL
SQL Server
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.
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:
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
Ce dessin n'a pas encore été mentionné:
Bien qu'il y ait des limites, si vous pouvez les supporter, c'est très simple et très efficace. Fonctionnalités:
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.
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 |
+-------------+----------------------+--------+-----+-----+
parent
.lft
est comprise entre lft
et rgt
du parent.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
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.
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.
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;