Puis-je obtenir une arborescence à partir d'une table auto-référencée (hiérarchique)?


8

Étant donné un tableau hiérarchique comme celui-ci:

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

Je souhaite obtenir la structure arborescente complète.

Par exemple, en utilisant ces données:

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

Je souhaite obtenir:

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

Je récupère des enregistrements à l'aide d'une requête récursive comme celle-ci:

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

Et voici le résultat actuel:

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

Je ne sais pas comment le classer par niveaux.

Existe-t-il un moyen de définir un classement pour chaque sous-niveau?

J'ai mis en place un Rextester

Réponses:


7

Ajoutez un champ "chemin d'accès" et triez-le comme un chemin de fichier. Comme ypercube l'a mentionné, le tri est trop simpliste dans cet exemple et se trouve juste fonctionner, mais pour des raisons de simplicité, je vais le laisser tel quel. La plupart du temps, lorsque j'utilise ce modèle, je trie par nom plutôt que par ID de toute façon.

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

Voici un rextester


C'est la bonne idée, mais dans le chemin, l'expression aurait dû être c2.idremplacée par un numéro de ligne et complétée à gauche de sorte que toutes les parties aient la même longueur. Sinon, cela ne fonctionnera pas pour toutes les données. Remplacez simplement 2 par 55 dans les données et la commande change
ypercubeᵀᴹ

Entièrement d'accord. Je suis sur mobile et je voulais gagner la course à la réponse :) En fait, j'utiliserais le champ "nom" dans le chemin en général. C'est généralement mon cas d'utilisation.
Ben Campbell

Je me trompe probablement sur le numéro de ligne (pas nécessaire) mais le rembourrage l'est. +1 (Si nous utilisons row_number, le chemin reconstruira la première partie du nom!)
ypercubeᵀᴹ

J'ai édité le Pathavec une petite correction, pour ajouter du rembourrage.
ypercubeᵀᴹ

1
J'utilise généralement le double de la longueur de chemin prévue en cas de doute sur la profondeur maximale. Vous pouvez également réduire le remplissage nul si vous connaissez l'ordre de grandeur maximum de l'ID / row_number.
Ben Campbell

4

Tricher, juste un peu;) Regardez ma, pas de récursivité!

Testé sur 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 ;

Bien sûr, ce qui précède est plutôt limité. Cela ne fonctionne que sous les hypothèses:

  • la namecolonne a stocké (dans la première partie) le "chemin" réel.
  • la profondeur de l'arbre est au maximum de 4 (donc le chemin comporte jusqu'à 4 parties).
  • le CAST .. AS intn'est nécessaire que si les pièces sont des nombres.

Explication: Le code fonctionne en utilisant la fonction PARSENAME()qui a pour principal objectif de diviser un nom d'objet en ses 4 parties:

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

Notez que l'ordre est inversé. Par exemple, PARSENAME('dbo.btree', 2)nous donnera 'dbo'comme résultat.Avec 3, nous obtiendrons NULL (c'est pourquoi le REVERSE()est utilisé deux fois dans le code. Sinon, nous obtiendrions les null au début. Le '1.2'serait analysé null, null, 1, 2pendant que nous le voulons 1, 2, null, null. )


Conclusion: après tout cela, je dois ajouter que la réponse de Bob Campbel est la voie à suivre car elle est plus générale et produit (dans la colonne "chemin" du résultat) la hiérarchie des chemins, qui peut ensuite être utilisée pour le ORDER BY.

D'autres options que vous pouvez envisager - si la taille de la table augmente et que la solution récursive devient lente - consiste à stocker le chemin dans une colonne distincte (dans un format qui est bon pour la commande, c'est-à-dire avec un remplissage) ou à utiliser le fourni HierarchyIDtype qui correspond exactement à ce cas d'utilisation, les données hiérarchiques.


:) C'est vraiment génial! Malheureusement, namene peut pas être utilisé dans ce cas. Il me faudra toute la nuit pour le déchiffrer, pourrais-je avoir une explication?
McNets

Ainsi, la colonne "nom" ne contient pas les données que vous avez fournies dans l'exemple? Pitié.
ypercubeᵀᴹ

Non, je l'ai utilisé comme exemple, juste pour remarquer qu'il y a des niveaux.
McNets

1
@Mcnets dans le cas (peu probable) où le namestocke un chemin (avec du texte), comme 'order173.palletA27.box9'.bag3A, vous pouvez toujours utiliser le code (il suffit de supprimer les transtypages en int). Dans tous les cas, la requête de BenCambell est la voie à suivre en général.
ypercubeᵀᴹ

1
@EvanCarroll oui, le type hierarchyid. Je venais d'ajouter un dernier paragraphe sur d'autres options avec lien.
ypercubeᵀᴹ
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.