Quali sono le opzioni per la memorizzazione di dati gerarchici in un database relazionale? [chiuso]


1334

Buone panoramiche

In generale, stai prendendo una decisione tra tempi di lettura rapidi (ad esempio, set nidificato) o tempi di scrittura rapidi (elenco di adiacenza). Di solito, si finisce con una combinazione delle opzioni di seguito che meglio si adattano alle proprie esigenze. Di seguito vengono fornite alcune letture approfondite:

Opzioni

Quelli di cui sono a conoscenza e caratteristiche generali:

  1. Elenco di adiacenza :
    • Colonne: ID, ParentID
    • Facile da implementare.
    • Spostamenti, inserimenti ed eliminazioni di nodi economici.
    • Costoso per trovare il livello, gli antenati e i discendenti, il percorso
    • Evita N + 1 tramite espressioni di tabella comuni nei database che le supportano
  2. Set nidificato (aka Traversal Tree Preorder modificato )
    • Colonne: sinistra, destra
    • Antenati economici, discendenti
    • Spostamenti O(n/2), inserimenti, eliminazioni molto costosi a causa della codifica volatile
  3. Bridge Table (aka Closure Table / w trigger )
    • Utilizza una tabella di join separata con: antenato, discendente, profondità (opzionale)
    • Antenati e discendenti economici
    • Scrive i costi O(log n)(dimensioni della sottostruttura) per inserimenti, aggiornamenti, eliminazioni
    • Codifica normalizzata: buona per statistiche RDBMS e pianificatore di query nei join
    • Richiede più righe per nodo
  4. Colonna di lignaggio (aka Path Materialized , Path Enumeration)
    • Colonna: lignaggio (ad es. / Genitore / figlio / nipote / ecc ...)
    • Discendenti economici tramite query prefisso (ad es. LEFT(lineage, #) = '/enumerated/path')
    • Scrive i costi O(log n)(dimensioni della sottostruttura) per inserimenti, aggiornamenti, eliminazioni
    • Non relazionale: si basa sul tipo di dati array o sul formato stringa serializzato
  5. Intervalli nidificati
    • Come set nidificato, ma con real / float / decimal in modo che la codifica non sia volatile (spostamento / inserimento / eliminazione economico)
    • Ha problemi di rappresentazione / precisione decimale / float / decimale
    • La variante di codifica Matrix aggiunge la codifica degli antenati (percorso materializzato) per "libero", ma con una maggiore delicatezza dell'algebra lineare.
  6. Tavolo piatto
    • Un elenco di adiacenza modificato che aggiunge una colonna Level e Rank (ad es. Ordinamento) a ciascun record.
    • Economico per iterare / impaginare
    • Spostamento ed eliminazione costosi
    • Buon uso: discussione discussione - forum / commenti sul blog
  7. Più colonne di lignaggio
    • Colonne: una per ogni livello di lignaggio, si riferisce a tutti i genitori fino alla radice, i livelli inferiori dal livello dell'elemento sono impostati su NULL
    • Antenati economici, discendenti, livello
    • Inserimento economico, cancellazione, spostamento delle foglie
    • Inserimento, eliminazione, spostamento costosi dei nodi interni
    • Difficile limite alla profondità della gerarchia

Note specifiche sul database

MySQL

Oracolo

  • Utilizzare CONNECT BY per attraversare gli elenchi di adiacenza

PostgreSQL

server SQL

  • Riepilogo generale
  • Il 2008 offre HierarchyId il tipo di dati sembra aiutare con l'approccio Colonna Lignea ed espandere la profondità che può essere rappresentata.

5
Secondo slideshare.net/billkarwin/sql-antipatterns-strike-back pagina 77, Closure Tablessono superiori a Adjacency List, Path Enumeratione Nested Setsin termini di facilità d'uso (e sto cercando di indovinare le prestazioni pure).
Gili

Mi manca una versione molto semplice qui: un semplice BLOB. Se la tua gerarchia ha solo alcuni elementi dozend, un albero serializzato di ID potrebbe essere l'opzione migliore.
Lothar,

@Lothar: la domanda è un wiki della comunità, quindi sentiti libero di averlo. Il mio pensiero al riguardo è che lo farei solo con quei database che supportano una sorta di strutturazione di BLOB come XML con un linguaggio di query stabile come XPATH. Altrimenti non vedo un buon modo di interrogare oltre a recuperare, deserializzare e affondare nel codice, non SQL. E se hai davvero un problema in cui hai bisogno di molti elementi arbitrari, potresti stare meglio usando il database Node come Neo4J, che ho usato e apprezzato, anche se mai portato alla produzione.
orangepips,


2
Quel collegamento MSDN per "Riepilogo generale" non mostra più l'articolo. Era nell'edizione di settembre 2008 di MSDN Magazine, che è possibile scaricare come file CHM o consultare l'archivio Web all'indirizzo web.archive.org/web/20080913041559/http://msdn.microsoft.com:80/ …
kͩeͣmͮpͥ ͩ

Risposte:


66

La mia risposta preferita è come suggerito dalla prima frase di questa discussione. Utilizzare un elenco di adiacenza per mantenere la gerarchia e utilizzare set nidificati per interrogare la gerarchia.

Il problema fino ad ora è stato che il metodo di copertura da un elenco di adiacenza a set nidificati è stato spaventosamente lento perché la maggior parte delle persone usa il metodo RBAR estremo noto come "Push Stack" per fare la conversione ed è stato considerato molto costoso per raggiungere il Nirvana della semplicità di manutenzione dall'elenco adiacenza e la straordinaria performance dei set nidificati. Di conseguenza, la maggior parte delle persone finisce per accontentarsi dell'uno o dell'altro, specialmente se ci sono più di, per esempio, un pessimo 100.000 nodi. L'uso del metodo push stack può richiedere un'intera giornata per eseguire la conversione su ciò che gli MLM considererebbero una piccola gerarchia di milioni di nodi.

Ho pensato di dare a Celko un po 'di competizione inventando un metodo per convertire un elenco di adiacenza in set nidificati a velocità che sembrano semplicemente impossibili. Ecco le prestazioni del metodo push stack sul mio laptop 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'

Ed ecco la durata del nuovo metodo (con il metodo push stack tra parentesi).

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

Sì, è corretto. 1 milione di nodi convertiti in meno di un minuto e 100.000 nodi in meno di 4 secondi.

Puoi leggere il nuovo metodo e ottenere una copia del codice al seguente URL. http://www.sqlservercentral.com/articles/Hierarchy/94040/

Ho anche sviluppato una gerarchia "pre-aggregata" usando metodi simili. Gli utenti di MLM e le persone che producono distinte materiali saranno particolarmente interessati a questo articolo. http://www.sqlservercentral.com/articles/T-SQL/94570/

Se ti fermi a dare un'occhiata a uno dei due articoli, passa al link "Partecipa alla discussione" e fammi sapere cosa ne pensi.


Che cos'è un MLMer?
David Mann,

MLM = "Marketing multilivello". Amway, Shaklee, ACN, ecc. Ecc.
Jeff Moden,

31

Questa è una risposta molto parziale alla tua domanda, ma spero ancora utile.

Microsoft SQL Server 2008 implementa due funzionalità estremamente utili per la gestione dei dati gerarchici:

  • il tipo di dati HierarchyId .
  • espressioni di tabella comuni, usando la parola chiave with .

Dai un'occhiata a "Modella le tue gerarchie di dati con SQL Server 2008" di Kent Tegels su MSDN per iniziare. Vedi anche la mia domanda: query ricorsiva sulla stessa tabella in SQL Server 2008


2
Interessante, il HierarchyId, non lo sapeva: msdn.microsoft.com/en-us/library/bb677290.aspx
orangepips,

1
Infatti. Lavoro con molti dati gerarchici ricorsivi e trovo estremamente utili le espressioni di tabella comuni. Vedere msdn.microsoft.com/en-us/library/ms186243.aspx per un'introduzione .
CesarGon,

28

Questo disegno non è stato ancora menzionato:

Più colonne di lignaggio

Sebbene abbia dei limiti, se puoi sopportarli, è molto semplice e molto efficiente. Caratteristiche:

  • Colonne: una per ogni livello di lignaggio, si riferisce a tutti i genitori fino alla radice, i livelli inferiori al livello degli elementi correnti sono impostati su 0 (o NULL)
  • C'è un limite fisso alla profondità della gerarchia
  • Antenati economici, discendenti, livello
  • Inserimento economico, cancellazione, spostamento delle foglie
  • Inserimento, eliminazione, spostamento costosi dei nodi interni

Ecco un esempio - albero tassonomico degli uccelli, quindi la gerarchia è Classe / Ordine / Famiglia / Genere / Specie - La specie è il livello più basso, 1 riga = 1 taxon (che corrisponde alle specie nel caso dei nodi fogliari):

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

e l'esempio dei dati:

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

Questo è fantastico perché in questo modo realizzi tutte le operazioni necessarie in un modo molto semplice, purché le categorie interne non cambino il loro livello nella struttura.


22

Modello di adiacenza + modello di set nidificati

Ci sono andato perché potevo inserire facilmente nuovi elementi nell'albero (hai solo bisogno dell'ID di un ramo per inserire un nuovo elemento) e anche interrogarlo abbastanza velocemente.

+-------------+----------------------+--------+-----+-----+
| 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 |
+-------------+----------------------+--------+-----+-----+
  • Ogni volta che hai bisogno di tutti i figli di qualsiasi genitore devi solo interrogare la parentcolonna.
  • Se hai bisogno di tutti i discendenti di qualsiasi genitore, richiedi elementi che hanno il loro lft tralft e trargt .
  • Se hai bisogno di tutti i genitori di qualsiasi nodo fino alla radice dell'albero, esegui una query per elementi che hanno un valore lftinferiore a quello del nodo lftergt più grandi di quello del nodo rgte ordina per parent.

Avevo bisogno di accedere e interrogare l'albero più velocemente degli inserti, ecco perché ho scelto questo

L'unico problema è correggere le colonne lefte rightquando si inseriscono nuovi elementi. bene ho creato una procedura memorizzata per questo e l'ho chiamata ogni volta che ho inserito un nuovo elemento che era raro nel mio caso ma è molto veloce. Ho preso l'idea dal libro di Joe Celko, e la procedura memorizzata e come me ne sono venuta in mente è spiegata qui in DBA SE https://dba.stackexchange.com/q/89051/41481


3
+1 questo è un approccio legittimo. Dalla mia esperienza personale, la chiave sta decidendo se stai bene con letture sporche quando si verificano grandi operazioni di aggiornamento. In caso contrario, diventa una questione o impedisce alle persone di interrogare direttamente le tabelle e passare sempre attraverso un API - DB sprocs / funzioni o codice.
orangepips,

1
Questa è una soluzione interessante; tuttavia, non sono sicuro che interrogare la colonna genitore offra davvero un grande vantaggio quando si cerca di trovare figli: ecco perché abbiamo le colonne sinistra e destra, in primo luogo.
Thomas

2
@Thomas, c'è una differenza tra childrene descendants. lefte rightsono usati per trovare i discendenti.
Azerafati,

14

Se il database supporta array, è anche possibile implementare una colonna di derivazione o un percorso materializzato come una matrice di ID parent.

In particolare con Postgres è quindi possibile utilizzare gli operatori set per interrogare la gerarchia e ottenere prestazioni eccellenti con gli indici GIN. Questo rende la ricerca di genitori, figli e profondità piuttosto banali in una singola query. Anche gli aggiornamenti sono abbastanza gestibili.

Ho una descrizione completa dell'uso di array per percorsi materializzati se sei curioso.


9

Questa è davvero una spina quadrata, una domanda a foro tondo.

Se i database relazionali e SQL sono l'unico martello che hai o sei disposto a usare, allora le risposte che sono state postate finora sono adeguate. Tuttavia, perché non utilizzare uno strumento progettato per gestire i dati gerarchici? Il database dei grafici è ideale per dati gerarchici complessi.

Le inefficienze del modello relazionale insieme alle complessità di qualsiasi soluzione di codice / query per mappare un modello grafico / gerarchico su un modello relazionale non valgono la pena rispetto alla facilità con cui una soluzione di database grafico può risolvere lo stesso problema.

Considera una distinta materiali come una struttura di dati gerarchica comune.

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

class PartOf extends Edge {
};

class AdjacentTo extends Edge {
};

Percorso più breve tra due sottoassiemi : algoritmo di attraversamento grafico semplice. I percorsi accettabili possono essere qualificati in base a criteri.

Somiglianza : qual è il grado di somiglianza tra due assemblee? Esegui un attraversamento su entrambi i sotto-alberi calcolando l'intersezione e l'unione dei due sotto-alberi. La percentuale simile è l'intersezione divisa per l'unione.

Chiusura transitiva : percorri l'albero secondario e riassumi i campi di interesse, ad esempio "Quanto alluminio è presente in un sottoassieme?"

Sì, puoi risolvere il problema con SQL e un database relazionale. Tuttavia, ci sono approcci molto migliori se si è disposti a utilizzare lo strumento giusto per il lavoro.


5
Questa risposta sarebbe immensamente più utile se i casi d'uso dimostrassero, o meglio ancora contrastassero, come interrogare un database grafico con SPARQL, ad esempio anziché SQL in un RDBMS.
orangepips,

1
SPARQL è rilevante per i database RDF che sono una sottoclasse del dominio più ampio dei database di grafi. Lavoro con InfiniteGraph che non è un database RDF e attualmente non supporta SPARQL. InfiniteGraph supporta diversi meccanismi di query: (1) un'API di navigazione dei grafici per l'impostazione di viste, filtri, qualificatori di percorso e gestori dei risultati, (2) un linguaggio di corrispondenza dei tracciati grafici complessi e (3) Gremlin.
Djhallx,

6

Sto usando PostgreSQL con tabelle di chiusura per le mie gerarchie. Ho una procedura memorizzata universale per l'intero database:

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

Quindi per ogni tabella in cui ho una gerarchia, creo un 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');

Per popolare una tabella di chiusura dalla gerarchia esistente utilizzo questa procedura memorizzata:

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

Le tabelle di chiusura sono definite con 3 colonne: ANCESTOR_ID, DESCENDANT_ID, DEPTH. È possibile (e consiglio anche io) di archiviare record con lo stesso valore per ANCESTOR e DESCENDANT e un valore zero per DEPTH. Ciò semplificherà le query per il recupero della gerarchia. E sono davvero molto semplici:

-- 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;
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.