Welche Möglichkeiten gibt es, um hierarchische Daten in einer relationalen Datenbank zu speichern? [geschlossen]

1333

Gute Übersichten

Im Allgemeinen treffen Sie eine Entscheidung zwischen schnellen Lesezeiten (z. B. verschachtelter Satz) oder schnellen Schreibzeiten (Adjazenzliste). Normalerweise erhalten Sie eine Kombination der folgenden Optionen, die Ihren Anforderungen am besten entspricht. Im Folgenden finden Sie einige ausführliche Informationen:

Optionen

Eine, die mir bekannt ist und allgemeine Merkmale:

  1. Adjazenzliste :
    • Spalten: ID, ParentID
    • Einfach zu implementieren.
    • Günstiger Knoten wird verschoben, eingefügt und gelöscht.
    • Teuer, um das Level, die Abstammung und die Nachkommen, den Pfad zu finden
    • Vermeiden Sie N + 1 über Common Table Expressions in Datenbanken, die diese unterstützen
  2. Verschachteltes Set (auch bekannt als Modified Preorder Tree Traversal )
    • Spalten: Links, rechts
    • Billige Abstammung, Nachkommen
    • Sehr teure O(n/2)Verschiebungen, Einfügungen, Löschungen aufgrund flüchtiger Codierung
  3. Brückentabelle (auch bekannt als Closure Table / w-Trigger )
    • Verwendet eine separate Verknüpfungstabelle mit: Vorfahr, Nachkomme, Tiefe (optional)
    • Billige Vorfahren und Nachkommen
    • Schreibt Kosten O(log n)(Größe des Teilbaums) für das Einfügen, Aktualisieren und Löschen
    • Normalisierte Codierung: Gut für RDBMS-Statistiken und Abfrageplaner in Joins
    • Erfordert mehrere Zeilen pro Knoten
  4. Abstammungsspalte (auch bekannt als Materialized Path , Path Enumeration)
    • Spalte: Abstammung (zB / Elternteil / Kind / Enkel / etc ...)
    • Günstige Nachkommen per Präfixabfrage (zB LEFT(lineage, #) = '/enumerated/path')
    • Schreibt Kosten O(log n)(Größe des Teilbaums) für das Einfügen, Aktualisieren und Löschen
    • Nicht relational: basiert auf dem Array-Datentyp oder dem serialisierten Zeichenfolgenformat
  5. Verschachtelte Intervalle
    • Wie verschachtelte Menge, jedoch mit real / float / decimal, damit die Codierung nicht flüchtig ist (kostengünstiges Verschieben / Einfügen / Löschen)
    • Hat Real / Float / Dezimaldarstellung / Präzisionsprobleme
    • Die Matrixcodierungsvariante fügt die Ahnencodierung (materialisierter Pfad) "kostenlos" hinzu, jedoch mit zusätzlicher Schwierigkeit der linearen Algebra.
  6. Flacher Tisch
    • Eine modifizierte Adjazenzliste, die jedem Datensatz eine Spalte für Stufe und Rang (z. B. Reihenfolge) hinzufügt.
    • Günstig zu iterieren / paginieren
    • Teuer verschieben und löschen
    • Gute Verwendung: Diskussionsfäden - Foren / Blog-Kommentare
  7. Mehrere Abstammungsspalten
    • Spalten: Eine für jede Abstammungsstufe bezieht sich auf alle Eltern bis zur Wurzel, Stufen von der Stufe des Gegenstands werden auf NULL gesetzt
    • Billige Vorfahren, Nachkommen, Level
    • Billig einfügen, löschen, bewegen der Blätter
    • Teures Einfügen, Löschen, Verschieben der internen Knoten
    • Harte Grenze, wie tief die Hierarchie sein kann

Datenbankspezifische Hinweise

MySQL

Orakel

  • Verwenden Sie CONNECT BY , um Adjazenzlisten zu durchlaufen

PostgreSQL

SQL Server

Orangepips
quelle
5
Nach slideshare.net/billkarwin/sql-antipatterns-strike-back Seite 77, Closure Tablesüberlegen sind Adjacency List, Path Enumerationund Nested Setsin Bezug auf die Benutzerfreundlichkeit (und ich nehme an Leistung als auch).
Gili
Ich vermisse hier eine sehr einfache Version: ein einfaches BLOB. Wenn Ihre Hierarchie nur wenige Dutzend Elemente enthält, ist ein serialisierter Baum von IDs möglicherweise die beste Option.
Lothar
@ Lothar: Frage ist ein Community-Wiki, also zögern Sie nicht, es zu haben. Meiner Meinung nach würde ich dies nur mit Datenbanken tun, die eine Art Blob-Strukturierung wie XML mit einer stabilen Abfragesprache wie XPATH unterstützen. Ansonsten sehe ich keine gute Möglichkeit zum Abfragen, abgesehen vom Abrufen, Deserialisieren und Eintauchen in Code, nicht in SQL. Und wenn Sie wirklich ein Problem haben, bei dem Sie viele beliebige Elemente benötigen, ist es möglicherweise besser, eine Knotendatenbank wie Neo4J zu verwenden, die ich verwendet und gemocht habe, obwohl sie nie in die Produktion übernommen wurde.
Orangepips
2
Dieser MSDN-Link für "Allgemeine Zusammenfassung" zeigt den Artikel nicht mehr an. Es war in der September 2008-Ausgabe des MSDN-Magazins, das Sie als CHM-Datei herunterladen oder über das Webarchiv unter web.archive.org/web/20080913041559/http://msdn.microsoft.com:80/ einsehen können. …
kͩeͣmͮpͥ ͩ

Antworten:

66

Meine Lieblingsantwort ist wie im ersten Satz dieses Threads vorgeschlagen. Verwenden Sie eine Adjazenzliste, um die Hierarchie zu verwalten, und verwenden Sie verschachtelte Mengen, um die Hierarchie abzufragen.

Das Problem bestand bisher darin, dass die Coversion-Methode von einer Adjacecy-Liste zu verschachtelten Sets furchtbar langsam war, da die meisten Leute die extreme RBAR-Methode verwenden, die als "Push Stack" bekannt ist, um die Konvertierung durchzuführen, und als viel zu teuer angesehen wurde um das Nirvana der Einfachheit der Wartung durch die Adjacency List und der beeindruckenden Leistung verschachtelter Sets zu erreichen. Infolgedessen müssen sich die meisten Menschen mit dem einen oder anderen zufrieden geben, insbesondere wenn es mehr als beispielsweise miese 100.000 Knoten gibt. Die Verwendung der Push-Stack-Methode kann einen ganzen Tag dauern, um die Konvertierung für eine MLM-Hierarchie von kleinen Millionen Knoten durchzuführen.

Ich dachte, ich würde Celko ein bisschen Konkurrenz machen, indem ich eine Methode entwickeln würde, um eine Adjazenzliste in verschachtelte Sätze mit Geschwindigkeiten umzuwandeln, die einfach unmöglich erscheinen. Hier ist die Leistung der Push-Stack-Methode auf meinem i5-Laptop.

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'

Und hier ist die Dauer für die neue Methode (mit der Push-Stack-Methode in Klammern).

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!!!)

Ja das ist richtig. 1 Million Knoten in weniger als einer Minute und 100.000 Knoten in weniger als 4 Sekunden konvertiert.

Sie können sich über die neue Methode informieren und eine Kopie des Codes unter der folgenden URL erhalten. http://www.sqlservercentral.com/articles/Hierarchy/94040/

Ich habe auch eine "voraggregierte" Hierarchie mit ähnlichen Methoden entwickelt. MLM'er und Personen, die Stücklisten erstellen, werden an diesem Artikel besonders interessiert sein. http://www.sqlservercentral.com/articles/T-SQL/94570/

Wenn Sie vorbeischauen, um sich einen der Artikel anzusehen, springen Sie in den Link "An der Diskussion teilnehmen" und teilen Sie mir Ihre Meinung mit.

Jeff Moden
quelle
Was ist ein MLMer?
David Mann
MLM = "Multi-Level-Marketing". Amway, Shaklee, ACN usw. usw.
Jeff Moden
31

Dies ist eine sehr teilweise Antwort auf Ihre Frage, aber ich hoffe immer noch nützlich.

Microsoft SQL Server 2008 implementiert zwei Funktionen, die für die Verwaltung hierarchischer Daten äußerst nützlich sind:

  • der HierarchyId- Datentyp.
  • allgemeine Tabellenausdrücke mit dem Schlüsselwort with .

Schauen Sie sich für Starts "Modellieren Ihrer Datenhierarchien mit SQL Server 2008" von Kent Tegels auf MSDN an. Siehe auch meine eigene Frage: Rekursive Abfrage derselben Tabelle in SQL Server 2008

CesarGon
quelle
2
Interessanterweise wusste die HierarchyId nichts davon: msdn.microsoft.com/en-us/library/bb677290.aspx
orangepips
1
Tatsächlich. Ich arbeite mit vielen rekursiv hierarchischen Daten und finde allgemeine Tabellenausdrücke äußerst nützlich. Eine Einführung finden Sie unter msdn.microsoft.com/en-us/library/ms186243.aspx .
CesarGon
28

Dieser Entwurf wurde noch nicht erwähnt:

Mehrere Abstammungsspalten

Obwohl es Einschränkungen gibt, ist es sehr einfach und sehr effizient, wenn Sie sie ertragen können. Eigenschaften:

  • Spalten: Eine für jede Abstammungsstufe bezieht sich auf alle Eltern bis zur Wurzel. Stufen unterhalb der Stufe der aktuellen Elemente werden auf 0 (oder NULL) gesetzt.
  • Es gibt eine feste Grenze für die Tiefe der Hierarchie
  • Billige Vorfahren, Nachkommen, Level
  • Billig einfügen, löschen, bewegen der Blätter
  • Teures Einfügen, Löschen, Verschieben der internen Knoten

Hier folgt ein Beispiel - taxonomischer Vogelbaum, daher ist die Hierarchie Klasse / Ordnung / Familie / Gattung / Art - Art ist die niedrigste Ebene, 1 Zeile = 1 Taxon (was im Fall der Blattknoten der Art entspricht):

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 ''
);

und das Beispiel der Daten:

+---------+---------+---------+----------+---------+-------------------------------+
| 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                   |

Dies ist großartig, da Sie auf diese Weise alle erforderlichen Vorgänge auf sehr einfache Weise ausführen können, solange die internen Kategorien ihre Ebene im Baum nicht ändern.

TMS
quelle
22

Adjazenzmodell + Modell verschachtelter Mengen

Ich habe mich dafür entschieden, weil ich einfach neue Elemente in den Baum einfügen konnte (Sie benötigen nur die ID eines Zweigs, um ein neues Element einzufügen) und es auch ziemlich schnell abfragen konnte.

+-------------+----------------------+--------+-----+-----+
| 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 |
+-------------+----------------------+--------+-----+-----+
  • Jedes Mal, wenn Sie alle untergeordneten Elemente eines Elternteils benötigen, fragen Sie einfach die parentSpalte ab.
  • Wenn Sie alle Nachkommen eines übergeordneten Elements benötigen, fragen Sie nach Elementen ab, deren lftZwischen- lftund rgtübergeordnetes Element vorhanden sind .
  • Wenn Sie alle übergeordneten Knoten eines Knotens bis zur Wurzel des Baums benötigen, fragen Sie nach Elementen, die lftkleiner als der Knoten lftund rgtgrößer als der Knoten sind, rgtund sortieren die nach parent.

Ich musste den Zugriff auf und das Abfragen des Baums schneller als das Einfügen machen, deshalb habe ich mich dafür entschieden

Das einzige Problem besteht darin, die Spalten leftund rightbeim Einfügen neuer Elemente zu korrigieren. Nun, ich habe eine gespeicherte Prozedur dafür erstellt und sie jedes Mal aufgerufen, wenn ich ein neues Element eingefügt habe, was in meinem Fall selten war, aber sehr schnell ist. Ich habe die Idee aus dem Buch von Joe Celko, und die gespeicherte Prozedur und wie ich darauf gekommen bin, wird hier in DBA SE https://dba.stackexchange.com/q/89051/41481 erklärt

Azerafati
quelle
3
+1 Dies ist ein legitimer Ansatz. Aus meiner eigenen Erfahrung entscheidet der Schlüssel, ob Sie mit Dirty Reads einverstanden sind, wenn große Aktualisierungsvorgänge auftreten. Wenn nicht, wird es zu einer Angelegenheit oder verhindert, dass Personen Tabellen direkt abfragen und immer eine API durchlaufen - DB-Sprocs / -Funktionen oder -Code.
orangepips
1
Dies ist eine interessante Lösung; Ich bin mir jedoch nicht sicher, ob das Abfragen der übergeordneten Spalte wirklich einen großen Vorteil bietet, wenn versucht wird, untergeordnete Spalten zu finden. Deshalb haben wir zunächst die linke und die rechte Spalte.
Thomas
2
@ Thomas, es gibt einen Unterschied zwischen childrenund descendants. leftund rightwerden verwendet, um die Nachkommen zu finden.
Azerafati
14

Wenn Ihre Datenbank Arrays unterstützt, können Sie auch eine Abstammungsspalte oder einen materialisierten Pfad als Array übergeordneter IDs implementieren.

Speziell mit Postgres können Sie dann die Set-Operatoren verwenden, um die Hierarchie abzufragen und mit GIN-Indizes eine hervorragende Leistung zu erzielen. Dies macht das Finden von Eltern, Kindern und Tiefe in einer einzigen Abfrage ziemlich trivial. Updates sind auch ziemlich überschaubar.

Ich habe eine vollständige Beschreibung der Verwendung von Arrays für materialisierte Pfade, wenn Sie neugierig sind.

Adam Sanderson
quelle
9

Dies ist wirklich eine Frage mit quadratischem Stift und rundem Loch.

Wenn relationale Datenbanken und SQL der einzige Hammer sind, den Sie haben oder verwenden möchten, sind die bisher veröffentlichten Antworten angemessen. Verwenden Sie jedoch ein Tool für den Umgang mit hierarchischen Daten. Die Grafikdatenbank ist ideal für komplexe hierarchische Daten.

Die Ineffizienzen des relationalen Modells zusammen mit der Komplexität einer Code- / Abfragelösung zum Abbilden eines Diagramms / hierarchischen Modells auf ein relationales Modell sind im Vergleich zu der Leichtigkeit, mit der eine Diagrammdatenbanklösung das gleiche Problem lösen kann, einfach nicht die Mühe wert.

Betrachten Sie eine Stückliste als eine gemeinsame hierarchische Datenstruktur.

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

class PartOf extends Edge {
};

class AdjacentTo extends Edge {
};

Kürzester Weg zwischen zwei Unterbaugruppen : Einfacher Graph-Traversal-Algorithmus. Akzeptable Pfade können anhand von Kriterien qualifiziert werden.

Ähnlichkeit : Wie groß ist die Ähnlichkeit zwischen zwei Baugruppen? Führen Sie eine Durchquerung beider Teilbäume durch und berechnen Sie den Schnittpunkt und die Vereinigung der beiden Teilbäume. Der Prozentsatz ähnlich ist der Schnittpunkt geteilt durch die Gewerkschaft.

Transitive Schließung : Gehen Sie über den Unterbaum und fassen Sie die interessierenden Felder zusammen, z. B. "Wie viel Aluminium befindet sich in einer Unterbaugruppe?"

Ja, Sie können das Problem mit SQL und einer relationalen Datenbank lösen. Es gibt jedoch viel bessere Ansätze, wenn Sie bereit sind, das richtige Werkzeug für den Job zu verwenden.

djhallx
quelle
5
Diese Antwort wäre immens nützlicher, wenn die Anwendungsfälle zeigen oder besser noch kontrastieren würden, wie eine Diagrammdatenbank beispielsweise mit SPARQL anstelle von SQL in einem RDBMS abgefragt werden kann.
Orangepips
1
SPARQL ist relevant für RDF-Datenbanken, die eine Unterklasse der größeren Domäne von Graphendatenbanken darstellen. Ich arbeite mit InfiniteGraph, das keine RDF-Datenbank ist und derzeit SPARQL nicht unterstützt. InfiniteGraph unterstützt verschiedene Abfragemechanismen: (1) eine Diagrammnavigations-API zum Einrichten von Ansichten, Filtern, Pfadqualifizierern und Ergebnishandlern, (2) eine komplexe Sprache zum Abgleichen von Diagrammpfadmustern und (3) Gremlin.
DJhallx
6

Ich verwende PostgreSQL mit Schließungstabellen für meine Hierarchien. Ich habe eine universelle gespeicherte Prozedur für die gesamte Datenbank:

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;
$_$;

Dann erstelle ich für jede Tabelle, in der ich eine Hierarchie habe, einen Trigger

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');

Zum Auffüllen einer Abschlusstabelle aus einer vorhandenen Hierarchie verwende ich diese gespeicherte Prozedur:

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;
$$;

Schließungstabellen werden mit 3 Spalten definiert - ANCESTOR_ID, DESCENDANT_ID, DEPTH. Es ist möglich (und ich rate sogar), Datensätze mit demselben Wert für ANCESTOR und DESCENDANT und einem Wert von Null für DEPTH zu speichern. Dies vereinfacht die Abfragen zum Abrufen der Hierarchie. Und sie sind in der Tat sehr einfach:

-- 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;
IVO GELOV
quelle