Kann ich eine Baumstruktur aus einer selbstreferenzierten (hierarchischen) Tabelle erhalten?

8

Bei einer hierarchischen Tabelle wie dieser:

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

Ich möchte die gesamte Baumstruktur erhalten.

Verwenden Sie zum Beispiel diese Daten:

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

Ich möchte erhalten:

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

Ich rufe Datensätze mit einer rekursiven Abfrage wie der folgenden ab:

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

Und das ist das aktuelle Ergebnis:

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

Ich kann nicht herausfinden, wie ich es nach Ebenen ordnen soll.

Gibt es eine Möglichkeit, für jede Unterebene einen Rang festzulegen?

Ich habe eine eingerichtet Rextester

McNets
quelle

Antworten:

7

Fügen Sie ein "Pfad" -Feld hinzu und sortieren Sie danach ähnlich einem Dateipfad. Wie von ypercube erwähnt, ist die Sortierung in diesem Beispiel zu einfach und funktioniert zufällig, aber der Einfachheit halber lasse ich sie unverändert. Die meiste Zeit, wenn ich dieses Muster verwende, sortiere ich sowieso eher nach Namen als nach 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)
;

Hier ein Rextester

Ben Campbell
quelle
Es ist die richtige Idee, aber im Pfad sollte der Ausdruck c2.iddurch eine row_number ersetzt und links aufgefüllt werden, damit alle Teile gleich lang sind. Andernfalls funktioniert es nicht für alle Daten. Ersetzen Sie einfach 2 durch 55 in den Daten und die Reihenfolge ändert sich
ypercubeᵀᴹ
Stimme voll und ganz zu. Ich bin auf dem Handy und wollte das Rennen um die Antwort gewinnen :) Eigentlich würde ich das Feld "Name" im Pfad allgemein verwenden. Das ist normalerweise mein Anwendungsfall.
Ben Campbell
Ich bin wahrscheinlich falsch in Bezug auf die Zeilennummer (nicht erforderlich), aber die Auffüllung ist. +1 (Wenn wir row_number verwenden, rekonstruiert der Pfad den ersten Teil des Namens!)
ypercubeᵀᴹ
Ich habe das Pathmit einer kleinen Korrektur bearbeitet , um Polsterung hinzuzufügen.
Ypercubeᵀᴹ
1
Normalerweise verwende ich die doppelte erwartete Pfadlänge, wenn Zweifel an der maximalen Tiefe bestehen. Sie können auch die Null-Auffüllung reduzieren, wenn Sie die maximale Größenordnung der ID / Zeilennummer kennen.
Ben Campbell
4

Schummeln, nur ein bisschen;) Schau ma, keine Rekursion!

Getestet bei 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 ;

Natürlich ist das oben Genannte eher begrenzt. Es funktioniert nur unter den Voraussetzungen:

  • Die nameSpalte hat (im ersten Teil) den tatsächlichen "Pfad" gespeichert.
  • Die Tiefe des Baumes beträgt maximal 4 (der Pfad besteht also aus bis zu 4 Teilen).
  • Das CAST .. AS intwird nur benötigt, wenn die Teile Zahlen sind.

Erläuterung: Der Code verwendet die Funktion PARSENAME(), die hauptsächlich dazu dient, einen Objektnamen in vier Teile aufzuteilen:

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

Beachten Sie, dass die Reihenfolge umgekehrt ist. Als Beispiel erhalten PARSENAME('dbo.btree', 2)wir 'dbo'als Ergebnis. Mit 3 erhalten wir NULL (deshalb REVERSE()wird das zweimal im Code verwendet. Andernfalls würden wir die Nullen am Anfang erhalten. Das '1.2'würde analysiert, null, null, 1, 2solange wir wollen 1, 2, null, null. )


Fazit: Nach all dem sollte ich hinzufügen, dass die Antwort von Bob Campbel der richtige Weg ist, da sie allgemeiner ist und (in der Spalte "Pfad" im Ergebnis) die Pfadhierarchie erzeugt, die dann für die verwendet werden kann ORDER BY.

Andere Optionen, die Sie in Betracht ziehen können - wenn die Größe der Tabelle größer wird und die rekursive Lösung langsam wird -, bestehen darin, den Pfad tatsächlich in einer separaten Spalte (in einem für die Bestellung geeigneten Format, dh mit Auffüllung) zu speichern oder das bereitgestellte zu verwenden HierarchyIDTyp, der genau für diesen Anwendungsfall ist, hierarchische Daten.

ypercubeᵀᴹ
quelle
:) Es ist wirklich super! nameKann in diesem Fall leider nicht verwendet werden. Ich werde die ganze Nacht brauchen, um es zu entziffern. Könnte ich eine Erklärung haben?
McNets
Die Spalte "Name" enthält also nicht die Daten, die Sie im Beispiel angegeben haben? Das Mitleid.
Ypercubeᵀᴹ
Nein, ich habe es als Beispiel verwendet, nur um zu bemerken, dass es einige Ebenen gibt.
McNets
1
@Mcnets in dem (unwahrscheinlichen) Fall, dass das nameeinen Pfad (mit Text) speichert 'order173.palletA27.box9'.bag3A, könnten Sie den Code trotzdem verwenden (entfernen Sie einfach die Casts zu int). In jedem Fall ist die Abfrage von BenCambell der richtige Weg.
Ypercubeᵀᴹ
1
@EvanCarroll ja, der Hierarchie-ID-Typ. Ich habe gerade einen letzten Absatz über andere Optionen mit Link hinzugefügt.
Ypercubeᵀᴹ