Posso obter uma estrutura em árvore a partir de uma tabela auto-referida (hierárquica)?

8

Dada uma tabela hierárquica como esta:

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

Eu gostaria de obter toda a estrutura da árvore.

Por exemplo, usando estes dados:

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

Eu gostaria de obter:

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

Estou buscando registros usando uma 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)
;

E este é o resultado atual:

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

Não consigo descobrir como ordená-lo por níveis.

Existe uma maneira de definir uma classificação para cada subnível?

Eu montei um Rextester

McNets
fonte

Respostas:

7

Adicione um campo "caminho" e classifique por um semelhante ao caminho do arquivo. Como o ypercube mencionou, a classificação é simplista demais neste exemplo e só funciona, mas por uma questão de simplicidade, deixarei como está. Na maioria das vezes, quando uso esse padrão, classifico por nome e não por ID de qualquer maneira.

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

Aqui um rextester

Ben Campbell
fonte
É a idéia certa, mas no caminho a expressão deve ter sido c2.idsubstituída por um número de linha e preenchida à esquerda, para que todas as partes tenham o mesmo comprimento. Caso contrário, não funcionará para todos os dados. Basta substituir 2 com 55 dos dados e das mudanças de ordem
ypercubeᵀᴹ
Concordo plenamente. Estou no celular e queria ganhar a corrida pela resposta :) Na verdade, usaria o campo "nome" no caminho em geral. Esse é geralmente o meu caso de uso.
Ben Campbell
Provavelmente estou errado sobre o número da linha (não necessário), mas o preenchimento é. +1 (Se fizermos uso row_number, o caminho vai reconstruir a primeira parte do nome!)
ypercubeᵀᴹ
Eu editei o Pathcom uma pequena correção, para adicionar preenchimento.
precisa saber é o seguinte
11
Normalmente, uso o dobro do comprimento esperado do caminho se houver alguma dúvida quanto à profundidade máxima. Além disso, você pode reduzir o preenchimento zero se souber a ordem máxima de magnitude do ID / número da linha.
Ben Campbell
4

Trapaça, só um pouquinho;) Olha ma, sem recursão!

Testado em 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 ;

Claro que o acima exposto é bastante limitado. Funciona apenas sob as premissas:

  • a namecoluna armazenou (na primeira parte) o "caminho" real.
  • a profundidade da árvore é máxima de 4 (portanto, o caminho tem até 4 partes).
  • o CAST .. AS inté necessário apenas se as peças forem números.

Explicação: O código funciona usando a função PARSENAME()que tem como principal objetivo dividir o nome de um objeto em suas 4 partes:

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

Observe que a ordem é inversa. Como exemplo, PARSENAME('dbo.btree', 2)nos dará 'dbo'como resultado. Com 3, obteremos NULL (é por isso que REVERSE()é usado duas vezes no código. Caso contrário, obteríamos os nulos no início. Eles '1.2'seriam analisados null, null, 1, 2quando desejássemos 1, 2, null, null. )


Conclusão: depois de tudo isso, devo acrescentar que a resposta de Bob Campbel é o caminho a seguir, pois é mais geral e produz (na coluna "caminho" no resultado) a hierarquia de caminhos, que pode ser usada para o ORDER BY.

Outras opções que você pode considerar - se o tamanho da tabela aumentar e a solução recursiva ficar lenta - é realmente armazenar o caminho em uma coluna separada (em um formato adequado para pedidos, por exemplo, com preenchimento) ou usar o fornecido HierarchyIDtipo exatamente para esse caso de uso, dados hierárquicos.

ypercubeᵀᴹ
fonte
:) É realmente incrível! Infelizmente namenão pode ser usado neste caso. Levarei a noite toda para decifrá-lo, eu poderia ter alguma explicação?
McNets
Portanto, a coluna "nome" não possui os dados que você forneceu no exemplo? Pena.
precisa saber é o seguinte
Não, eu usei como exemplo, apenas para observar que existem alguns níveis.
McNets
11
@Mcnets no caso (improvável) de namearmazenar um caminho (com texto), como 'order173.palletA27.box9'.bag3A, você ainda pode usar o código (basta remover os lançamentos para int). De qualquer forma, a consulta de BenCambell é o caminho a percorrer em geral.
usar o seguinte comando
11
@EvanCarroll sim, o tipo hierarchyid. Eu estava apenas adicionando um último parágrafo sobre outras opções com o link.
precisa saber é o seguinte