Posso ottenere una struttura ad albero da una tabella autoreferenziata (gerarchica)?


8

Data una tabella gerarchica come questa:

CREATE TABLE [dbo].[btree]
(
  id INT PRIMARY KEY
, parent_id INT REFERENCES [dbo].[btree] ([id])
, name NVARCHAR(20)
);

Vorrei ottenere l'intera struttura ad albero.

Ad esempio, usando questi dati:

INSERT INTO [btree] VALUES (1, null, '1 Root');
INSERT INTO [btree] VALUES (2,    1, '1.1 Group');
INSERT INTO [btree] VALUES (3,    1, '1.2 Group');
INSERT INTO [btree] VALUES (4,    2, '1.1.1 Group');
INSERT INTO [btree] VALUES (5,    2, '1.1.2 Group');
INSERT INTO [btree] VALUES (6,    3, '1.2.1 Group');
INSERT INTO [btree] VALUES (7,    3, '1.2.2 Group');
INSERT INTO [btree] VALUES (8,    4, '1.1.1.1 Items');
INSERT INTO [btree] VALUES (9,    4, '1.1.1.2 Items');
INSERT INTO [btree] VALUES (10,   5, '1.1.2.1 Items');
INSERT INTO [btree] VALUES (11,   5, '1.1.1.2 Items');
INSERT INTO [btree] VALUES (12,   6, '1.2.1.1 Items');
INSERT INTO [btree] VALUES (13,   6, '1.2.1.2 Items');
INSERT INTO [btree] VALUES (14,   7, '1.2.2.1 Items');

Vorrei ottenere:

+----+-----------+---------------------+
| id | parent_id | description         |
+----+-----------+---------------------+
|  1 |    NULL   | 1 Root              |
|  2 |     1     |   1.1 Group         |
|  4 |     2     |     1.1.1 Group     |
|  8 |     4     |       1.1.1.1 Items |
|  9 |     4     |       1.1.1.2 Items |
|  5 |     2     |     1.1.2 Group     |
| 10 |     5     |       1.1.2.1 Items |
| 11 |     5     |       1.1.2.2 Items |
|  3 |     1     |   1.2 Group         |
|  6 |     3     |     1.2.1 Group     |
| 12 |     6     |       1.2.1.1 Items |
| 13 |     6     |       1.2.1.2 Items |
|  7 |     3     |     1.2.2 Group     |
| 14 |     7     |       1.2.2.1 Items |
+----+-----------+---------------------+

Sto recuperando i record utilizzando una query ricorsiva come questa:

;WITH tree AS
(
    SELECT c1.id, c1.parent_id, c1.name, [level] = 1
    FROM dbo.[btree] c1
    WHERE c1.parent_id IS NULL
    UNION ALL
    SELECT c2.id, c2.parent_id, c2.name, [level] = tree.[level] + 1
    FROM dbo.[btree] c2 INNER JOIN tree ON tree.id = c2.parent_id
)
SELECT tree.level, tree.id, parent_id, REPLICATE('  ', tree.level - 1) + tree.name AS description
FROM tree
OPTION (MAXRECURSION 0)
;

E questo è il risultato attuale:

+----+-----------+---------------------+
| id | parent_id | description         |
|  1 |    NULL   | 1 Root              |
|  2 |     1     |   1.1 Group         |
|  3 |     1     |   1.2 Group         |
|  6 |     3     |     1.2.1 Group     |
|  7 |     3     |     1.2.2 Group     |
| 14 |     7     |       1.2.2.1 Items |
| 12 |     6     |       1.2.1.1 Items |
| 13 |     6     |       1.2.1.2 Items |
|  4 |     2     |     1.1.1 Group     |
|  5 |     2     |     1.1.2 Group     |
| 10 |     5     |       1.1.2.1 Items |
| 11 |     5     |       1.1.1.2 Items |
|  8 |     4     |       1.1.1.1 Items |
|  9 |     4     |       1.1.1.2 Items |
+----+-----------+---------------------+

Non riesco a capire come ordinarlo per livelli.

Esiste un modo per stabilire una classifica per ciascun sotto-livello?

Ho creato un Rextester

Risposte:


7

Aggiungi un campo "percorso" e ordina quello simile a un percorso file. Come menzionato in ypercube, l'ordinamento è eccessivamente semplicistico in questo esempio e sembra funzionare, ma per semplicità partirò così com'è. La maggior parte delle volte quando uso questo modello, ordino comunque per nome anziché per ID.

IF OBJECT_ID('[dbo].[btree]', 'U') IS NOT NULL 
    DROP TABLE [dbo].[btree];
GO

CREATE TABLE [dbo].[btree]
(
  id INT PRIMARY KEY
, parent_id INT REFERENCES [dbo].[btree] ([id])
, name NVARCHAR(20)
);
GO

INSERT INTO [btree] VALUES (1, null, '1 Root');
INSERT INTO [btree] VALUES (2,    1, '1.1 Group');
INSERT INTO [btree] VALUES (3,    1, '1.2 Group');
INSERT INTO [btree] VALUES (4,    2, '1.1.1 Group');
INSERT INTO [btree] VALUES (5,    2, '1.1.2 Group');
INSERT INTO [btree] VALUES (6,    3, '1.2.1 Group');
INSERT INTO [btree] VALUES (7,    3, '1.2.2 Group');
INSERT INTO [btree] VALUES (8,    4, '1.1.1.1 Items');
INSERT INTO [btree] VALUES (9,    4, '1.1.1.2 Items');
INSERT INTO [btree] VALUES (10,   5, '1.1.2.1 Items');
INSERT INTO [btree] VALUES (11,   5, '1.1.2.2 Items');
INSERT INTO [btree] VALUES (12,   6, '1.2.1.1 Items');
INSERT INTO [btree] VALUES (13,   6, '1.2.1.2 Items');
INSERT INTO [btree] VALUES (14,   7, '1.2.2.1 Items');

;WITH tree AS
(
    SELECT c1.id, c1.parent_id, c1.name, [level] = 1, path = cast('root' as varchar(100))
    FROM dbo.[btree] c1
    WHERE c1.parent_id IS NULL
    UNION ALL
    SELECT c2.id, c2.parent_id, c2.name, [level] = tree.[level] + 1, 
           Path = Cast(tree.path+'/'+right('000000000' + cast(c2.id as varchar(10)),10) as varchar(100))
    FROM dbo.[btree] c2 INNER JOIN tree ON tree.id = c2.parent_id
)
SELECT tree.path, tree.id, parent_id, REPLICATE('  ', tree.level - 1) + tree.name AS description
FROM tree
Order by path
OPTION (MAXRECURSION 0)
;

Qui un rextester


È l'idea giusta ma nell'espressione del percorso avrebbe dovuto essere c2.idsostituita con un numero di riga e riempita a sinistra in modo che tutte le parti abbiano la stessa lunghezza. Altrimenti, non funzionerà per tutti i dati. Basta sostituire 2 con 55 nei dati e l'ordine cambia
ypercubeᵀᴹ

Completamente d'accordo. Sono su cellulare e volevo vincere la gara alla risposta :) In realtà avrei usato il campo "nome" nel percorso in generale. Di solito è il mio caso d'uso.
Ben Campbell,

Probabilmente sbaglio sul numero di riga (non necessario) ma l'imbottitura lo è. +1 (Se utilizziamo row_number, il percorso ricostruirà la prima parte del nome!)
ypercubeᵀᴹ

Ho modificato il Pathcon una piccola correzione, per aggiungere il riempimento.
ypercubeᵀᴹ

1
Di solito uso il doppio della lunghezza del mio percorso prevista in caso di dubbi sulla profondità massima. Inoltre, è possibile ridurre il riempimento zero se si conosce l'ordine di grandezza massimo dell'ID / numero_di_stampa.
Ben Campbell,

4

Imbroglione, solo un po ';) Ma guarda, nessuna ricorsione!

Testato su rextester.com

SELECT btree.*        -- , a,b,c,d     -- uncomment to see the parts
FROM btree 
  OUTER APPLY
    ( SELECT rlc = REVERSE(LEFT(name, CHARINDEX(' ', name)-1))) AS r
  OUTER APPLY
    ( SELECT a = CAST(REVERSE(PARSENAME(r.rlc, 1)) AS int),
             b = CAST(REVERSE(PARSENAME(r.rlc, 2)) AS int),
             c = CAST(REVERSE(PARSENAME(r.rlc, 3)) AS int),
             d = CAST(REVERSE(PARSENAME(r.rlc, 4)) AS int)
    ) AS p 
ORDER BY a, b, c, d ;

Naturalmente quanto sopra è piuttosto limitato. Funziona solo con le ipotesi:

  • la namecolonna ha memorizzato (nella prima parte) l'attuale "percorso".
  • la profondità dell'albero è al massimo 4 (quindi il percorso ha fino a 4 parti).
  • l' CAST .. AS intè necessario solo se le parti sono numeri.

Spiegazione: Il codice funziona utilizzando la funzione PARSENAME()che ha lo scopo principale di suddividere il nome di un oggetto in 4 parti:

Server.Database.Schema.Object
  |        |       |      |
 4th      3rd     2nd    1st

Si noti che l'ordine è inverso. Come esempio, PARSENAME('dbo.btree', 2)ci darà 'dbo'come risultato. Con 3, otterremo NULL (ecco perché REVERSE()viene usato due volte nel codice. Altrimenti avremmo i null all'inizio. '1.2'Sarebbe analizzato null, null, 1, 2mentre vorremmo 1, 2, null, null. )


Conclusione: dopo tutto ciò, dovrei aggiungere che la risposta di Bob Campbel è la strada da percorrere in quanto è più generale e produce (nella colonna "percorso" nel risultato) la gerarchia del percorso, che può quindi essere utilizzata per il ORDER BY.

Altre opzioni che potresti prendere in considerazione - se le dimensioni della tabella diventano grandi e la soluzione ricorsiva diventa lenta - è in realtà memorizzare il percorso in una colonna separata (in un formato adatto all'ordinamento, ad esempio con il riempimento) o utilizzare il fornito HierarchyIDtipo che è esattamente per questo caso d'uso, dati gerarchici.


:) È davvero fantastico! Purtroppo namenon può essere utilizzato in questo caso. Mi ci vorrà tutta la notte per decifrarlo, potrei avere qualche spiegazione?
McNets,

Quindi, la colonna "nome" non contiene i dati che hai fornito nell'esempio? Pietà.
ypercubeᵀᴹ

No, l'ho usato come esempio, solo per notare che ci sono alcuni livelli.
McNets,

1
@Mcnets nel caso (improbabile) in cui namememorizza un percorso (con testo), ad esempio 'order173.palletA27.box9'.bag3A, potresti comunque usare il codice (rimuovi i cast per int). In ogni caso, la query di BenCambell è la strada da percorrere in generale.
ypercubeᵀᴹ

1
@EvanCarroll sì, il tipo hierarchyid. Stavo solo aggiungendo un ultimo paragrafo su altre opzioni con link.
ypercubeᵀᴹ
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.