Могу ли я получить древовидную структуру из таблицы, на которую ссылаются (иерархической)?

8

Имеется иерархическая таблица, подобная этой:

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

Я хотел бы получить всю древовидную структуру.

Например, используя эти данные:

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

Я хотел бы получить:

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

Я выбираю записи, используя рекурсивный запрос, подобный этому:

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

И это текущий результат:

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

Я не могу понять, как заказать его по уровням.

Есть ли способ установить ранг для каждого подуровня?

Я создал Rextester

McNets
источник

Ответы:

7

Добавьте поле «путь» и отсортируйте его по аналогии с путем к файлу. Как упоминал ypercube, в этом примере сортировка слишком упрощена и просто работает, но для простоты я оставлю все как есть. Большую часть времени, когда я использую этот шаблон, я в любом случае сортирую по имени, а не по идентификатору.

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

Здесь ректестер

Бен Кэмпбелл
источник
Это правильная идея, но в выражении пути следовало бы c2.idзаменить row_number и дополнить его слева, чтобы все части имели одинаковую длину. В противном случае это не сработает для всех данных. Просто замените 2 на 55 в данных и порядок изменится
ypercube
Полностью согласен. Я на мобильном и хотел выиграть гонку, чтобы ответить :) На самом деле я бы использовал поле «имя» в пути вообще. Это обычно мой вариант использования.
Бен Кэмпбелл
Я, наверное, ошибаюсь насчет номера строки (не нужен), но отступы есть. +1 (если мы используем row_number, путь восстановит первую часть имени!)
ypercubeᵀᴹ
Я отредактировал Pathс небольшой поправкой, чтобы добавить отступы.
ypercubeᵀᴹ
1
Я обычно использую удвоенную ожидаемую длину пути, если есть сомнения относительно максимальной глубины. Также вы можете уменьшить заполнение нулями, если вы знаете максимальный порядок величины ID / row_number.
Бен Кэмпбелл
4

Обман, только немного;) Смотри, мам, нет рекурсии!

Проверено на 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 ;

Конечно, вышесказанное довольно ограничено. Работает только при допущениях:

  • nameстолбец сохраняется (в первой части) фактического «пути».
  • глубина дерева максимально 4 (поэтому путь имеет до 4 частей).
  • CAST .. AS intнужно только если части номер.

Объяснение: Код работает с использованием функции PARSENAME(), основной целью которой является разбиение имени объекта на 4 части:

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

Обратите внимание, что порядок обратный. Как пример, PARSENAME('dbo.btree', 2)даст нам 'dbo'в результате. С 3 мы получим NULL (вот почему REVERSE()в коде используется дважды. В противном случае мы получили бы нулевые значения в начале. Они '1.2'будут проанализированы, null, null, 1, 2когда мы захотим 1, 2, null, null. )


Вывод: после всего этого я должен добавить, что ответ Боба Кэмпбела - это путь, поскольку он более общий и создает (в столбце «путь» в результате) иерархию путей, которую затем можно использовать для ORDER BY.

Другие варианты, которые вы можете рассмотреть - если размер таблицы увеличивается, а рекурсивное решение становится медленным, - это фактически сохранить путь в отдельном столбце (в формате, который удобен для упорядочения, то есть с отступом) или использовать предоставленный HierarchyIDтип, который именно для этого варианта использования, иерархические данные.

ypercubeᵀᴹ
источник
:) Это действительно круто! К сожалению, nameне может быть использовано в этом случае. Мне понадобится вся ночь, чтобы расшифровать его, могу ли я дать какое-нибудь объяснение?
Макнетс
Итак, в столбце «имя» нет данных, которые вы указали в примере? Жалость.
ypercubeᵀᴹ
Нет, я использовал это как пример, просто чтобы отметить, что есть несколько уровней.
McNets
1
@Mcnets в (маловероятном) случае, когда nameон хранит путь (с текстом), например 'order173.palletA27.box9'.bag3A, вы все равно можете использовать код (просто удалите приведения к int). В любом случае, запрос от BenCambell - это общий путь.
ypercubeᵀᴹ
1
@EvanCarroll да, тип иерархии. Я просто добавил последний абзац о других опциях со ссылкой.
ypercubeᵀᴹ