¿Puedo obtener una estructura de árbol de una tabla autorreferenciada (jerárquica)?

8

Dada una tabla jerárquica como esta:

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

Me gustaría obtener toda la estructura del árbol.

Por ejemplo, usando estos datos:

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

Me gustaría obtener:

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

Estoy buscando registros usando una consulta recursiva como esta:

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

Y este es el resultado actual:

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

No puedo entender cómo ordenarlo por niveles.

¿Hay alguna manera de establecer un rango para cada subnivel?

He establecido un Rextester

McNets
fuente

Respuestas:

7

Agregue un campo "ruta" y ordénelo de manera similar a una ruta de archivo. Como mencionó ypercube, la clasificación es demasiado simplista en este ejemplo y simplemente funciona, pero por simplicidad, me iré como está. La mayoría de las veces cuando uso este patrón, ordeno por nombre en lugar de ID de todos modos.

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

Aquí un rextester

Ben Campbell
fuente
Es la idea correcta, pero en la expresión de ruta debería haberse c2.idreemplazado por un número de fila y rellenado a la izquierda para que todas las partes tengan la misma longitud. De lo contrario, no funcionará para todos los datos. Simplemente reemplace 2 con 55 en los datos y el orden cambia
ypercubeᵀᴹ
Totalmente de acuerdo. Estoy en el móvil y quería ganar la carrera para la respuesta :) En realidad, generalmente usaría el campo "nombre" en la ruta. Ese suele ser mi caso de uso.
Ben Campbell
Probablemente estoy equivocado sobre el número de fila (no es necesario) pero el relleno sí. +1 (¡Si usamos row_number, la ruta reconstruirá la primera parte del nombre!)
ypercubeᵀᴹ
He editado el Pathcon una pequeña corrección, para agregar relleno.
ypercubeᵀᴹ
1
Usualmente uso el doble de la longitud de mi ruta esperada si hay alguna duda sobre la profundidad máxima. También puede reducir el relleno cero si conoce el orden máximo de magnitud de la ID / número_fila.
Ben Campbell
4

Hacer trampa, solo un poco;) ¡Mira mamá, no hay recurrencia!

Probado en 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 ;

Por supuesto, lo anterior es bastante limitado. Funciona solo bajo los supuestos:

  • la namecolumna ha almacenado (en la primera parte) la "ruta" real.
  • la profundidad del árbol es máxima 4 (por lo que el camino tiene hasta 4 partes).
  • el CAST .. AS intse necesita sólo si las partes son números.

Explicación: El código funciona utilizando la función PARSENAME()que tiene el propósito principal de dividir un nombre de objeto en sus 4 partes:

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

Tenga en cuenta que el orden es inverso. Como ejemplo, PARSENAME('dbo.btree', 2)nos dará 'dbo'como resultado. Con 3, obtendremos NULL (es por eso que REVERSE()se usa dos veces en el código. De lo contrario, obtendríamos los valores nulos al principio. Se '1.2'analizarían null, null, 1, 2mientras queramos) 1, 2, null, null. )


Conclusión: después de todo eso, debo agregar que la respuesta de Bob Campbel es el camino a seguir, ya que es más general y produce (en la columna "ruta" en el resultado) la jerarquía de ruta, que luego puede usarse para el ORDER BY.

Otras opciones que puede considerar, si el tamaño de la tabla crece y la solución recursiva se vuelve lenta, es almacenar la ruta en una columna separada (en un formato que sea bueno para ordenar, es decir, con relleno) o usar el proporcionado HierarchyIDtipo que es exactamente para este caso de uso, datos jerárquicos.

ypercubeᵀᴹ
fuente
:) ¡Es realmente asombroso! Lamentablemente nameno se puede utilizar en este caso. Me llevará toda la noche descifrarlo, ¿podría tener alguna explicación?
McNets
Entonces, ¿la columna "nombre" no tiene los datos que proporcionó en el ejemplo? Lástima.
ypercubeᵀᴹ
No, lo he usado como ejemplo, solo para comentar que hay algunos niveles.
McNets
1
@Mcnets en el caso (improbable) de que namealmacene una ruta (con texto), por ejemplo 'order173.palletA27.box9'.bag3A, aún podría usar el código (simplemente elimine los moldes a int). En cualquier caso, la consulta de BenCambell es el camino a seguir en general.
ypercubeᵀᴹ
1
@EvanCarroll sí, el tipo de jerarquía. Estaba agregando un último párrafo sobre otras opciones con enlace.
ypercubeᵀᴹ