it-swarm.com.de

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

Gute Übersichten

Im Allgemeinen treffen Sie eine Entscheidung zwischen schnellen Lesezeiten (z. B. verschachtelte Menge) und 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

Mir sind folgende allgemeine Merkmale bekannt:

  1. Adjacency List :
    • Spalten: ID, ParentID
    • Einfach zu implementieren.
    • Günstige Knoten verschieben, einfügen und löschen.
    • Teuer, um die Ebene, Vorfahren und Nachkommen, Pfad zu finden
    • Vermeiden Sie N + 1 über Common Table Expressions in Datenbanken, die diese unterstützen
  2. Nested Set (a.k.a Modified Preorder Tree Traversal )
    • Spalten: Links, rechts
    • Günstige Vorfahren, Nachkommen
    • Sehr teuer O(n/2) verschiebt, fügt ein, löscht aufgrund flüchtiger Codierung
  3. Bridge Table (a.k.a. Closure Table/w Trigger )
    • Verwendet eine separate Verknüpfungstabelle mit: Vorfahr, Nachfahre, Tiefe (optional)
    • Günstige Vorfahren und Nachkommen
    • Schreibt Kosten O(log n) (Größe des Teilbaums) für Einfügen, Aktualisieren, Löschen
    • Normalisierte Codierung: Gut für RDBMS-Statistiken und Abfrageplaner in Joins
    • Erfordert mehrere Zeilen pro Knoten
  4. Lineage Column (a.k.a. Materialized Path , Path Enumeration)
    • Spalte: Abstammung (z. B./Eltern/Kind/Enkel/etc ...)
    • Günstige Nachkommen über Präfixabfrage (z. B. LEFT(lineage, #) = '/enumerated/path')
    • Schreibt Kosten O(log n) (Größe des Teilbaums) für Einfügen, Aktualisieren, Löschen
    • Nicht relational: Verwendet den Array-Datentyp oder das serialisierte Zeichenfolgenformat
  5. verschachtelte Intervalle
    • Wie verschachtelter Satz, jedoch mit real/float/decimal, damit die Codierung nicht flüchtig wird (kostengünstiges Verschieben/Einfügen/Löschen)
    • Hat Real/Float/Dezimal-Darstellung/Genauigkeitsprobleme
    • Matrixkodierungsvariante fügt eine Ahnenkodierung (materialisierter Pfad) für "frei" hinzu, jedoch mit einer zusätzlichen Schwierigkeit der linearen Algebra.
  6. Flacher Tisch
    • Eine geänderte Adjacency-Liste, die jedem Datensatz eine Spalte für Stufe und Rang (z. B. Reihenfolge) hinzufügt.
    • Günstig zu iterieren/zu paginieren
    • Teuer verschieben und löschen
    • Gute Verwendung: Diskussion über Themen - Foren/Blog-Kommentare
  7. Spalten mit mehreren Abstammungslinien
    • Spalten: eine für jede Abstammungsstufe, bezieht sich auf alle Eltern bis zur Wurzel, Stufen ab der Stufe des Gegenstands werden auf NULL gesetzt
    • Billige Vorfahren, Nachkommen, Niveau
    • Günstiges Einfügen, Löschen, Verschieben der Blätter
    • Teures Einfügen, Löschen, Verschieben der internen Knoten
    • Harte Grenze, wie tief die Hierarchie sein kann

Datenbankspezifische Hinweise

MySQL

Oracle

  • Verwenden Sie CONNECT BY , um Adjacency-Listen zu durchlaufen

PostgreSQL

SQL Server

1248
orangepips

Meine Lieblingsantwort ist der erste Satz in diesem Thread. Verwenden Sie eine Adjacency-Liste, um die Hierarchie zu verwalten, und verwenden Sie verschachtelte Gruppen, um die Hierarchie abzufragen.

Bisher bestand das Problem darin, dass die Cover-Methode von einer Adjacecy-Liste zu verschachtelten Sätzen furchtbar langsam war, da die meisten Benutzer die extreme RBAR-Methode, die als "Push-Stack" bezeichnet wird, für die Konvertierung verwenden und dies als viel zu teuer angesehen wurde um das Nirwana von der Einfachheit der Wartung durch die Adjacency List und der großartigen Leistung von Nested Sets zu erreichen. Infolgedessen müssen sich die meisten Menschen mit dem einen oder anderen zufrieden geben, insbesondere wenn es mehr als etwa 100.000 Knoten gibt. Die Verwendung der Push-Stack-Methode kann einen ganzen Tag dauern, um die Konvertierung für MLMs durchzuführen, die als Hierarchie mit einer kleinen Million Knoten betrachtet werden.

Ich dachte, ich würde Celko ein wenig Konkurrenz machen, indem ich mir eine Methode überlege, um eine Adjacency-Liste in verschachtelte Sets mit einer Geschwindigkeit umzuwandeln, die einfach unmöglich erscheint. 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 konvertiert und 100.000 Knoten in weniger als 4 Sekunden.

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 Leute, die Stücklisten erstellen, werden sich besonders für diesen Artikel interessieren. http://www.sqlservercentral.com/articles/T-SQL/94570/

Wenn Sie sich einen der Artikel ansehen möchten, klicken Sie auf den Link "An der Diskussion teilnehmen" und teilen Sie mir Ihre Meinung mit.

60
Jeff Moden

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 "Modellieren Sie Ihre Datenhierarchien mit SQL Server 2008" von Kent Tegels auf MSDN für Starts an. Siehe auch meine eigene Frage: Rekursive Gleiche-Tabelle-Abfrage in SQL Server 2008

31
CesarGon

Dieser Entwurf wurde noch nicht erwähnt:

Spalten mit mehreren Abstammungslinien

Obwohl es Einschränkungen gibt, ist es sehr einfach und sehr effizient, wenn man sie erträgt. Eigenschaften:

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

Hier folgt ein Beispiel - taxonomischer Baum der Vögel, daher lautet die Hierarchie Klasse/Ordnung/Familie/Gattung/Art - Art ist die unterste 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 in der Baumstruktur nicht ändern.

27
TMS

Adjacency Model + Nested Sets Model

Ich habe mich dafür entschieden, weil ich problemlos neue Elemente in den Baum einfügen konnte (Sie brauchen 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 Kinder eines Elternteils benötigen, fragen Sie einfach die Spalte parent ab.
  • Wenn Sie alle Nachkommen eines übergeordneten Elements benötigen, fragen Sie nach Elementen, deren lft zwischen lft und rgt des übergeordneten Elements liegt.
  • Wenn Sie alle übergeordneten Elemente eines Knotens bis zur Wurzel des Baums benötigen, fragen Sie nach Elementen ab, deren lft niedriger als die lft und rgt des Knotens größer als die rgt sind. und sortiere die nach parent.

Ich musste schneller auf den Baum zugreifen und ihn abfragen als Einfügungen. Deshalb habe ich dies gewählt.

Das einzige Problem besteht darin, die Spalten left und right beim 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 einfügte, 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 erklärt https://dba.stackexchange.com/q/89051/41481

19
azerafati

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

Speziell bei 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 Tiefen 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.

13
Adam Sanderson

Dies ist wirklich eine quadratische Frage mit runden Löchern.

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, mit dem hierarchische Daten verarbeitet werden können. Graph database sind ideal für komplexe hierarchische Daten.

Die Ineffizienzen des relationalen Modells und die Komplexität einer Code-/Abfragelösung zur Abbildung eines Diagramms/hierarchischen Modells auf ein relationales Modell sind im Vergleich zu der Einfachheit, mit der eine Diagrammdatenbanklösung dasselbe 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 Pfad zwischen zwei Unterbaugruppen : Einfacher Diagrammdurchlaufalgorithmus. Akzeptable Pfade können anhand von Kriterien qualifiziert werden.

Ähnlichkeit : Wie ähnlich sind zwei Baugruppen? Durchlaufen Sie beide Teilbäume, indem Sie den Schnittpunkt und die Vereinigung der beiden Teilbäume berechnen. Der ähnliche Prozentsatz ist der Schnittpunkt geteilt durch die Vereinigung.

Transitive Closure : Gehen Sie durch den Unterbaum und addieren Sie das/die interessierende (n) Feld (er), z. "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.

9
djhallx

Ich verwende PostgreSQL mit Abschlusstabellen 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 die folgende 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;
$$;

Abschlusstabellen werden mit drei 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;
5
IVO GELOV